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

openmc-dev / openmc / 17323748789

29 Aug 2025 12:25PM UTC coverage: 85.22% (+0.01%) from 85.207%
17323748789

Pull #3525

github

web-flow
Merge d39487942 into 5529731e2
Pull Request #3525: fixed a bug in a combination of time_filter, mesh_filter and tracklength estimator

56 of 58 new or added lines in 3 files covered. (96.55%)

1 existing line in 1 file now uncovered.

53006 of 62199 relevant lines covered (85.22%)

38203793.71 hits per line

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

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

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

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

43
#include <algorithm> // for max
44
#include <cassert>
45
#include <cstddef> // for size_t
46
#include <string>
47

48
namespace openmc {
49

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

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

69
double distance_to_time_boundary(double time, double speed)
77,761,992✔
70
{
71
  if (time_grid.size() == 0) {
77,761,992✔
NEW
72
    return INFTY;
×
73
  } else if (time >= time_grid[time_grid.size() - 1]) {
77,761,992✔
74
    return INFTY;
269,896✔
75
  } else {
76
    return (*std::upper_bound(time_grid.begin(), time_grid.end(), time) -
77,492,096✔
77
             time) *
78
           speed;
77,492,096✔
79
  }
80
}
81

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

199
  this->set_nuclides(node);
24,249✔
200

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

204
  this->set_scores(node);
24,249✔
205

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

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

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

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

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

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

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

333
    deriv_ = it->second;
320✔
334

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

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

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

367
  // =======================================================================
368
  // SET TALLY ESTIMATOR
369

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

501
  for (auto* filter : filters) {
76,896✔
502
    add_filter(filter);
50,242✔
503
  }
504
}
26,654✔
505

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

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

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

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

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

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

552
  // Check for the presence of certain restrictive filters.
553
  bool energyout_present = energyout_filter_ != C_NONE;
25,454✔
554
  bool legendre_present = false;
25,454✔
555
  bool cell_present = false;
25,454✔
556
  bool cellfrom_present = false;
25,454✔
557
  bool surface_present = false;
25,454✔
558
  bool meshsurface_present = false;
25,454✔
559
  bool non_cell_energy_present = false;
25,454✔
560
  for (auto i_filt : filters_) {
75,167✔
561
    const auto* filt {model::tally_filters[i_filt].get()};
49,713✔
562
    // Checking for only cell and energy filters for pulse-height tally
563
    if (!(filt->type() == FilterType::CELL ||
98,353✔
564
          filt->type() == FilterType::ENERGY)) {
48,640✔
565
      non_cell_energy_present = true;
31,698✔
566
    }
567
    if (filt->type() == FilterType::LEGENDRE) {
49,713✔
568
      legendre_present = true;
2,045✔
569
    } else if (filt->type() == FilterType::CELLFROM) {
47,668✔
570
      cellfrom_present = true;
304✔
571
    } else if (filt->type() == FilterType::CELL) {
47,364✔
572
      cell_present = true;
1,073✔
573
    } else if (filt->type() == FilterType::SURFACE) {
46,291✔
574
      surface_present = true;
193✔
575
    } else if (filt->type() == FilterType::MESH_SURFACE) {
46,098✔
576
      meshsurface_present = true;
367✔
577
    }
578
  }
579

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

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

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

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

610
    case SCORE_SCATTER:
3,897✔
611
      if (legendre_present)
3,897✔
612
        estimator_ = TallyEstimator::ANALOG;
1,362✔
613
    case SCORE_NU_FISSION:
614
    case SCORE_DELAYED_NU_FISSION:
615
    case SCORE_PROMPT_NU_FISSION:
616
      if (energyout_present)
8,636✔
617
        estimator_ = TallyEstimator::ANALOG;
3,571✔
618
      break;
8,636✔
619

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

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

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

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

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

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

680
    scores_.push_back(score);
35,280✔
681
  }
35,280✔
682

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

840
  if (mpi::master || !settings::reduce_tallies) {
204,458✔
841
    // Calculate total source strength for normalization
842
    double total_source = 0.0;
166,693✔
843
    if (settings::run_mode == RunMode::FIXED_SOURCE) {
166,693✔
844
      total_source = model::external_sources_probability.integral();
44,367✔
845
    } else {
846
      total_source = 1.0;
122,326✔
847
    }
848

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

853
    if (settings::solver_type == SolverType::RANDOM_RAY) {
166,693✔
854
      norm = 1.0;
11,226✔
855
    }
856

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

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

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

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

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

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

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

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

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

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

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

935
  write_message("Reading tallies XML file...", 5);
463✔
936

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

942
  read_tallies_xml(root);
463✔
943
}
1,362✔
944

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

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

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

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

962
  // ==========================================================================
963
  // READ FILTER DATA
964

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

970
  // ==========================================================================
971
  // READ TALLY DATA
972

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1089
  // Accumulate results for each tally
1090
  for (int i_tally : model::active_tallies) {
316,068✔
1091
    auto& tally {model::tallies[i_tally]};
204,458✔
1092
    tally->accumulate();
204,458✔
1093
  }
1094
}
111,610✔
1095

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

1108
  for (auto i = 0; i < model::tallies.size(); ++i) {
445,060✔
1109
    const auto& tally {*model::tallies[i]};
333,438✔
1110

1111
    if (tally.active_) {
333,438✔
1112
      model::active_tallies.push_back(i);
204,458✔
1113
      bool mesh_present = ((tally.get_filter<MeshFilter>() != nullptr) ||
358,886✔
1114
                           (tally.get_filter<MeshMaterialFilter>() != nullptr));
154,428✔
1115
      auto time_filter = tally.get_filter<TimeFilter>();
204,458✔
1116
      switch (tally.type_) {
204,458✔
1117

1118
      case TallyType::VOLUME:
197,965✔
1119
        switch (tally.estimator_) {
197,965✔
1120
        case TallyEstimator::ANALOG:
81,203✔
1121
          model::active_analog_tallies.push_back(i);
81,203✔
1122
          break;
81,203✔
1123
        case TallyEstimator::TRACKLENGTH:
111,652✔
1124
          if ((time_filter != nullptr) && mesh_present) {
111,652✔
1125
            model::active_timed_tracklength_tallies.push_back(i);
220✔
1126
            model::add_to_time_grid(time_filter->bins());
220✔
1127
          } else {
1128
            model::active_tracklength_tallies.push_back(i);
111,432✔
1129
          }
1130
          break;
111,652✔
1131
        case TallyEstimator::COLLISION:
5,110✔
1132
          model::active_collision_tallies.push_back(i);
5,110✔
1133
        }
1134
        break;
197,965✔
1135

1136
      case TallyType::MESH_SURFACE:
4,311✔
1137
        model::active_meshsurf_tallies.push_back(i);
4,311✔
1138
        break;
4,311✔
1139

1140
      case TallyType::SURFACE:
2,102✔
1141
        model::active_surface_tallies.push_back(i);
2,102✔
1142
        break;
2,102✔
1143

1144
      case TallyType::PULSE_HEIGHT:
80✔
1145
        model::active_pulse_height_tallies.push_back(i);
80✔
1146
        break;
80✔
1147
      }
1148
    }
1149
  }
1150
}
111,622✔
1151

1152
void free_memory_tally()
7,010✔
1153
{
1154
  model::tally_derivs.clear();
7,010✔
1155
  model::tally_deriv_map.clear();
7,010✔
1156

1157
  model::tally_filters.clear();
7,010✔
1158
  model::filter_map.clear();
7,010✔
1159

1160
  model::tallies.clear();
7,010✔
1161

1162
  model::active_tallies.clear();
7,010✔
1163
  model::active_analog_tallies.clear();
7,010✔
1164
  model::active_tracklength_tallies.clear();
7,010✔
1165
  model::active_timed_tracklength_tallies.clear();
7,010✔
1166
  model::active_collision_tallies.clear();
7,010✔
1167
  model::active_meshsurf_tallies.clear();
7,010✔
1168
  model::active_surface_tallies.clear();
7,010✔
1169
  model::active_pulse_height_tallies.clear();
7,010✔
1170
  model::time_grid.clear();
7,010✔
1171

1172
  model::tally_map.clear();
7,010✔
1173
}
7,010✔
1174

1175
//==============================================================================
1176
// C-API functions
1177
//==============================================================================
1178

1179
extern "C" int openmc_extend_tallies(
1,095✔
1180
  int32_t n, int32_t* index_start, int32_t* index_end)
1181
{
1182
  if (index_start)
1,095✔
1183
    *index_start = model::tallies.size();
1,095✔
1184
  if (index_end)
1,095✔
1185
    *index_end = model::tallies.size() + n - 1;
×
1186
  for (int i = 0; i < n; ++i) {
2,190✔
1187
    model::tallies.push_back(make_unique<Tally>(-1));
1,095✔
1188
  }
1189
  return 0;
1,095✔
1190
}
1191

1192
extern "C" int openmc_get_tally_index(int32_t id, int32_t* index)
22,722✔
1193
{
1194
  auto it = model::tally_map.find(id);
22,722✔
1195
  if (it == model::tally_map.end()) {
22,722✔
1196
    set_errmsg(fmt::format("No tally exists with ID={}.", id));
22✔
1197
    return OPENMC_E_INVALID_ID;
22✔
1198
  }
1199

1200
  *index = it->second;
22,700✔
1201
  return 0;
22,700✔
1202
}
1203

1204
extern "C" void openmc_get_tally_next_id(int32_t* id)
×
1205
{
1206
  int32_t largest_tally_id = 0;
×
1207
  for (const auto& t : model::tallies) {
×
1208
    largest_tally_id = std::max(largest_tally_id, t->id_);
×
1209
  }
1210
  *id = largest_tally_id + 1;
×
1211
}
1212

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

1220
  *estimator = static_cast<int>(model::tallies[index]->estimator_);
×
1221
  return 0;
×
1222
}
1223

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

1231
  auto& t {model::tallies[index]};
720✔
1232

1233
  std::string est = estimator;
720✔
1234
  if (est == "analog") {
720✔
1235
    t->estimator_ = TallyEstimator::ANALOG;
720✔
1236
  } else if (est == "collision") {
×
1237
    t->estimator_ = TallyEstimator::COLLISION;
×
1238
  } else if (est == "tracklength") {
×
1239
    t->estimator_ = TallyEstimator::TRACKLENGTH;
×
1240
  } else {
1241
    set_errmsg("Unknown tally estimator: " + est);
×
1242
    return OPENMC_E_INVALID_ARGUMENT;
×
1243
  }
1244
  return 0;
720✔
1245
}
720✔
1246

1247
extern "C" int openmc_tally_get_id(int32_t index, int32_t* id)
29,277✔
1248
{
1249
  if (index < 0 || index >= model::tallies.size()) {
29,277✔
1250
    set_errmsg("Index in tallies array is out of bounds.");
×
1251
    return OPENMC_E_OUT_OF_BOUNDS;
×
1252
  }
1253

1254
  *id = model::tallies[index]->id_;
29,277✔
1255
  return 0;
29,277✔
1256
}
1257

1258
extern "C" int openmc_tally_set_id(int32_t index, int32_t id)
1,095✔
1259
{
1260
  if (index < 0 || index >= model::tallies.size()) {
1,095✔
1261
    set_errmsg("Index in tallies array is out of bounds.");
×
1262
    return OPENMC_E_OUT_OF_BOUNDS;
×
1263
  }
1264

1265
  model::tallies[index]->set_id(id);
1,095✔
1266
  return 0;
1,095✔
1267
}
1268

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

1277
  return 0;
11✔
1278
}
1279

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

1299
  return 0;
720✔
1300
}
1301

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

1310
  return 0;
22✔
1311
}
1312

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

1321
  return 0;
731✔
1322
}
1323

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

1332
  return 0;
22✔
1333
}
1334

1335
extern "C" int openmc_tally_set_writable(int32_t index, bool writable)
375✔
1336
{
1337
  if (index < 0 || index >= model::tallies.size()) {
375✔
1338
    set_errmsg("Index in tallies array is out of bounds.");
×
1339
    return OPENMC_E_OUT_OF_BOUNDS;
×
1340
  }
1341
  model::tallies[index]->set_writable(writable);
375✔
1342

1343
  return 0;
375✔
1344
}
1345

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

1354
  return 0;
22✔
1355
}
1356

1357
extern "C" int openmc_tally_set_multiply_density(int32_t index, bool value)
232✔
1358
{
1359
  if (index < 0 || index >= model::tallies.size()) {
232✔
1360
    set_errmsg("Index in tallies array is out of bounds.");
×
1361
    return OPENMC_E_OUT_OF_BOUNDS;
×
1362
  }
1363
  model::tallies[index]->set_multiply_density(value);
232✔
1364

1365
  return 0;
232✔
1366
}
1367

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

1375
  *scores = model::tallies[index]->scores_.data();
187✔
1376
  *n = model::tallies[index]->scores_.size();
187✔
1377
  return 0;
187✔
1378
}
1379

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

1388
  vector<std::string> scores_str(scores, scores + n);
1,106✔
1389
  try {
1390
    model::tallies[index]->set_scores(scores_str);
1,106✔
1391
  } catch (const std::invalid_argument& ex) {
×
1392
    set_errmsg(ex.what());
×
1393
    return OPENMC_E_INVALID_ARGUMENT;
×
1394
  }
×
1395

1396
  return 0;
1,106✔
1397
}
1,106✔
1398

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

1407
  *n = model::tallies[index]->nuclides_.size();
231✔
1408
  *nuclides = model::tallies[index]->nuclides_.data();
231✔
1409

1410
  return 0;
231✔
1411
}
1412

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

1422
  vector<std::string> words(nuclides, nuclides + n);
1,339✔
1423
  vector<int> nucs;
1,339✔
1424
  for (auto word : words) {
5,760✔
1425
    if (word == "total") {
4,432✔
1426
      nucs.push_back(-1);
720✔
1427
    } else {
1428
      auto search = data::nuclide_map.find(word);
3,712✔
1429
      if (search == data::nuclide_map.end()) {
3,712✔
1430
        int err = openmc_load_nuclide(word.c_str(), nullptr, 0);
809✔
1431
        if (err < 0) {
809✔
1432
          set_errmsg(openmc_err_msg);
11✔
1433
          return OPENMC_E_DATA;
11✔
1434
        }
1435
      }
1436
      nucs.push_back(data::nuclide_map.at(word));
3,701✔
1437
    }
1438
  }
4,432✔
1439

1440
  model::tallies[index]->nuclides_ = nucs;
1,328✔
1441

1442
  return 0;
1,328✔
1443
}
1,339✔
1444

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

1453
  *indices = model::tallies[index]->filters().data();
979✔
1454
  *n = model::tallies[index]->filters().size();
979✔
1455
  return 0;
979✔
1456
}
1457

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

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

1481
  return 0;
1,150✔
1482
}
1483

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

1493
  model::tallies[index]->reset();
2,928✔
1494
  return 0;
2,928✔
1495
}
1496

1497
extern "C" int openmc_tally_get_n_realizations(int32_t index, int32_t* n)
3,594✔
1498
{
1499
  // Make sure the index fits in the array bounds.
1500
  if (index < 0 || index >= model::tallies.size()) {
3,594✔
1501
    set_errmsg("Index in tallies array is out of bounds.");
×
1502
    return OPENMC_E_OUT_OF_BOUNDS;
×
1503
  }
1504

1505
  *n = model::tallies[index]->n_realizations_;
3,594✔
1506
  return 0;
3,594✔
1507
}
1508

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

1520
  const auto& t {model::tallies[index]};
17,230✔
1521
  if (t->results_.size() == 0) {
17,230✔
1522
    set_errmsg("Tally results have not been allocated yet.");
×
1523
    return OPENMC_E_ALLOCATE;
×
1524
  }
1525

1526
  // Set pointer to results and copy shape
1527
  *results = t->results_.data();
17,230✔
1528
  auto s = t->results_.shape();
17,230✔
1529
  shape[0] = s[0];
17,230✔
1530
  shape[1] = s[1];
17,230✔
1531
  shape[2] = s[2];
17,230✔
1532
  return 0;
17,230✔
1533
}
1534

1535
extern "C" int openmc_global_tallies(double** ptr)
11✔
1536
{
1537
  *ptr = simulation::global_tallies.data();
11✔
1538
  return 0;
11✔
1539
}
1540

1541
extern "C" size_t tallies_size()
1,139✔
1542
{
1543
  return model::tallies.size();
1,139✔
1544
}
1545

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

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

1559
  return 0;
11✔
1560
}
1561

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