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

openmc-dev / openmc / 21489819490

29 Jan 2026 06:21PM UTC coverage: 80.077% (-1.9%) from 81.953%
21489819490

Pull #3757

github

web-flow
Merge d08626053 into f7a734189
Pull Request #3757: Testing point detectors

16004 of 22621 branches covered (70.75%)

Branch coverage included in aggregate %.

94 of 518 new or added lines in 26 files covered. (18.15%)

1021 existing lines in 52 files now uncovered.

53779 of 64524 relevant lines covered (83.35%)

8016833.26 hits per line

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

71.59
/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/position.h"
15
#include "openmc/reaction.h"
16
#include "openmc/reaction_product.h"
17
#include "openmc/settings.h"
18
#include "openmc/simulation.h"
19
#include "openmc/source.h"
20
#include "openmc/tallies/derivative.h"
21
#include "openmc/tallies/filter.h"
22
#include "openmc/tallies/filter_cell.h"
23
#include "openmc/tallies/filter_cellborn.h"
24
#include "openmc/tallies/filter_cellfrom.h"
25
#include "openmc/tallies/filter_collision.h"
26
#include "openmc/tallies/filter_delayedgroup.h"
27
#include "openmc/tallies/filter_energy.h"
28
#include "openmc/tallies/filter_legendre.h"
29
#include "openmc/tallies/filter_mesh.h"
30
#include "openmc/tallies/filter_meshborn.h"
31
#include "openmc/tallies/filter_meshmaterial.h"
32
#include "openmc/tallies/filter_meshsurface.h"
33
#include "openmc/tallies/filter_particle.h"
34
#include "openmc/tallies/filter_point.h"
35
#include "openmc/tallies/filter_sph_harm.h"
36
#include "openmc/tallies/filter_surface.h"
37
#include "openmc/tallies/filter_time.h"
38
#include "openmc/xml_interface.h"
39

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

45
#include <algorithm> // for max, set_union
46
#include <cassert>
47
#include <cstddef>  // for size_t
48
#include <iterator> // for back_inserter
49
#include <string>
50

51
namespace openmc {
52

53
//==============================================================================
54
// Global variable definitions
55
//==============================================================================
56

57
namespace model {
58
//! a mapping of tally ID to index in the tallies vector
59
std::unordered_map<int, int> tally_map;
60
vector<unique_ptr<Tally>> tallies;
61
vector<int> active_tallies;
62
vector<int> active_analog_tallies;
63
vector<int> active_tracklength_tallies;
64
vector<int> active_timed_tracklength_tallies;
65
vector<int> active_collision_tallies;
66
vector<int> active_point_tallies;
67
vector<int> active_meshsurf_tallies;
68
vector<int> active_surface_tallies;
69
vector<int> active_pulse_height_tallies;
70
vector<int> pulse_height_cells;
71
vector<double> time_grid;
72
std::set<Position> active_point_detectors;
73
} // namespace model
74

75
namespace simulation {
76
xt::xtensor_fixed<double, xt::xshape<N_GLOBAL_TALLIES, 3>> global_tallies;
77
int32_t n_realizations {0};
78
} // namespace simulation
79

80
double global_tally_absorption;
81
double global_tally_collision;
82
double global_tally_tracklength;
83
double global_tally_leakage;
84

85
//==============================================================================
86
// Tally object implementation
87
//==============================================================================
88

89
Tally::Tally(int32_t id)
105✔
90
{
91
  index_ = model::tallies.size(); // Avoids warning about narrowing
105✔
92
  this->set_id(id);
105✔
93
  this->set_filters({});
105✔
94
}
105✔
95

96
Tally::Tally(pugi::xml_node node)
1,698✔
97
{
98
  index_ = model::tallies.size(); // Avoids warning about narrowing
1,698✔
99

100
  // Copy and set tally id
101
  if (!check_for_node(node, "id")) {
1,698!
102
    throw std::runtime_error {"Must specify id for tally in tally XML file."};
×
103
  }
104
  int32_t id = std::stoi(get_node_value(node, "id"));
1,698✔
105
  this->set_id(id);
1,698✔
106

107
  if (check_for_node(node, "name"))
1,698✔
108
    name_ = get_node_value(node, "name");
175✔
109

110
  if (check_for_node(node, "multiply_density")) {
1,698✔
111
    multiply_density_ = get_node_value_bool(node, "multiply_density");
5✔
112
  }
113

114
  if (check_for_node(node, "higher_moments")) {
1,698✔
115
    higher_moments_ = get_node_value_bool(node, "higher_moments");
1✔
116
  }
117
  // =======================================================================
118
  // READ DATA FOR FILTERS
119

120
  // Check if user is using old XML format and throw an error if so
121
  if (check_for_node(node, "filter")) {
1,698!
122
    throw std::runtime_error {
×
123
      "Tally filters must be specified independently of "
124
      "tallies in a <filter> element. The <tally> element itself should "
125
      "have a list of filters that apply, e.g., <filters>1 2</filters> "
126
      "where 1 and 2 are the IDs of filters specified outside of "
127
      "<tally>."};
×
128
  }
129

130
  // Determine number of filters
131
  vector<int> filter_ids;
1,698✔
132
  if (check_for_node(node, "filters")) {
1,698✔
133
    filter_ids = get_node_array<int>(node, "filters");
1,622✔
134
  }
135

136
  // Allocate and store filter user ids
137
  vector<Filter*> filters;
1,698✔
138
  for (int filter_id : filter_ids) {
5,034✔
139
    // Determine if filter ID is valid
140
    auto it = model::filter_map.find(filter_id);
3,336✔
141
    if (it == model::filter_map.end()) {
3,336!
142
      throw std::runtime_error {fmt::format(
×
143
        "Could not find filter {} specified on tally {}", filter_id, id_)};
×
144
    }
145

146
    // Store the index of the filter
147
    filters.push_back(model::tally_filters[it->second].get());
3,336✔
148
  }
149

150
  // Set the filters
151
  this->set_filters(filters);
1,698✔
152

153
  // Check for the presence of certain filter types
154
  bool has_energyout = energyout_filter_ >= 0;
1,698✔
155
  int particle_filter_index = C_NONE;
1,698✔
156
  for (int64_t j = 0; j < filters_.size(); ++j) {
5,034✔
157
    int i_filter = filters_[j];
3,336✔
158
    const auto& f = model::tally_filters[i_filter].get();
3,336✔
159

160
    auto pf = dynamic_cast<ParticleFilter*>(f);
3,336!
161
    if (pf)
3,336✔
162
      particle_filter_index = i_filter;
32✔
163

164
    // Change the tally estimator if a filter demands it
165
    FilterType filt_type = f->type();
3,336✔
166
    if (filt_type == FilterType::ENERGY_OUT ||
3,336✔
167
        filt_type == FilterType::LEGENDRE) {
168
      estimator_ = TallyEstimator::ANALOG;
471✔
169
    } else if (filt_type == FilterType::SPHERICAL_HARMONICS) {
2,865✔
170
      auto sf = dynamic_cast<SphericalHarmonicsFilter*>(f);
5!
171
      if (sf->cosine() == SphericalHarmonicsCosine::scatter) {
5✔
172
        estimator_ = TallyEstimator::ANALOG;
1✔
173
      }
174
    } else if (filt_type == FilterType::SPATIAL_LEGENDRE ||
2,860✔
175
               filt_type == FilterType::ZERNIKE ||
2,855!
176
               filt_type == FilterType::ZERNIKE_RADIAL) {
177
      estimator_ = TallyEstimator::COLLISION;
5✔
178
    }
179
  }
180

181
  // =======================================================================
182
  // READ DATA FOR NUCLIDES
183

184
  this->set_nuclides(node);
1,698✔
185

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

189
  this->set_scores(node);
1,698✔
190

191
  if (!check_for_node(node, "scores")) {
1,698!
192
    fatal_error(fmt::format("No scores specified on tally {}.", id_));
×
193
  }
194

195
  // Set IFP if needed
196
  if (!settings::ifp_on) {
1,698✔
197
    // Determine if this tally has an IFP score
198
    bool has_ifp_score = false;
1,688✔
199
    for (int score : scores_) {
4,091✔
200
      if (score == SCORE_IFP_TIME_NUM || score == SCORE_IFP_BETA_NUM ||
2,411!
201
          score == SCORE_IFP_DENOM) {
202
        has_ifp_score = true;
8✔
203
        break;
8✔
204
      }
205
    }
206

207
    // Check for errors
208
    if (has_ifp_score) {
1,688✔
209
      if (settings::run_mode == RunMode::EIGENVALUE) {
8✔
210
        if (settings::ifp_n_generation < 0) {
7✔
211
          settings::ifp_n_generation = DEFAULT_IFP_N_GENERATION;
1✔
212
          warning(fmt::format(
1✔
213
            "{} generations will be used for IFP (default value). It can be "
214
            "changed using the 'ifp_n_generation' settings.",
215
            settings::ifp_n_generation));
216
        }
217
        if (settings::ifp_n_generation > settings::n_inactive) {
7✔
218
          fatal_error("'ifp_n_generation' must be lower than or equal to the "
1✔
219
                      "number of inactive cycles.");
220
        }
221
        settings::ifp_on = true;
6✔
222
      } else if (settings::run_mode == RunMode::FIXED_SOURCE) {
1!
223
        fatal_error(
1✔
224
          "Iterated Fission Probability can only be used in an eigenvalue "
225
          "calculation.");
226
      }
227
    }
228
  }
229

230
  // Set IFP parameters if needed
231
  if (settings::ifp_on) {
1,696✔
232
    for (int score : scores_) {
34✔
233
      switch (score) {
18!
234
      case SCORE_IFP_TIME_NUM:
6✔
235
        if (settings::ifp_parameter == IFPParameter::None) {
6!
236
          settings::ifp_parameter = IFPParameter::GenerationTime;
6✔
237
        } else if (settings::ifp_parameter == IFPParameter::BetaEffective) {
×
238
          settings::ifp_parameter = IFPParameter::Both;
×
239
        }
240
        break;
6✔
241
      case SCORE_IFP_BETA_NUM:
12✔
242
      case SCORE_IFP_DENOM:
243
        if (settings::ifp_parameter == IFPParameter::None) {
12!
244
          settings::ifp_parameter = IFPParameter::BetaEffective;
×
245
        } else if (settings::ifp_parameter == IFPParameter::GenerationTime) {
12✔
246
          settings::ifp_parameter = IFPParameter::Both;
6✔
247
        }
248
        break;
12✔
249
      }
250
    }
251
  }
252

253
  // Check if tally is compatible with particle type
254
  if (!settings::photon_transport) {
1,696✔
255
    for (int score : scores_) {
4,039✔
256
      switch (score) {
2,374!
257
      case SCORE_PULSE_HEIGHT:
×
258
        fatal_error("For pulse-height tallies, photon transport needs to be "
×
259
                    "activated.");
260
        break;
261
      }
262
    }
263
  }
264
  if (settings::photon_transport) {
1,696✔
265
    if (particle_filter_index == C_NONE) {
31✔
266
      for (int score : scores_) {
18✔
267
        switch (score) {
9!
268
        case SCORE_INVERSE_VELOCITY:
×
269
          fatal_error("Particle filter must be used with photon "
×
270
                      "transport on and inverse velocity score");
271
          break;
272
        case SCORE_FLUX:
4✔
273
        case SCORE_TOTAL:
274
        case SCORE_SCATTER:
275
        case SCORE_NU_SCATTER:
276
        case SCORE_ABSORPTION:
277
        case SCORE_FISSION:
278
        case SCORE_NU_FISSION:
279
        case SCORE_CURRENT:
280
        case SCORE_EVENTS:
281
        case SCORE_DELAYED_NU_FISSION:
282
        case SCORE_PROMPT_NU_FISSION:
283
        case SCORE_DECAY_RATE:
284
          warning("You are tallying the '" + reaction_name(score) +
4✔
285
                  "' score and haven't used a particle filter. This score will "
286
                  "include contributions from all particles.");
287
          break;
4✔
288
        }
289
      }
290
    }
291
  } else {
292
    if (particle_filter_index >= 0) {
1,665✔
293
      const auto& f = model::tally_filters[particle_filter_index].get();
10✔
294
      auto pf = dynamic_cast<ParticleFilter*>(f);
10!
295
      for (auto p : pf->particles()) {
30✔
296
        if (p != ParticleType::neutron) {
20✔
297
          warning(fmt::format(
10✔
298
            "Particle filter other than NEUTRON used with "
299
            "photon transport turned off. All tallies for particle type {}"
300
            " will have no scores",
301
            static_cast<int>(p)));
20✔
302
        }
303
      }
304
    }
305
  }
306

307
  // Check for a tally derivative.
308
  if (check_for_node(node, "derivative")) {
1,696✔
309
    int deriv_id = std::stoi(get_node_value(node, "derivative"));
20✔
310

311
    // Find the derivative with the given id, and store it's index.
312
    auto it = model::tally_deriv_map.find(deriv_id);
20✔
313
    if (it == model::tally_deriv_map.end()) {
20!
314
      fatal_error(fmt::format(
×
315
        "Could not find derivative {} specified on tally {}", deriv_id, id_));
×
316
    }
317

318
    deriv_ = it->second;
20✔
319

320
    // Only analog or collision estimators are supported for differential
321
    // tallies.
322
    if (estimator_ == TallyEstimator::TRACKLENGTH) {
20✔
323
      estimator_ = TallyEstimator::COLLISION;
15✔
324
    }
325

326
    const auto& deriv = model::tally_derivs[deriv_];
20✔
327
    if (deriv.variable == DerivativeVariable::NUCLIDE_DENSITY ||
20✔
328
        deriv.variable == DerivativeVariable::TEMPERATURE) {
12✔
329
      for (int i_nuc : nuclides_) {
27✔
330
        if (has_energyout && i_nuc == -1) {
15!
331
          fatal_error(fmt::format(
×
332
            "Error on tally {}: Cannot use a "
333
            "'nuclide_density' or 'temperature' derivative on a tally with "
334
            "an "
335
            "outgoing energy filter and 'total' nuclide rate. Instead, tally "
336
            "each nuclide in the material individually.",
337
            id_));
×
338
          // Note that diff tallies with these characteristics would work
339
          // correctly if no tally events occur in the perturbed material
340
          // (e.g. pertrubing moderator but only tallying fuel), but this
341
          // case would be hard to check for by only reading inputs.
342
        }
343
      }
344
    }
345
  }
346

347
  // If settings.xml trigger is turned on, create tally triggers
348
  if (settings::trigger_on) {
1,696✔
349
    this->init_triggers(node);
13✔
350
  }
351

352
  // =======================================================================
353
  // SET TALLY ESTIMATOR
354

355
  // Check if user specified estimator
356
  if (check_for_node(node, "estimator")) {
1,696✔
357
    std::string est = get_node_value(node, "estimator");
1,326✔
358
    if (est == "analog") {
1,326✔
359
      estimator_ = TallyEstimator::ANALOG;
572✔
360
    } else if (est == "tracklength" || est == "track-length" ||
810!
361
               est == "pathlength" || est == "path-length") {
810!
362
      // If the estimator was set to an analog estimator, this means the
363
      // tally needs post-collision information
364
      if (estimator_ == TallyEstimator::ANALOG ||
726!
365
          estimator_ == TallyEstimator::COLLISION) {
726!
366
        throw std::runtime_error {fmt::format("Cannot use track-length "
×
367
                                              "estimator for tally {}",
368
          id_)};
×
369
      }
370

371
      // Set estimator to track-length estimator
372
      estimator_ = TallyEstimator::TRACKLENGTH;
726✔
373

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

383
      // Set estimator to collision estimator
384
      estimator_ = TallyEstimator::COLLISION;
28✔
385

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

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

407
Tally::~Tally()
1,801✔
408
{
409
  model::tally_map.erase(id_);
1,801✔
410
}
1,801✔
411

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

418
void Tally::set_id(int32_t id)
1,900✔
419
{
420
  assert(id >= 0 || id == C_NONE);
1,900!
421

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

428
  // Make sure no other tally has the same ID
429
  if (model::tally_map.find(id) != model::tally_map.end()) {
1,900!
430
    throw std::runtime_error {
×
431
      fmt::format("Two tallies have the same ID: {}", id)};
×
432
  }
433

434
  // If no ID specified, auto-assign next ID in sequence
435
  if (id == C_NONE) {
1,900✔
436
    id = 0;
105✔
437
    for (const auto& t : model::tallies) {
321✔
438
      id = std::max(id, t->id_);
216✔
439
    }
440
    ++id;
105✔
441
  }
442

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

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

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

462
    filter_indices[f->type()] = i;
56✔
463
  }
464
  return filter_indices;
20✔
465
}
×
466

467
bool Tally::has_filter(FilterType filter_type) const
60✔
468
{
469
  for (auto idx : this->filters()) {
143✔
470
    if (model::tally_filters[idx]->type() == filter_type)
137✔
471
      return true;
54✔
472
  }
473
  return false;
6✔
474
}
475

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

482
  // Copy in the given filter indices.
483
  auto n = filters.size();
1,909✔
484
  filters_.reserve(n);
1,909✔
485

486
  for (auto* filter : filters) {
5,391✔
487
    add_filter(filter);
3,482✔
488
  }
489
}
1,909✔
490

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

498
  // Keep track of indices for special filters
499
  if (filter->type() == FilterType::ENERGY_OUT) {
3,501✔
500
    energyout_filter_ = filters_.size();
341✔
501
  } else if (filter->type() == FilterType::DELAYED_GROUP) {
3,160✔
502
    delayedgroup_filter_ = filters_.size();
53✔
503
  }
504
  filters_.push_back(filter_idx);
3,501✔
505
}
506

507
void Tally::set_strides()
1,833✔
508
{
509
  // Set the strides.  Filters are traversed in reverse so that the last
510
  // filter has the shortest stride in memory and the first filter has the
511
  // longest stride.
512
  auto n = filters_.size();
1,833✔
513
  strides_.resize(n, 0);
1,833✔
514
  int stride = 1;
1,833✔
515
  for (int i = n - 1; i >= 0; --i) {
5,364✔
516
    strides_[i] = stride;
3,531✔
517
    stride *= model::tally_filters[filters_[i]]->n_bins();
3,531✔
518
  }
519
  n_filter_bins_ = stride;
1,833✔
520
}
1,833✔
521

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

527
  auto scores = get_node_array<std::string>(node, "scores");
1,698✔
528
  set_scores(scores);
1,698✔
529
}
1,698✔
530

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

537
  // Check for the presence of certain restrictive filters.
538
  bool energyout_present = energyout_filter_ != C_NONE;
1,803✔
539
  bool legendre_present = false;
1,803✔
540
  bool cell_present = false;
1,803✔
541
  bool cellfrom_present = false;
1,803✔
542
  bool point_present = false;
1,803✔
543
  bool surface_present = false;
1,803✔
544
  bool meshsurface_present = false;
1,803✔
545
  bool non_cell_energy_present = false;
1,803✔
546
  for (auto i_filt : filters_) {
5,234✔
547
    const auto* filt {model::tally_filters[i_filt].get()};
3,431✔
548
    // Checking for only cell and energy filters for pulse-height tally
549
    if (!(filt->type() == FilterType::CELL ||
6,787✔
550
          filt->type() == FilterType::ENERGY)) {
3,356✔
551
      non_cell_energy_present = true;
2,218✔
552
    }
553
    if (filt->type() == FilterType::LEGENDRE) {
3,431✔
554
      legendre_present = true;
147✔
555
    } else if (filt->type() == FilterType::CELLFROM) {
3,284✔
556
      cellfrom_present = true;
19✔
557
    } else if (filt->type() == FilterType::CELL) {
3,265✔
558
      cell_present = true;
75✔
559
    } else if (filt->type() == FilterType::POINT) {
3,190!
NEW
560
      point_present = true;
×
NEW
561
      type_ = TallyType::NEXT_EVENT;
×
NEW
562
      estimator_ = TallyEstimator::POINT;
×
563
    } else if (filt->type() == FilterType::SURFACE) {
3,190✔
564
      surface_present = true;
15✔
565
    } else if (filt->type() == FilterType::MESH_SURFACE) {
3,175✔
566
      meshsurface_present = true;
27✔
567
    }
568
  }
569

570
  if (point_present) {
1,803!
NEW
571
    if (simulation::nonvacuum_boundary_present)
×
NEW
572
      fatal_error(
×
573
        "Cannot use point detectors with non-vacuum boundary conditions.");
NEW
574
    if (legendre_present)
×
NEW
575
      fatal_error("Cannot use LegendreFilter with PointFilter.");
×
NEW
576
    if (energyout_present)
×
NEW
577
      fatal_error("Cannot use EnergyoutFilter with PointFilter.");
×
NEW
578
    if (surface_present || meshsurface_present)
×
NEW
579
      fatal_error(
×
580
        "Cannot use surface or mesh-surface filters with PointFilter.");
581
  }
582

583
  // Iterate over the given scores.
584
  for (auto score_str : scores) {
4,384✔
585
    // Make sure a delayed group filter wasn't used with an incompatible
586
    // score.
587
    if (delayedgroup_filter_ != C_NONE) {
2,581✔
588
      if (score_str != "delayed-nu-fission" && score_str != "decay-rate" &&
57!
589
          score_str != "ifp-beta-numerator")
3✔
590
        fatal_error("Cannot tally " + score_str + "with a delayedgroup filter");
×
591
    }
592

593
    // Determine integer code for score
594
    int score = reaction_type(score_str);
2,581✔
595

596
    switch (score) {
2,581✔
597
    case SCORE_FLUX:
757✔
598
      if (!nuclides_.empty())
757!
599
        if (!(nuclides_.size() == 1 && nuclides_[0] == -1))
757!
600
          fatal_error("Cannot tally flux for an individual nuclide.");
×
601
      if (energyout_present)
757!
602
        fatal_error("Cannot tally flux with an outgoing energy filter.");
×
603
      break;
757✔
604

605
    case SCORE_TOTAL:
503✔
606
    case SCORE_ABSORPTION:
607
    case SCORE_FISSION:
608
      if (energyout_present)
503!
609
        fatal_error("Cannot tally " + score_str +
×
610
                    " reaction rate with an "
611
                    "outgoing energy filter");
612
      break;
503✔
613

614
    case SCORE_SCATTER:
302✔
615
      if (legendre_present)
302✔
616
        estimator_ = TallyEstimator::ANALOG;
104✔
617
    case SCORE_NU_FISSION:
618
    case SCORE_DELAYED_NU_FISSION:
619
    case SCORE_PROMPT_NU_FISSION:
620
      if (energyout_present)
653✔
621
        estimator_ = TallyEstimator::ANALOG;
270✔
622
      break;
653✔
623

624
    case SCORE_NU_SCATTER:
151✔
625
      if (settings::run_CE) {
151✔
626
        if (point_present)
143!
NEW
627
          fatal_error("Cannot use nu-scatter score with PointFilter in "
×
628
                      "continuous energy mode.");
629
        estimator_ = TallyEstimator::ANALOG;
143✔
630
      } else {
631
        if (energyout_present || legendre_present)
8!
632
          estimator_ = TallyEstimator::ANALOG;
4✔
633
      }
634
      break;
151✔
635

636
    case SCORE_CURRENT:
44✔
637
      if (point_present)
44!
NEW
638
        fatal_error("Cannot use current score with PointFilter.");
×
639
      // Check which type of current is desired: mesh or surface currents.
640
      if (surface_present || cell_present || cellfrom_present) {
44!
641
        if (meshsurface_present)
17!
642
          fatal_error("Cannot tally mesh surface currents in the same tally as "
×
643
                      "normal surface currents");
644
        type_ = TallyType::SURFACE;
17✔
645
        estimator_ = TallyEstimator::ANALOG;
17✔
646
      } else if (meshsurface_present) {
27!
647
        type_ = TallyType::MESH_SURFACE;
27✔
648
      } else {
649
        fatal_error("Cannot tally currents without surface type filters");
×
650
      }
651
      break;
44✔
652

653
    case HEATING:
25✔
654
      if (point_present)
25!
NEW
655
        fatal_error("Cannot use heating score with PointFilter.");
×
656
      if (settings::photon_transport)
25✔
657
        estimator_ = TallyEstimator::COLLISION;
14✔
658
      break;
25✔
659

660
    case SCORE_PULSE_HEIGHT:
1✔
661
      if (point_present)
1!
NEW
662
        fatal_error("Cannot use pulse-height score with PointFilter.");
×
663
      if (non_cell_energy_present) {
1!
664
        fatal_error("Pulse-height tallies are not compatible with filters "
×
665
                    "other than CellFilter and EnergyFilter");
666
      }
667
      type_ = TallyType::PULSE_HEIGHT;
1✔
668

669
      // Collecting indices of all cells covered by the filters in the pulse
670
      // height tally in global variable pulse_height_cells
671
      for (const auto& i_filt : filters_) {
3✔
672
        auto cell_filter =
673
          dynamic_cast<CellFilter*>(model::tally_filters[i_filt].get());
2!
674
        if (cell_filter) {
2✔
675
          const auto& cells = cell_filter->cells();
1✔
676
          for (int i = 0; i < cell_filter->n_bins(); i++) {
2✔
677
            int cell_index = cells[i];
1✔
678
            if (!contains(model::pulse_height_cells, cell_index)) {
1!
679
              model::pulse_height_cells.push_back(cell_index);
1✔
680
            }
681
          }
682
        }
683
      }
684
      break;
1✔
685

686
    case SCORE_IFP_TIME_NUM:
24✔
687
    case SCORE_IFP_BETA_NUM:
688
    case SCORE_IFP_DENOM:
689
      if (point_present)
24!
NEW
690
        fatal_error("Cannot use ifp scores with PointFilter.");
×
691
      estimator_ = TallyEstimator::COLLISION;
24✔
692
      break;
24✔
693
    }
694

695
    scores_.push_back(score);
2,581✔
696
  }
2,581✔
697

698
  // Make sure that no duplicate scores exist.
699
  for (auto it1 = scores_.begin(); it1 != scores_.end(); ++it1) {
4,384✔
700
    for (auto it2 = it1 + 1; it2 != scores_.end(); ++it2) {
5,977✔
701
      if (*it1 == *it2)
3,396!
702
        fatal_error(
×
703
          fmt::format("Duplicate score of type \"{}\" found in tally {}",
×
704
            reaction_name(*it1), id_));
×
705
    }
706
  }
707

708
  // Make sure all scores are compatible with multigroup mode.
709
  if (!settings::run_CE) {
1,803✔
710
    for (auto sc : scores_)
408✔
711
      if (sc > 0)
297!
712
        fatal_error("Cannot tally " + reaction_name(sc) +
×
713
                    " reaction rate "
714
                    "in multi-group mode");
715
  }
716

717
  // Make sure current scores are not mixed in with volumetric scores.
718
  if (type_ == TallyType::SURFACE || type_ == TallyType::MESH_SURFACE) {
1,803✔
719
    if (scores_.size() != 1)
44!
720
      fatal_error("Cannot tally other scores in the same tally as surface "
×
721
                  "currents.");
722
  }
723
  if ((surface_present || meshsurface_present) && scores_[0] != SCORE_CURRENT)
1,803!
724
    fatal_error("Cannot tally score other than 'current' when using a surface "
×
725
                "or mesh-surface filter.");
726
}
1,803✔
727

728
void Tally::set_nuclides(pugi::xml_node node)
1,698✔
729
{
730
  nuclides_.clear();
1,698✔
731

732
  // By default, we tally just the total material rates.
733
  if (!check_for_node(node, "nuclides")) {
1,698✔
734
    nuclides_.push_back(-1);
475✔
735
    return;
475✔
736
  }
737

738
  // The user provided specifics nuclides.  Parse it as an array with either
739
  // "total" or a nuclide name like "U235" in each position.
740
  auto words = get_node_array<std::string>(node, "nuclides");
1,223✔
741
  this->set_nuclides(words);
1,223✔
742
}
1,223✔
743

744
void Tally::set_nuclides(const vector<std::string>& nuclides)
1,223✔
745
{
746
  nuclides_.clear();
1,223✔
747

748
  for (const auto& nuc : nuclides) {
3,138✔
749
    if (nuc == "total") {
1,915✔
750
      nuclides_.push_back(-1);
998✔
751
    } else {
752
      auto search = data::nuclide_map.find(nuc);
917✔
753
      if (search == data::nuclide_map.end()) {
917✔
754
        int err = openmc_load_nuclide(nuc.c_str(), nullptr, 0);
13✔
755
        if (err < 0)
13!
756
          throw std::runtime_error {openmc_err_msg};
×
757
      }
758
      nuclides_.push_back(data::nuclide_map.at(nuc));
917✔
759
    }
760
  }
761
}
1,223✔
762

763
void Tally::init_triggers(pugi::xml_node node)
13✔
764
{
765
  for (auto trigger_node : node.children("trigger")) {
19✔
766
    // Read the trigger type.
767
    TriggerMetric metric;
768
    if (check_for_node(trigger_node, "type")) {
6!
769
      auto type_str = get_node_value(trigger_node, "type");
6✔
770
      if (type_str == "std_dev") {
6!
771
        metric = TriggerMetric::standard_deviation;
×
772
      } else if (type_str == "variance") {
6!
773
        metric = TriggerMetric::variance;
×
774
      } else if (type_str == "rel_err") {
6!
775
        metric = TriggerMetric::relative_error;
6✔
776
      } else {
777
        fatal_error(fmt::format(
×
778
          "Unknown trigger type \"{}\" in tally {}", type_str, id_));
×
779
      }
780
    } else {
6✔
781
      fatal_error(fmt::format(
×
782
        "Must specify trigger type for tally {} in tally XML file", id_));
×
783
    }
784

785
    // Read the trigger threshold.
786
    double threshold;
787
    if (check_for_node(trigger_node, "threshold")) {
6!
788
      threshold = std::stod(get_node_value(trigger_node, "threshold"));
6✔
789
      if (threshold <= 0) {
6!
790
        fatal_error("Tally trigger threshold must be positive");
×
791
      }
792
    } else {
793
      fatal_error(fmt::format(
×
794
        "Must specify trigger threshold for tally {} in tally XML file", id_));
×
795
    }
796

797
    // Read whether to allow zero-tally bins to be ignored.
798
    bool ignore_zeros = false;
6✔
799
    if (check_for_node(trigger_node, "ignore_zeros")) {
6✔
800
      ignore_zeros = get_node_value_bool(trigger_node, "ignore_zeros");
1✔
801
    }
802

803
    // Read the trigger scores.
804
    vector<std::string> trigger_scores;
6✔
805
    if (check_for_node(trigger_node, "scores")) {
6!
806
      trigger_scores = get_node_array<std::string>(trigger_node, "scores");
6✔
807
    } else {
808
      trigger_scores.push_back("all");
×
809
    }
810

811
    // Parse the trigger scores and populate the triggers_ vector.
812
    for (auto score_str : trigger_scores) {
12✔
813
      if (score_str == "all") {
6✔
814
        triggers_.reserve(triggers_.size() + this->scores_.size());
1✔
815
        for (auto i_score = 0; i_score < this->scores_.size(); ++i_score) {
5✔
816
          triggers_.push_back({metric, threshold, ignore_zeros, i_score});
4✔
817
        }
818
      } else {
819
        int i_score = 0;
5✔
820
        for (; i_score < this->scores_.size(); ++i_score) {
7!
821
          if (this->scores_[i_score] == reaction_type(score_str))
7✔
822
            break;
5✔
823
        }
824
        if (i_score == this->scores_.size()) {
5!
825
          fatal_error(
×
826
            fmt::format("Could not find the score \"{}\" in tally "
×
827
                        "{} but it was listed in a trigger on that tally",
828
              score_str, id_));
×
829
        }
830
        triggers_.push_back({metric, threshold, ignore_zeros, i_score});
5✔
831
      }
832
    }
6✔
833
  }
6✔
834
}
13✔
835

836
void Tally::init_results()
1,833✔
837
{
838
  int n_scores = scores_.size() * nuclides_.size();
1,833✔
839
  if (higher_moments_) {
1,833✔
840
    results_ = xt::empty<double>({n_filter_bins_, n_scores, 5});
1✔
841
  } else {
842
    results_ = xt::empty<double>({n_filter_bins_, n_scores, 3});
1,832✔
843
  }
844
}
1,833✔
845

846
void Tally::reset()
3,991✔
847
{
848
  n_realizations_ = 0;
3,991✔
849
  if (results_.size() != 0) {
3,991✔
850
    xt::view(results_, xt::all()) = 0.0;
3,909✔
851
  }
852
}
3,991✔
853

854
void Tally::accumulate()
23,818✔
855
{
856
  // Increment number of realizations
857
  n_realizations_ += settings::reduce_tallies ? 1 : mpi::n_procs;
23,818✔
858

859
  if (mpi::master || !settings::reduce_tallies) {
23,818!
860
    // Calculate total source strength for normalization
861
    double total_source = 0.0;
23,818✔
862
    if (settings::run_mode == RunMode::FIXED_SOURCE) {
23,818✔
863
      total_source = model::external_sources_probability.integral();
12,524✔
864
    } else {
865
      total_source = 1.0;
11,294✔
866
    }
867

868
    // Determine number of particles contributing to tally
869
    double contributing_particles = settings::reduce_tallies
23,818✔
870
                                      ? settings::n_particles
871
                                      : simulation::work_per_rank;
872

873
    // Account for number of source particles in normalization
874
    double norm =
23,818✔
875
      total_source / (contributing_particles * settings::gen_per_batch);
23,818✔
876

877
    if (settings::solver_type == SolverType::RANDOM_RAY) {
23,818✔
878
      norm = 1.0;
1,070✔
879
    }
880

881
    // Accumulate each result
882
    if (higher_moments_) {
23,818✔
883
#pragma omp parallel for
884
      // filter bins (specific cell, energy bins)
885
      for (int i = 0; i < results_.shape()[0]; ++i) {
250✔
886
        // score bins (flux, total reaction rate, fission reaction rate, etc.)
887
        for (int j = 0; j < results_.shape()[1]; ++j) {
1,200✔
888
          double val = results_(i, j, TallyResult::VALUE) * norm;
960✔
889
          double val2 = val * val;
960✔
890
          results_(i, j, TallyResult::VALUE) = 0.0;
960✔
891
          results_(i, j, TallyResult::SUM) += val;
960✔
892
          results_(i, j, TallyResult::SUM_SQ) += val2;
960✔
893
          results_(i, j, TallyResult::SUM_THIRD) += val2 * val;
960✔
894
          results_(i, j, TallyResult::SUM_FOURTH) += val2 * val2;
960✔
895
        }
896
      }
897
    } else {
898
#pragma omp parallel for
899
      // filter bins (specific cell, energy bins)
900
      for (int i = 0; i < results_.shape()[0]; ++i) {
9,176,176✔
901
        // score bins (flux, total reaction rate, fission reaction rate, etc.)
902
        for (int j = 0; j < results_.shape()[1]; ++j) {
22,208,596✔
903
          double val = results_(i, j, TallyResult::VALUE) * norm;
13,056,228✔
904
          results_(i, j, TallyResult::VALUE) = 0.0;
13,056,228✔
905
          results_(i, j, TallyResult::SUM) += val;
13,056,228✔
906
          results_(i, j, TallyResult::SUM_SQ) += val * val;
13,056,228✔
907
        }
908
      }
909
    }
910
  }
911
}
23,818✔
912

913
int Tally::score_index(const std::string& score) const
20✔
914
{
915
  for (int i = 0; i < scores_.size(); i++) {
20!
916
    if (this->score_name(i) == score)
20!
917
      return i;
20✔
918
  }
919
  return -1;
×
920
}
921

922
xt::xarray<double> Tally::get_reshaped_data() const
×
923
{
924
  std::vector<uint64_t> shape;
×
925
  for (auto f : filters()) {
×
926
    shape.push_back(model::tally_filters[f]->n_bins());
×
927
  }
928

929
  // add number of scores and nuclides to tally
930
  shape.push_back(results_.shape()[1]);
×
931
  shape.push_back(results_.shape()[2]);
×
932

933
  xt::xarray<double> reshaped_results = results_;
×
934
  reshaped_results.reshape(shape);
×
935
  return reshaped_results;
×
936
}
×
937

938
std::string Tally::score_name(int score_idx) const
20✔
939
{
940
  if (score_idx < 0 || score_idx >= scores_.size()) {
20!
941
    fatal_error("Index in scores array is out of bounds.");
×
942
  }
943
  return reaction_name(scores_[score_idx]);
20✔
944
}
945

946
std::vector<std::string> Tally::scores() const
×
947
{
948
  std::vector<std::string> score_names;
×
949
  for (int score : scores_)
×
950
    score_names.push_back(reaction_name(score));
×
951
  return score_names;
×
952
}
×
953

UNCOV
954
std::string Tally::nuclide_name(int nuclide_idx) const
×
955
{
UNCOV
956
  if (nuclide_idx < 0 || nuclide_idx >= nuclides_.size()) {
×
957
    fatal_error("Index in nuclides array is out of bounds");
×
958
  }
959

UNCOV
960
  int nuclide = nuclides_.at(nuclide_idx);
×
UNCOV
961
  if (nuclide == -1) {
×
UNCOV
962
    return "total";
×
963
  }
964
  return data::nuclides.at(nuclide)->name_;
×
965
}
966

967
//==============================================================================
968
// Non-member functions
969
//==============================================================================
970

971
void read_tallies_xml()
103✔
972
{
973
  // Check if tallies.xml exists. If not, just return since it is optional
974
  std::string filename = settings::path_input + "tallies.xml";
103✔
975
  if (!file_exists(filename))
103✔
976
    return;
68✔
977

978
  write_message("Reading tallies XML file...", 5);
35✔
979

980
  // Parse tallies.xml file
981
  pugi::xml_document doc;
35✔
982
  doc.load_file(filename.c_str());
35✔
983
  pugi::xml_node root = doc.document_element();
35✔
984

985
  read_tallies_xml(root);
35✔
986
}
103✔
987

988
void read_tallies_xml(pugi::xml_node root)
336✔
989
{
990
  // Check for <assume_separate> setting
991
  if (check_for_node(root, "assume_separate")) {
336✔
992
    settings::assume_separate = get_node_value_bool(root, "assume_separate");
1✔
993
  }
994

995
  // Check for user meshes and allocate
996
  read_meshes(root);
336✔
997

998
  // We only need the mesh info for plotting
999
  if (settings::run_mode == RunMode::PLOTTING)
336✔
1000
    return;
3✔
1001

1002
  // Read data for tally derivatives
1003
  read_tally_derivatives(root);
333✔
1004

1005
  // ==========================================================================
1006
  // READ FILTER DATA
1007

1008
  // Check for user filters and allocate
1009
  for (auto node_filt : root.children("filter")) {
996✔
1010
    auto f = Filter::create(node_filt);
663✔
1011
  }
1012

1013
  // ==========================================================================
1014
  // READ TALLY DATA
1015

1016
  // Check for user tallies
1017
  int n = 0;
333✔
1018
  for (auto node : root.children("tally"))
2,031✔
1019
    ++n;
1,698✔
1020
  if (n == 0 && mpi::master) {
333!
1021
    warning("No tallies present in tallies.xml file.");
×
1022
  }
1023

1024
  for (auto node_tal : root.children("tally")) {
2,029✔
1025
    model::tallies.push_back(make_unique<Tally>(node_tal));
1,698✔
1026
  }
1027
}
1028

1029
#ifdef OPENMC_MPI
1030
void reduce_tally_results()
1031
{
1032
  // Don't reduce tally is no_reduce option is on
1033
  if (settings::reduce_tallies) {
1034
    for (int i_tally : model::active_tallies) {
1035
      // Skip any tallies that are not active
1036
      auto& tally {model::tallies[i_tally]};
1037

1038
      // Get view of accumulated tally values
1039
      auto values_view = xt::view(tally->results_, xt::all(), xt::all(),
1040
        static_cast<int>(TallyResult::VALUE));
1041

1042
      // Make copy of tally values in contiguous array
1043
      xt::xtensor<double, 2> values = values_view;
1044
      xt::xtensor<double, 2> values_reduced = xt::empty_like(values);
1045

1046
      // Reduce contiguous set of tally results
1047
      MPI_Reduce(values.data(), values_reduced.data(), values.size(),
1048
        MPI_DOUBLE, MPI_SUM, 0, mpi::intracomm);
1049

1050
      // Transfer values on master and reset on other ranks
1051
      if (mpi::master) {
1052
        values_view = values_reduced;
1053
      } else {
1054
        values_view = 0.0;
1055
      }
1056
    }
1057
  }
1058

1059
  // Note that global tallies are *always* reduced even when no_reduce option
1060
  // is on.
1061

1062
  // Get view of global tally values
1063
  auto& gt = simulation::global_tallies;
1064
  auto gt_values_view =
1065
    xt::view(gt, xt::all(), static_cast<int>(TallyResult::VALUE));
1066

1067
  // Make copy of values in contiguous array
1068
  xt::xtensor<double, 1> gt_values = gt_values_view;
1069
  xt::xtensor<double, 1> gt_values_reduced = xt::empty_like(gt_values);
1070

1071
  // Reduce contiguous data
1072
  MPI_Reduce(gt_values.data(), gt_values_reduced.data(), N_GLOBAL_TALLIES,
1073
    MPI_DOUBLE, MPI_SUM, 0, mpi::intracomm);
1074

1075
  // Transfer values on master and reset on other ranks
1076
  if (mpi::master) {
1077
    gt_values_view = gt_values_reduced;
1078
  } else {
1079
    gt_values_view = 0.0;
1080
  }
1081

1082
  // We also need to determine the total starting weight of particles from the
1083
  // last realization
1084
  double weight_reduced;
1085
  MPI_Reduce(&simulation::total_weight, &weight_reduced, 1, MPI_DOUBLE, MPI_SUM,
1086
    0, mpi::intracomm);
1087
  if (mpi::master)
1088
    simulation::total_weight = weight_reduced;
1089
}
1090
#endif
1091

1092
void accumulate_tallies()
10,722✔
1093
{
1094
#ifdef OPENMC_MPI
1095
  // Combine tally results onto master process
1096
  if (mpi::n_procs > 1 && settings::solver_type == SolverType::MONTE_CARLO) {
1097
    reduce_tally_results();
1098
  }
1099
#endif
1100

1101
  // Increase number of realizations (only used for global tallies)
1102
  simulation::n_realizations += 1;
10,722✔
1103

1104
  // Accumulate on master only unless run is not reduced then do it on all
1105
  if (mpi::master || !settings::reduce_tallies) {
10,722!
1106
    auto& gt = simulation::global_tallies;
10,722✔
1107

1108
    if (settings::run_mode == RunMode::EIGENVALUE) {
10,722✔
1109
      if (simulation::current_batch > settings::n_inactive) {
6,484✔
1110
        // Accumulate products of different estimators of k
1111
        double k_col = gt(GlobalTally::K_COLLISION, TallyResult::VALUE) /
5,005✔
1112
                       simulation::total_weight;
5,005✔
1113
        double k_abs = gt(GlobalTally::K_ABSORPTION, TallyResult::VALUE) /
5,005✔
1114
                       simulation::total_weight;
5,005✔
1115
        double k_tra = gt(GlobalTally::K_TRACKLENGTH, TallyResult::VALUE) /
5,005✔
1116
                       simulation::total_weight;
5,005✔
1117
        simulation::k_col_abs += k_col * k_abs;
5,005✔
1118
        simulation::k_col_tra += k_col * k_tra;
5,005✔
1119
        simulation::k_abs_tra += k_abs * k_tra;
5,005✔
1120
      }
1121
    }
1122

1123
    // Accumulate results for global tallies
1124
    for (int i = 0; i < N_GLOBAL_TALLIES; ++i) {
53,610✔
1125
      double val = gt(i, TallyResult::VALUE) / simulation::total_weight;
42,888✔
1126
      gt(i, TallyResult::VALUE) = 0.0;
42,888✔
1127
      gt(i, TallyResult::SUM) += val;
42,888✔
1128
      gt(i, TallyResult::SUM_SQ) += val * val;
42,888✔
1129
    }
1130
  }
1131

1132
  // Accumulate results for each tally
1133
  for (int i_tally : model::active_tallies) {
34,540✔
1134
    auto& tally {model::tallies[i_tally]};
23,818✔
1135
    tally->accumulate();
23,818✔
1136
  }
1137
}
10,722✔
1138

1139
double distance_to_time_boundary(double time, double speed)
707,913✔
1140
{
1141
  if (model::time_grid.empty()) {
707,913!
1142
    return INFTY;
×
1143
  } else if (time >= model::time_grid.back()) {
707,913✔
1144
    return INFTY;
2,420✔
1145
  } else {
1146
    double next_time =
1147
      *std::upper_bound(model::time_grid.begin(), model::time_grid.end(), time);
705,493✔
1148
    return (next_time - time) * speed;
705,493✔
1149
  }
1150
}
1151

1152
//! Add new points to the global time grid
1153
//
1154
//! \param grid Vector of new time points to add
1155
void add_to_time_grid(vector<double> grid)
20✔
1156
{
1157
  if (grid.empty())
20!
1158
    return;
×
1159

1160
  // Create new vector with enough space to hold old and new grid points
1161
  vector<double> merged;
20✔
1162
  merged.reserve(model::time_grid.size() + grid.size());
20✔
1163

1164
  // Merge and remove duplicates
1165
  std::set_union(model::time_grid.begin(), model::time_grid.end(), grid.begin(),
20✔
1166
    grid.end(), std::back_inserter(merged));
1167

1168
  // Swap in the new grid
1169
  model::time_grid.swap(merged);
20✔
1170
}
20✔
1171

1172
//! Add new points to global point_detectors
1173
//
1174
//! \param detector Position of new point detector to add
NEW
1175
void add_point_detector(Position& detector)
×
1176
{
NEW
1177
  model::active_point_detectors.insert(detector);
×
NEW
1178
}
×
1179

1180
void setup_active_tallies()
10,724✔
1181
{
1182
  model::active_tallies.clear();
10,724✔
1183
  model::active_analog_tallies.clear();
10,724✔
1184
  model::active_tracklength_tallies.clear();
10,724✔
1185
  model::active_timed_tracklength_tallies.clear();
10,724✔
1186
  model::active_collision_tallies.clear();
10,724✔
1187
  model::active_point_tallies.clear();
10,724✔
1188
  model::active_meshsurf_tallies.clear();
10,724✔
1189
  model::active_surface_tallies.clear();
10,724✔
1190
  model::active_pulse_height_tallies.clear();
10,724✔
1191
  model::time_grid.clear();
10,724✔
1192
  model::active_point_detectors.clear();
10,724✔
1193

1194
  std::set<int32_t> particle_filter_ids;
10,724✔
1195

1196
  for (auto i = 0; i < model::tallies.size(); ++i) {
43,391✔
1197
    const auto& tally {*model::tallies[i]};
32,667✔
1198

1199
    if (tally.active_) {
32,667✔
1200
      model::active_tallies.push_back(i);
23,818✔
1201
      bool mesh_present = (tally.get_filter<MeshFilter>() ||
43,892✔
1202
                           tally.get_filter<MeshMaterialFilter>());
20,074✔
1203
      auto time_filter = tally.get_filter<TimeFilter>();
23,818✔
1204
      switch (tally.type_) {
23,818!
1205

1206
      case TallyType::VOLUME:
23,305✔
1207
        switch (tally.estimator_) {
23,305!
1208
        case TallyEstimator::ANALOG:
12,883✔
1209
          model::active_analog_tallies.push_back(i);
12,883✔
1210
          break;
12,883✔
1211
        case TallyEstimator::TRACKLENGTH:
9,821✔
1212
          if (time_filter && mesh_present) {
9,821✔
1213
            model::active_timed_tracklength_tallies.push_back(i);
20✔
1214
            add_to_time_grid(time_filter->bins());
20✔
1215
          } else {
1216
            model::active_tracklength_tallies.push_back(i);
9,801✔
1217
          }
1218
          break;
9,821✔
1219
        case TallyEstimator::COLLISION:
601✔
1220
          model::active_collision_tallies.push_back(i);
601✔
1221
        }
1222
        break;
23,305✔
1223

1224
      case TallyType::MESH_SURFACE:
341✔
1225
        model::active_meshsurf_tallies.push_back(i);
341✔
1226
        break;
341✔
1227

1228
      case TallyType::SURFACE:
167✔
1229
        model::active_surface_tallies.push_back(i);
167✔
1230
        break;
167✔
1231

1232
      case TallyType::PULSE_HEIGHT:
5✔
1233
        model::active_pulse_height_tallies.push_back(i);
5✔
1234
        break;
5✔
1235

NEW
1236
      case TallyType::NEXT_EVENT:
×
NEW
1237
        switch (tally.estimator_) {
×
NEW
1238
        case TallyEstimator::POINT:
×
NEW
1239
          model::active_point_tallies.push_back(i);
×
NEW
1240
          auto pf = tally.get_filter<PointFilter>();
×
NEW
1241
          for (auto [det, r] : pf->detectors()) {
×
NEW
1242
            add_point_detector(det);
×
1243
          }
NEW
1244
          particle_filter_ids.insert(model::tally_map[pf->id()]);
×
NEW
1245
          break;
×
1246
        }
1247
      }
1248
    }
1249
  }
1250
  for (auto id : particle_filter_ids) {
10,724!
NEW
1251
    auto pf = dynamic_cast<PointFilter*>(model::tally_filters.at(id).get());
×
NEW
1252
    pf->reset_indices();
×
1253
  }
1254
}
10,724✔
1255

1256
void free_memory_tally()
635✔
1257
{
1258
  model::tally_derivs.clear();
635✔
1259
  model::tally_deriv_map.clear();
635✔
1260

1261
  model::tally_filters.clear();
635✔
1262
  model::filter_map.clear();
635✔
1263

1264
  model::tallies.clear();
635✔
1265

1266
  model::active_tallies.clear();
635✔
1267
  model::active_analog_tallies.clear();
635✔
1268
  model::active_tracklength_tallies.clear();
635✔
1269
  model::active_timed_tracklength_tallies.clear();
635✔
1270
  model::active_collision_tallies.clear();
635✔
1271
  model::active_point_tallies.clear();
635✔
1272
  model::active_meshsurf_tallies.clear();
635✔
1273
  model::active_surface_tallies.clear();
635✔
1274
  model::active_pulse_height_tallies.clear();
635✔
1275
  model::time_grid.clear();
635✔
1276
  model::active_point_detectors.clear();
635✔
1277

1278
  model::tally_map.clear();
635✔
1279
}
635✔
1280

1281
//==============================================================================
1282
// C-API functions
1283
//==============================================================================
1284

1285
extern "C" int openmc_extend_tallies(
97✔
1286
  int32_t n, int32_t* index_start, int32_t* index_end)
1287
{
1288
  if (index_start)
97!
1289
    *index_start = model::tallies.size();
97✔
1290
  if (index_end)
97!
1291
    *index_end = model::tallies.size() + n - 1;
×
1292
  for (int i = 0; i < n; ++i) {
194✔
1293
    model::tallies.push_back(make_unique<Tally>(-1));
97✔
1294
  }
1295
  return 0;
97✔
1296
}
1297

1298
extern "C" int openmc_get_tally_index(int32_t id, int32_t* index)
1,900✔
1299
{
1300
  auto it = model::tally_map.find(id);
1,900✔
1301
  if (it == model::tally_map.end()) {
1,900✔
1302
    set_errmsg(fmt::format("No tally exists with ID={}.", id));
2✔
1303
    return OPENMC_E_INVALID_ID;
2✔
1304
  }
1305

1306
  *index = it->second;
1,898✔
1307
  return 0;
1,898✔
1308
}
1309

1310
extern "C" void openmc_get_tally_next_id(int32_t* id)
×
1311
{
1312
  int32_t largest_tally_id = 0;
×
1313
  for (const auto& t : model::tallies) {
×
1314
    largest_tally_id = std::max(largest_tally_id, t->id_);
×
1315
  }
1316
  *id = largest_tally_id + 1;
×
1317
}
×
1318

1319
extern "C" int openmc_tally_get_estimator(int32_t index, int* estimator)
×
1320
{
1321
  if (index < 0 || index >= model::tallies.size()) {
×
1322
    set_errmsg("Index in tallies array is out of bounds.");
×
1323
    return OPENMC_E_OUT_OF_BOUNDS;
×
1324
  }
1325

1326
  *estimator = static_cast<int>(model::tallies[index]->estimator_);
×
1327
  return 0;
×
1328
}
1329

1330
extern "C" int openmc_tally_set_estimator(int32_t index, const char* estimator)
60✔
1331
{
1332
  if (index < 0 || index >= model::tallies.size()) {
60!
1333
    set_errmsg("Index in tallies array is out of bounds.");
×
1334
    return OPENMC_E_OUT_OF_BOUNDS;
×
1335
  }
1336

1337
  auto& t {model::tallies[index]};
60✔
1338

1339
  std::string est = estimator;
60✔
1340
  if (est == "analog") {
60!
1341
    t->estimator_ = TallyEstimator::ANALOG;
60✔
1342
  } else if (est == "collision") {
×
1343
    t->estimator_ = TallyEstimator::COLLISION;
×
1344
  } else if (est == "tracklength") {
×
1345
    t->estimator_ = TallyEstimator::TRACKLENGTH;
×
1346
  } else {
1347
    set_errmsg("Unknown tally estimator: " + est);
×
1348
    return OPENMC_E_INVALID_ARGUMENT;
×
1349
  }
1350
  return 0;
60✔
1351
}
60✔
1352

1353
extern "C" int openmc_tally_get_id(int32_t index, int32_t* id)
2,462✔
1354
{
1355
  if (index < 0 || index >= model::tallies.size()) {
2,462!
1356
    set_errmsg("Index in tallies array is out of bounds.");
×
1357
    return OPENMC_E_OUT_OF_BOUNDS;
×
1358
  }
1359

1360
  *id = model::tallies[index]->id_;
2,462✔
1361
  return 0;
2,462✔
1362
}
1363

1364
extern "C" int openmc_tally_set_id(int32_t index, int32_t id)
97✔
1365
{
1366
  if (index < 0 || index >= model::tallies.size()) {
97!
1367
    set_errmsg("Index in tallies array is out of bounds.");
×
1368
    return OPENMC_E_OUT_OF_BOUNDS;
×
1369
  }
1370

1371
  model::tallies[index]->set_id(id);
97✔
1372
  return 0;
97✔
1373
}
1374

1375
extern "C" int openmc_tally_get_type(int32_t index, int32_t* type)
1✔
1376
{
1377
  if (index < 0 || index >= model::tallies.size()) {
1!
1378
    set_errmsg("Index in tallies array is out of bounds.");
×
1379
    return OPENMC_E_OUT_OF_BOUNDS;
×
1380
  }
1381
  *type = static_cast<int>(model::tallies[index]->type_);
1✔
1382

1383
  return 0;
1✔
1384
}
1385

1386
extern "C" int openmc_tally_set_type(int32_t index, const char* type)
60✔
1387
{
1388
  if (index < 0 || index >= model::tallies.size()) {
60!
1389
    set_errmsg("Index in tallies array is out of bounds.");
×
1390
    return OPENMC_E_OUT_OF_BOUNDS;
×
1391
  }
1392
  if (strcmp(type, "volume") == 0) {
60✔
1393
    model::tallies[index]->type_ = TallyType::VOLUME;
45✔
1394
  } else if (strcmp(type, "mesh-surface") == 0) {
15!
1395
    model::tallies[index]->type_ = TallyType::MESH_SURFACE;
15✔
1396
  } else if (strcmp(type, "surface") == 0) {
×
1397
    model::tallies[index]->type_ = TallyType::SURFACE;
×
1398
  } else if (strcmp(type, "pulse-height") == 0) {
×
1399
    model::tallies[index]->type_ = TallyType::PULSE_HEIGHT;
×
1400
  } else {
1401
    set_errmsg(fmt::format("Unknown tally type: {}", type));
×
1402
    return OPENMC_E_INVALID_ARGUMENT;
×
1403
  }
1404

1405
  return 0;
60✔
1406
}
1407

1408
extern "C" int openmc_tally_get_active(int32_t index, bool* active)
2✔
1409
{
1410
  if (index < 0 || index >= model::tallies.size()) {
2!
1411
    set_errmsg("Index in tallies array is out of bounds.");
×
1412
    return OPENMC_E_OUT_OF_BOUNDS;
×
1413
  }
1414
  *active = model::tallies[index]->active_;
2✔
1415

1416
  return 0;
2✔
1417
}
1418

1419
extern "C" int openmc_tally_set_active(int32_t index, bool active)
61✔
1420
{
1421
  if (index < 0 || index >= model::tallies.size()) {
61!
1422
    set_errmsg("Index in tallies array is out of bounds.");
×
1423
    return OPENMC_E_OUT_OF_BOUNDS;
×
1424
  }
1425
  model::tallies[index]->active_ = active;
61✔
1426

1427
  return 0;
61✔
1428
}
1429

1430
extern "C" int openmc_tally_get_writable(int32_t index, bool* writable)
2✔
1431
{
1432
  if (index < 0 || index >= model::tallies.size()) {
2!
1433
    set_errmsg("Index in tallies array is out of bounds.");
×
1434
    return OPENMC_E_OUT_OF_BOUNDS;
×
1435
  }
1436
  *writable = model::tallies[index]->writable();
2✔
1437

1438
  return 0;
2✔
1439
}
1440

1441
extern "C" int openmc_tally_set_writable(int32_t index, bool writable)
37✔
1442
{
1443
  if (index < 0 || index >= model::tallies.size()) {
37!
1444
    set_errmsg("Index in tallies array is out of bounds.");
×
1445
    return OPENMC_E_OUT_OF_BOUNDS;
×
1446
  }
1447
  model::tallies[index]->set_writable(writable);
37✔
1448

1449
  return 0;
37✔
1450
}
1451

1452
extern "C" int openmc_tally_get_multiply_density(int32_t index, bool* value)
2✔
1453
{
1454
  if (index < 0 || index >= model::tallies.size()) {
2!
1455
    set_errmsg("Index in tallies array is out of bounds.");
×
1456
    return OPENMC_E_OUT_OF_BOUNDS;
×
1457
  }
1458
  *value = model::tallies[index]->multiply_density();
2✔
1459

1460
  return 0;
2✔
1461
}
1462

1463
extern "C" int openmc_tally_set_multiply_density(int32_t index, bool value)
24✔
1464
{
1465
  if (index < 0 || index >= model::tallies.size()) {
24!
1466
    set_errmsg("Index in tallies array is out of bounds.");
×
1467
    return OPENMC_E_OUT_OF_BOUNDS;
×
1468
  }
1469
  model::tallies[index]->set_multiply_density(value);
24✔
1470

1471
  return 0;
24✔
1472
}
1473

1474
extern "C" int openmc_tally_get_scores(int32_t index, int** scores, int* n)
17✔
1475
{
1476
  if (index < 0 || index >= model::tallies.size()) {
17!
1477
    set_errmsg("Index in tallies array is out of bounds.");
×
1478
    return OPENMC_E_OUT_OF_BOUNDS;
×
1479
  }
1480

1481
  *scores = model::tallies[index]->scores_.data();
17✔
1482
  *n = model::tallies[index]->scores_.size();
17✔
1483
  return 0;
17✔
1484
}
1485

1486
extern "C" int openmc_tally_set_scores(
98✔
1487
  int32_t index, int n, const char** scores)
1488
{
1489
  if (index < 0 || index >= model::tallies.size()) {
98!
1490
    set_errmsg("Index in tallies array is out of bounds.");
×
1491
    return OPENMC_E_OUT_OF_BOUNDS;
×
1492
  }
1493

1494
  vector<std::string> scores_str(scores, scores + n);
98✔
1495
  try {
1496
    model::tallies[index]->set_scores(scores_str);
98✔
1497
  } catch (const std::invalid_argument& ex) {
×
1498
    set_errmsg(ex.what());
×
1499
    return OPENMC_E_INVALID_ARGUMENT;
×
1500
  }
×
1501

1502
  return 0;
98✔
1503
}
98✔
1504

1505
extern "C" int openmc_tally_get_nuclides(int32_t index, int** nuclides, int* n)
21✔
1506
{
1507
  // Make sure the index fits in the array bounds.
1508
  if (index < 0 || index >= model::tallies.size()) {
21!
1509
    set_errmsg("Index in tallies array is out of bounds.");
×
1510
    return OPENMC_E_OUT_OF_BOUNDS;
×
1511
  }
1512

1513
  *n = model::tallies[index]->nuclides_.size();
21✔
1514
  *nuclides = model::tallies[index]->nuclides_.data();
21✔
1515

1516
  return 0;
21✔
1517
}
1518

1519
extern "C" int openmc_tally_set_nuclides(
122✔
1520
  int32_t index, int n, const char** nuclides)
1521
{
1522
  // Make sure the index fits in the array bounds.
1523
  if (index < 0 || index >= model::tallies.size()) {
122!
1524
    set_errmsg("Index in tallies array is out of bounds.");
×
1525
    return OPENMC_E_OUT_OF_BOUNDS;
×
1526
  }
1527

1528
  vector<std::string> words(nuclides, nuclides + n);
122✔
1529
  vector<int> nucs;
122✔
1530
  for (auto word : words) {
570✔
1531
    if (word == "total") {
449✔
1532
      nucs.push_back(-1);
60✔
1533
    } else {
1534
      auto search = data::nuclide_map.find(word);
389✔
1535
      if (search == data::nuclide_map.end()) {
389✔
1536
        int err = openmc_load_nuclide(word.c_str(), nullptr, 0);
91✔
1537
        if (err < 0) {
91✔
1538
          set_errmsg(openmc_err_msg);
1✔
1539
          return OPENMC_E_DATA;
1✔
1540
        }
1541
      }
1542
      nucs.push_back(data::nuclide_map.at(word));
388✔
1543
    }
1544
  }
449✔
1545

1546
  model::tallies[index]->nuclides_ = nucs;
121✔
1547

1548
  return 0;
121✔
1549
}
122✔
1550

1551
extern "C" int openmc_tally_get_filters(
92✔
1552
  int32_t index, const int32_t** indices, size_t* n)
1553
{
1554
  if (index < 0 || index >= model::tallies.size()) {
92!
1555
    set_errmsg("Index in tallies array is out of bounds.");
×
1556
    return OPENMC_E_OUT_OF_BOUNDS;
×
1557
  }
1558

1559
  *indices = model::tallies[index]->filters().data();
92✔
1560
  *n = model::tallies[index]->filters().size();
92✔
1561
  return 0;
92✔
1562
}
1563

1564
extern "C" int openmc_tally_set_filters(
102✔
1565
  int32_t index, size_t n, const int32_t* indices)
1566
{
1567
  // Make sure the index fits in the array bounds.
1568
  if (index < 0 || index >= model::tallies.size()) {
102!
1569
    set_errmsg("Index in tallies array is out of bounds.");
×
1570
    return OPENMC_E_OUT_OF_BOUNDS;
×
1571
  }
1572

1573
  // Set the filters.
1574
  try {
1575
    // Convert indices to filter pointers
1576
    vector<Filter*> filters;
102✔
1577
    for (int64_t i = 0; i < n; ++i) {
242✔
1578
      int32_t i_filt = indices[i];
140✔
1579
      filters.push_back(model::tally_filters.at(i_filt).get());
140✔
1580
    }
1581
    model::tallies[index]->set_filters(filters);
102✔
1582
  } catch (const std::out_of_range& ex) {
102!
1583
    set_errmsg("Index in tally filter array out of bounds.");
×
1584
    return OPENMC_E_OUT_OF_BOUNDS;
×
1585
  }
×
1586

1587
  return 0;
102✔
1588
}
1589

1590
//! Reset tally results and number of realizations
1591
extern "C" int openmc_tally_reset(int32_t index)
244✔
1592
{
1593
  // Make sure the index fits in the array bounds.
1594
  if (index < 0 || index >= model::tallies.size()) {
244!
1595
    set_errmsg("Index in tallies array is out of bounds.");
×
1596
    return OPENMC_E_OUT_OF_BOUNDS;
×
1597
  }
1598

1599
  model::tallies[index]->reset();
244✔
1600
  return 0;
244✔
1601
}
1602

1603
extern "C" int openmc_tally_get_n_realizations(int32_t index, int32_t* n)
312✔
1604
{
1605
  // Make sure the index fits in the array bounds.
1606
  if (index < 0 || index >= model::tallies.size()) {
312!
1607
    set_errmsg("Index in tallies array is out of bounds.");
×
1608
    return OPENMC_E_OUT_OF_BOUNDS;
×
1609
  }
1610

1611
  *n = model::tallies[index]->n_realizations_;
312✔
1612
  return 0;
312✔
1613
}
1614

1615
//! \brief Returns a pointer to a tally results array along with its shape.
1616
//! This allows a user to obtain in-memory tally results from Python directly.
1617
extern "C" int openmc_tally_results(
1,448✔
1618
  int32_t index, double** results, size_t* shape)
1619
{
1620
  // Make sure the index fits in the array bounds.
1621
  if (index < 0 || index >= model::tallies.size()) {
1,448!
1622
    set_errmsg("Index in tallies array is out of bounds.");
×
1623
    return OPENMC_E_OUT_OF_BOUNDS;
×
1624
  }
1625

1626
  const auto& t {model::tallies[index]};
1,448✔
1627
  if (t->results_.size() == 0) {
1,448!
1628
    set_errmsg("Tally results have not been allocated yet.");
×
1629
    return OPENMC_E_ALLOCATE;
×
1630
  }
1631

1632
  // Set pointer to results and copy shape
1633
  *results = t->results_.data();
1,448✔
1634
  auto s = t->results_.shape();
1,448✔
1635
  shape[0] = s[0];
1,448✔
1636
  shape[1] = s[1];
1,448✔
1637
  shape[2] = s[2];
1,448✔
1638
  return 0;
1,448✔
1639
}
1640

1641
extern "C" int openmc_global_tallies(double** ptr)
1✔
1642
{
1643
  *ptr = simulation::global_tallies.data();
1✔
1644
  return 0;
1✔
1645
}
1646

1647
extern "C" size_t tallies_size()
101✔
1648
{
1649
  return model::tallies.size();
101✔
1650
}
1651

1652
// given a tally ID, remove it from the tallies vector
1653
extern "C" int openmc_remove_tally(int32_t index)
1✔
1654
{
1655
  // check that id is in the map
1656
  if (index < 0 || index >= model::tallies.size()) {
1!
1657
    set_errmsg("Index in tallies array is out of bounds.");
×
1658
    return OPENMC_E_OUT_OF_BOUNDS;
×
1659
  }
1660

1661
  // delete the tally via iterator pointing to correct position
1662
  // this calls the Tally destructor, removing the tally from the map as well
1663
  model::tallies.erase(model::tallies.begin() + index);
1✔
1664

1665
  return 0;
1✔
1666
}
1667

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