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

openmc-dev / openmc / 22777075007

06 Mar 2026 06:42PM UTC coverage: 81.594%. First build
22777075007

Pull #3732

github

web-flow
Merge c217ba86b into 908e63115
Pull Request #3732: Volume Calculation enhancement including refactoring and real-valued scoring implementation

17616 of 25327 branches covered (69.55%)

Branch coverage included in aggregate %.

302 of 307 new or added lines in 5 files covered. (98.37%)

58066 of 67427 relevant lines covered (86.12%)

44723153.47 hits per line

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

93.32
/src/volume_calc.cpp
1
#include "openmc/volume_calc.h"
2

3
#include "openmc/capi.h"
4
#include "openmc/cell.h"
5
#include "openmc/constants.h"
6
#include "openmc/distribution_multi.h"
7
#include "openmc/error.h"
8
#include "openmc/finalize.h"
9
#include "openmc/geometry.h"
10
#include "openmc/hdf5_interface.h"
11
#include "openmc/initialize.h"
12
#include "openmc/material.h"
13
#include "openmc/message_passing.h"
14
#include "openmc/mgxs_interface.h"
15
#include "openmc/nuclide.h"
16
#include "openmc/openmp_interface.h"
17
#include "openmc/output.h"
18
#include "openmc/random_dist.h"
19
#include "openmc/random_lcg.h"
20
#include "openmc/settings.h"
21
#include "openmc/timer.h"
22
#include "openmc/xml_interface.h"
23

24
#include "openmc/tensor.h"
25
#include <fmt/core.h>
26

27
#include <algorithm> // for copy
28
#include <cmath>     // for pow, sqrt
29
#include <unordered_set>
30

31
namespace openmc {
32

33
//==============================================================================
34
// Global variables
35
//==============================================================================
36

37
namespace model {
38
vector<VolumeCalculation> volume_calcs;
39
} // namespace model
40

41
//==============================================================================
42
// VolumeCalculation implementation
43
//==============================================================================
44

45
VolumeCalculation::VolumeCalculation(pugi::xml_node node)
344✔
46
{
47
  // Read domain type (cell, material or universe)
48
  std::string domain_type = get_node_value(node, "domain_type");
344✔
49
  if (domain_type == "cell") {
344✔
50
    domain_type_ = TallyDomain::CELL;
152✔
51
  } else if (domain_type == "material") {
192✔
52
    domain_type_ = TallyDomain::MATERIAL;
142✔
53
  } else if (domain_type == "universe") {
50!
54
    domain_type_ = TallyDomain::UNIVERSE;
50✔
55
  } else {
56
    fatal_error(std::string("Unrecognized domain type for stochastic "
×
57
                            "volume calculation: " +
58
                            domain_type));
59
  }
60

61
  // Read domain IDs, bounding corodinates and number of samples
62
  domain_ids_ = get_node_array<int>(node, "domain_ids");
344✔
63
  lower_left_ = get_node_array<double>(node, "lower_left");
344✔
64
  upper_right_ = get_node_array<double>(node, "upper_right");
344✔
65
  n_samples_ = std::stoull(get_node_value(node, "samples"));
688✔
66

67
  // Determine volume of bounding box
68
  const Position d {upper_right_ - lower_left_};
344✔
69
  volume_sample_ = d.x * d.y * d.z;
344✔
70

71
  if (check_for_node(node, "threshold")) {
344✔
72
    pugi::xml_node threshold_node = node.child("threshold");
120✔
73

74
    threshold_ = std::stod(get_node_value(threshold_node, "threshold"));
240✔
75
    if (threshold_ <= 0.0) {
120!
76
      fatal_error(fmt::format("Invalid error threshold {} provided for a "
×
77
                              "volume calculation.",
78
        threshold_));
×
79
    }
80

81
    // Prior calculation of the trigger volume fractions-related values t'
82
    // deriviated from the given volumes-related values t to prevent excessive
83
    // repeated computations during Monte Carlo execution. The values of t' are
84
    // computed via the sample mean \bar{x} and the adjusted sample variance
85
    // s^2.
86
    std::string tmp = get_node_value(threshold_node, "type");
120✔
87
    if (tmp == "variance") {
120✔
88
      trigger_type_ = TriggerMetric::variance;
30✔
89
      // Condition: s^2 < t' / N
90
      // t' = s^2 = t / (V_b)^2, in sq. volume fraction units
91
      threshold_cnd_ = threshold_ / std::pow(volume_sample_, 2);
30✔
92
      ;
30✔
93
    } else if (tmp == "std_dev") {
90✔
94
      trigger_type_ = TriggerMetric::standard_deviation;
30✔
95
      // Condition: s^2 < t' / N
96
      // t' = s^2 = (t / V_b)^2, in sq. volume fraction units
97
      threshold_cnd_ = std::pow(threshold_ / volume_sample_, 2);
30✔
98
    } else if (tmp == "rel_err") {
60!
99
      trigger_type_ = TriggerMetric::relative_error;
60✔
100
      // Condition: s^2 / \bar{x}^2 < t' / N
101
      // t' = s^2 / \bar{x}^2 = t^2, in relative units
102
      threshold_cnd_ = threshold_ * threshold_;
60✔
103
    } else {
104
      fatal_error(fmt::format(
×
105
        "Invalid volume calculation trigger type '{}' provided.", tmp));
106
    }
107

108
    if (check_for_node(threshold_node, "max_iterations")) {
120✔
109
      max_iterations_ =
60✔
110
        std::stoi(get_node_value(threshold_node, "max_iterations"));
60✔
111
      if (max_iterations_ <= 0) {
30!
NEW
112
        fatal_error(fmt::format(
×
NEW
113
          "Invalid error max_iterations {} provided.", max_iterations_));
×
114
      }
115
    }
116
  }
120✔
117

118
  // Ensure there are no duplicates by copying elements to a set and then
119
  // comparing the length with the original vector
120
  std::unordered_set<int> unique_ids(domain_ids_.cbegin(), domain_ids_.cend());
344!
121
  if (unique_ids.size() != domain_ids_.size()) {
344!
122
    throw std::runtime_error {"Domain IDs for a volume calculation "
×
123
                              "must be unique."};
×
124
  }
125
}
344✔
126

127
void VolumeCalculation::execute(CalcResults& master_results) const
330✔
128
{
129
  // Check to make sure domain IDs are valid
130
  for (auto uid : domain_ids_) {
936✔
131
    switch (domain_type_) {
633!
132
    case TallyDomain::CELL:
347✔
133
      if (model::cell_map.find(uid) == model::cell_map.end()) {
347✔
134
        throw std::runtime_error {fmt::format(
9!
135
          "Cell {} in volume calculation does not exist in geometry.", uid)};
18✔
136
      }
137
      break;
138
    case TallyDomain::MATERIAL:
236✔
139
      if (model::material_map.find(uid) == model::material_map.end()) {
236✔
140
        throw std::runtime_error {fmt::format(
9!
141
          "Material {} in volume calculation does not exist in geometry.",
142
          uid)};
18✔
143
      }
144
      break;
145
    case TallyDomain::UNIVERSE:
50✔
146
      if (model::universe_map.find(uid) == model::universe_map.end()) {
50✔
147
        throw std::runtime_error {fmt::format(
9!
148
          "Universe {} in volume calculation does not exist in geometry.",
149
          uid)};
18✔
150
      }
151
    }
152
  }
153

154
  // Shared data that is collected from all threads
155
  const int n_domains = domain_ids_.size();
303!
156

157
  // Divide work over MPI processes
158
  uint64_t min_samples = n_samples_ / mpi::n_procs;
303✔
159
  uint64_t remainder = n_samples_ % mpi::n_procs;
303✔
160
  uint64_t i_start, i_end;
303✔
161
  if (mpi::rank < remainder) {
303!
162
    i_start = (min_samples + 1) * mpi::rank;
×
163
    i_end = i_start + min_samples + 1;
×
164
  } else {
165
    i_start =
303✔
166
      (min_samples + 1) * remainder + (mpi::rank - remainder) * min_samples;
303✔
167
    i_end = i_start + min_samples;
303✔
168
  }
169

170
  while (true) {
28,551✔
171

172
#pragma omp parallel
11,735✔
173
    {
7,828✔
174
      // Temporary variables that are private to each thread
175
      CalcResults results(*this);
7,828✔
176
      results.sampling_time.start();
7,828✔
177
      Particle p;
7,828✔
178

179
      // Sample locations and count scores
180
#pragma omp for
181
      for (size_t i = i_start; i < i_end; i++) {
3,688,878✔
182
        uint64_t id = master_results.iterations * n_samples_ + i;
3,681,050✔
183
        uint64_t seed = init_seed(id, STREAM_VOLUME);
3,681,050✔
184

185
        p.n_coord() = 1;
3,681,050✔
186
        p.r() = {uniform_distribution(lower_left_.x, upper_right_.x, &seed),
7,362,100✔
187
          uniform_distribution(lower_left_.y, upper_right_.y, &seed),
3,681,050✔
188
          uniform_distribution(lower_left_.z, upper_right_.z, &seed)};
3,681,050✔
189
        const double sqrt3_1 = 1. / std::sqrt(3.);
3,681,050✔
190
        p.u() = {sqrt3_1, sqrt3_1, sqrt3_1};
3,681,050✔
191

192
        // TO REVIEWER: THE SWITCH IS TRANSFERED TO score_hit()
193
        if (exhaustive_find_cell(p))
3,681,050✔
194
          this->score_hit(p, results);
2,733,235✔
195

196
        results.n_samples++;
3,681,050✔
197

198
        // This passing across all tallies after each sample can be
199
        // inefficient for the case of large number of domains and small
200
        // size of batch, but the batch size is currently assigned to 1
201
        // for keep the input format being unchanged
202
        const double batch_size_1 = 1. / static_cast<double>(1);
3,681,050✔
203
        for (auto& vt : results.vol_tallies) {
11,636,200✔
204
          for (auto& vol_tally : vt) {
24,561,983✔
205
            vol_tally.finalize_batch(batch_size_1);
16,606,833✔
206
          }
207
        }
208
      } // sample/batch loop
209

210
      results.sampling_time.stop();
7,828✔
211
      results.cost = results.sampling_time.elapsed();
7,828✔
212

213
      // At this point, each thread has its own volume tallies lists and we
214
      // now need to reduce them. OpenMP is not nearly smart enough to do
215
      // this on its own, so we have to manually reduce them
216
      reduce_results(results, master_results);
7,828✔
217
    } // omp parallel
7,828✔
218

219
    // bump iteration counter
220
    master_results.iterations++;
19,563✔
221

222
    // TO REVIEWER: THE MPI-RELATED PART IS MOVED TO collect_MPI()
223
#ifdef OPENMC_MPI
224
    master_results.collect_MPI(); // collect results to master process
10,421✔
225
#endif
226
    // Process volume estimation results in master process for the trigger state
227
    // determination
228
    bool stop_calc =
39,014✔
229
      mpi::master && (trigger_type_ == TriggerMetric::not_active ||
19,563✔
230
                       master_results.iterations == max_iterations_);
14,212✔
231

232
    if (!stop_calc) {
19,451✔
233
      // Compute current trigger state across totals (0th elements) only
234
      for (const auto& vt : master_results.vol_tallies) {
19,606✔
235
        stop_calc = vt[0].trigger_state(
19,516✔
236
          trigger_type_, threshold_cnd_, master_results.n_samples);
19,516✔
237
        if (!stop_calc)
19,516✔
238
          break;
239
      }
240
    }
241

242
#ifdef OPENMC_MPI
243
    // Send the state of calculation continuation just obtained in master
244
    // process to all processes
245
    MPI_Bcast(&stop_calc, 1, MPI_CXX_BOOL, 0, mpi::intracomm);
10,421✔
246
#endif
247

248
    if (!stop_calc)
19,563✔
249
      continue; // while loop
19,260✔
250
    // No trigger is applied or the trigger condition is satisfied, we're
251
    // done
252

253
    if (mpi::master) {
303✔
254
      // Normalize all results on the bounding primitive volume and compute
255
      // stddev
256
      for (auto& vt : master_results.vol_tallies) {
725✔
257
        for (auto& vol_tally : vt) {
1,480✔
258
          vol_tally.score_acc = get_tally_results(
1,002✔
259
            master_results.n_samples, volume_sample_, vol_tally);
1,002✔
260
        }
261
      }
262

263
      // Compute nuclides
264
      for (int i_domain = 0; i_domain < n_domains; ++i_domain) {
725✔
265
        // Create 2D array to store atoms/uncertainty for each nuclide. Later
266
        // this is compressed into vectors storing only those nuclides that are
267
        // non-zero
268
        auto n_nuc =
478✔
269
          settings::run_CE ? data::nuclides.size() : data::mg.nuclides_.size();
478✔
270
        auto atoms =
478✔
271
          tensor::zeros<double>({static_cast<size_t>(n_nuc), size_t {2}});
478✔
272

273
        for (int j = 0; j < master_results.vol_tallies[i_domain].size(); ++j) {
1,480✔
274
          const int i_material = master_results.vol_tallies[i_domain][j].index;
1,002✔
275
          if (i_material == MATERIAL_VOID || i_material == _INDEX_TOTAL)
1,002✔
276
            continue;
489✔
277

278
          const auto& mat = model::materials[i_material];
513✔
279
          for (int k = 0; k < mat->nuclide_.size(); ++k) {
2,008✔
280
            auto& volume = master_results.vol_tallies[i_domain][j].score_acc;
1,495✔
281
            // Collect calculated nuclide amounts N [atoms] and stddev as
282
            // N = V [cm^3] * \rho [atoms/b-cm] * 1.e24 [b-cm/cm^3]
283
            atoms(mat->nuclide_[k], 0) +=
1,495✔
284
              volume[0] * mat->atom_density(k) * 1.0e24;
1,495✔
285
            atoms(mat->nuclide_[k], 1) +=
1,495✔
286
              volume[1] * mat->atom_density(k) * 1.0e24;
1,495✔
287
          }
288
        }
289

290
        // Get reference to result for this domain
291
        auto& result {master_results.nuc_results[i_domain]};
478✔
292
        // Convert full arrays to vectors
293
        for (int j = 0; j < n_nuc; ++j) {
3,440✔
294
          if (atoms(j, 0) > 0.0) {
2,962✔
295
            result.nuclides.push_back(j);
1,399✔
296
            result.atoms.push_back(atoms(j, 0));
1,399✔
297
            result.uncertainty.push_back(atoms(j, 1));
1,399✔
298
          }
299
        }
300
      } // end domains loop
478✔
301
    }
302

303
    return;
303✔
304

305
  } // end while
8,988✔
306
}
307

308
void VolumeCalculation::show_vol_stat(
741✔
309
  const std::string label, const std::string units, const double value) const
310
{
311
  fmt::print("{0:<20} = {2:10.4e} {1:<}\n", label, units, value);
741✔
312
}
741✔
313

314
void VolumeCalculation::show_volume(const std::string domain_type,
478✔
315
  const int domain_id, const std::string region_name, const double mean,
316
  const double stddev) const
317
{
318
  fmt::print(" {0:>9}{1:>6}: {2:10.4e} +/- {3:10.4e} cm^3", domain_type,
478✔
319
    domain_id, mean, stddev);
320
  if (!region_name.empty()) {
478✔
321
    fmt::print("   //{:<}", region_name);
92✔
322
  }
323
  fmt::print("\n");
478✔
324
}
478✔
325

326
// TO REVIEWER: I/O BLOCK BEGINS, I WOULD PREFFER TO PUT IT AT THE END OF FILE
327
// BUT KEEPT THE FILE STRUCTURE UNCHANGED
328
void VolumeCalculation::show_results(const CalcResults& results) const
247✔
329
{
330
  // Show tracing statistics
331
  write_message(5, " ");
247✔
332
  show_vol_stat(
494✔
333
    "Total sample size", "", static_cast<double>(results.n_samples));
247✔
334
  show_vol_stat("Running cost", "thread-sec", results.cost);
494✔
335
  show_vol_stat("Cost of hitting", "thread-sec/hit",
494✔
336
    static_cast<double>(results.cost) / static_cast<double>(results.n_samples));
247✔
337
  write_message(5, " ");
247✔
338

339
  std::string domain_type;
247✔
340
  if (domain_type_ == TallyDomain::CELL) {
247✔
341
    domain_type = "Cell";
112✔
342
  } else if (domain_type_ == TallyDomain::MATERIAL) {
135✔
343
    domain_type = "Material";
102✔
344
  } else {
345
    domain_type = "Universe";
33✔
346
  }
347

348
  // Display domain volumes
349
  for (int j = 0; j < domain_ids_.size(); j++) {
725✔
350
    std::string region_name {""};
478✔
351
    if (domain_type_ == TallyDomain::CELL) {
478✔
352
      int cell_idx = model::cell_map[domain_ids_[j]];
266✔
353
      region_name = model::cells[cell_idx]->name();
266✔
354
    } else if (domain_type_ == TallyDomain::MATERIAL) {
212✔
355
      int mat_idx = model::material_map[domain_ids_[j]];
179✔
356
      region_name = model::materials[mat_idx]->name();
179✔
357
    }
358
    if (region_name.size())
478✔
359
      region_name.insert(0, " "); // prepend space for formatting
80✔
360

361
    show_volume(domain_type, domain_ids_[j], region_name,
956✔
362
      results.vol_tallies[j][0].score_acc[0],
478✔
363
      results.vol_tallies[j][0].score_acc[1]);
478✔
364
  }
478✔
365
  write_message(4, " "); // Blank line afer results printed
494✔
366
}
247✔
367

368
void VolumeCalculation::to_hdf5(
247✔
369
  const std::string& filename, const CalcResults& results) const
370
{
371
  // Create HDF5 file
372
  hid_t file_id = file_open(filename, 'w');
247✔
373

374
  // Write header info
375
  write_attribute(file_id, "filetype", "volume");
247✔
376
  write_attribute(file_id, "version", VERSION_VOLUME);
247✔
377
  write_attribute(file_id, "openmc_version", VERSION);
247✔
378
#ifdef GIT_SHA1
379
  write_attribute(file_id, "git_sha1", GIT_SHA1);
380
#endif
381

382
  // Write current date and time
383
  write_attribute(file_id, "date_and_time", time_stamp());
494✔
384

385
  // Write basic metadata
386
  write_attribute(file_id, "samples", n_samples_);
247✔
387
  write_attribute(file_id, "lower_left", lower_left_);
247✔
388
  write_attribute(file_id, "upper_right", upper_right_);
247✔
389
  // Write trigger info
390
  if (trigger_type_ != TriggerMetric::not_active) {
247✔
391
    write_attribute(file_id, "iterations", results.iterations);
88✔
392
    write_attribute(file_id, "threshold", threshold_);
88✔
393
    std::string trigger_str;
88!
394
    switch (trigger_type_) {
88!
395
    case TriggerMetric::variance:
22✔
396
      trigger_str = "variance";
22✔
397
      break;
398
    case TriggerMetric::standard_deviation:
22✔
399
      trigger_str = "std_dev";
22✔
400
      break;
401
    case TriggerMetric::relative_error:
44✔
402
      trigger_str = "rel_err";
44✔
403
      break;
404
    default:
405
      break;
406
    }
407
    write_attribute(file_id, "trigger_type", trigger_str);
88✔
408
    // check max_iterations on default value
409
    if (max_iterations_ !=
88✔
410
        std::numeric_limits<decltype(max_iterations_)>::max())
411
      write_attribute(file_id, "max_iterations", max_iterations_);
22✔
412
  } else {
88✔
413
    write_attribute(file_id, "iterations", 1);
159✔
414
  }
415

416
  if (domain_type_ == TallyDomain::CELL) {
247✔
417
    write_attribute(file_id, "domain_type", "cell");
112✔
418
  } else if (domain_type_ == TallyDomain::MATERIAL) {
135✔
419
    write_attribute(file_id, "domain_type", "material");
102✔
420
  } else if (domain_type_ == TallyDomain::UNIVERSE) {
33!
421
    write_attribute(file_id, "domain_type", "universe");
33✔
422
  }
423

424
  for (int i = 0; i < domain_ids_.size(); ++i) {
725✔
425
    hid_t group_id =
478✔
426
      create_group(file_id, fmt::format("domain_{}", domain_ids_[i]));
478✔
427

428
    // Write volume for domain
429
    const auto& result {results.nuc_results[i]};
478✔
430
    write_dataset(group_id, "volume", results.vol_tallies[i][0].score_acc);
478✔
431

432
    // Create array of nuclide names from the vector
433
    auto n_nuc = result.nuclides.size();
478✔
434

435
    vector<std::string> nucnames;
478✔
436
    for (int i_nuc : result.nuclides) {
1,877✔
437
      nucnames.push_back(settings::run_CE ? data::nuclides[i_nuc]->name_
1,399✔
438
                                          : data::mg.nuclides_[i_nuc].name);
484✔
439
    }
440

441
    // Create array of total # of atoms with uncertainty for each nuclide
442
    tensor::Tensor<double> atom_data({static_cast<size_t>(n_nuc), size_t {2}});
478✔
443
    for (size_t k = 0; k < static_cast<size_t>(n_nuc); ++k) {
1,877✔
444
      atom_data(k, 0) = result.atoms[k];
1,399✔
445
      atom_data(k, 1) = result.uncertainty[k];
1,399✔
446
    }
447

448
    // Write results
449
    write_dataset(group_id, "nuclides", nucnames);
478✔
450
    write_dataset(group_id, "atoms", atom_data);
478✔
451

452
    close_group(group_id);
478✔
453
  }
478✔
454

455
  file_close(file_id);
247✔
456
}
247✔
457

458
// TO REVIEWER: THIS IS THE SAME check_hit()
459
void VolumeCalculation::check_hit(const int32_t i_material,
10,974,551✔
460
  const double contrib, vector<VolTally>& vol_tallies) const
461
{
462
  // Contribute to entire domain result tally
463
  vol_tallies[0].score += contrib;
10,974,551✔
464

465
  // Check if this material was previously hit and if so, contribute score
466
  for (int j = 1; j < vol_tallies.size(); j++) {
11,566,721✔
467
    if (vol_tallies[j].index == i_material) {
11,485,465✔
468
      vol_tallies[j].score += contrib;
10,893,295✔
469
      return;
10,893,295✔
470
    }
471
  }
472

473
  // The material was not previously hit, append an entry to the material
474
  // indices and hits lists
475
  vol_tallies.push_back(VolTally(i_material, contrib));
81,256✔
476
}
477

478
// TO REVIEWER: THIS IS AN EQUIVALENT OF THE reduce_indicies_hits() TEMPLATE
479
// FROM volume_calc.h
480
void VolumeCalculation::reduce_results(
31,298✔
481
  const CalcResults& local_results, CalcResults& results) const
482
{
483
  auto n_threads = num_threads();
31,298✔
484

485
  // Collect scalar variables
486
#pragma omp for ordered schedule(static, 1)
23,470!
487
  for (int i = 0; i < n_threads; ++i) {
39,126✔
488
#pragma omp ordered
23,470✔
489
    {
31,298✔
490
      results.n_samples += local_results.n_samples;
31,298✔
491
      results.cost += local_results.cost;
31,298!
492
    }
493
  }
494

495
  // Collect vectored domain-wise results
496
  for (int i_domain = 0; i_domain < domain_ids_.size(); ++i_domain) {
123,948✔
497
    const vector<VolTally>& local_vol_tall =
92,650✔
498
      local_results.vol_tallies[i_domain];
92,650!
499
    vector<VolTally>& vol_tall = results.vol_tallies[i_domain];
92,650!
500

501
#pragma omp for ordered schedule(static, 1)
69,488!
502
    for (int i = 0; i < n_threads; ++i) {
115,812✔
503
#pragma omp ordered
69,488✔
504
      for (int j = 0; j < local_vol_tall.size(); ++j) {
266,556✔
505
        // Check if this material has been added to the master list and if
506
        // so, accumulate scores
507
        const auto ind {local_vol_tall[j].index};
173,906✔
508
        const auto it = std::find_if(vol_tall.begin(), vol_tall.end(),
173,906✔
509
          [ind](const VolTally& vt) { return vt.index == ind; });
3,226,471!
510
        if (it == vol_tall.end()) {
173,906✔
511
          vol_tall.push_back(local_vol_tall[j]);
21,028✔
512
        } else {
513
          vol_tall[it - vol_tall.begin()].append_tally(local_vol_tall[j]);
152,878✔
514
        }
515
      }
516
    }
517
  }
518
}
31,298✔
519

520
#ifdef OPENMC_MPI
521
// TO REVIEWER: THIS IS AN EQUIVALENT OF THE RELATED MPI-INTERCHANGE BLOCK OF
522
// execute()
523
void VolumeCalculation::CalcResults::collect_MPI()
10,421✔
524
{
525
  vector<int> domain_sizes(vol_tallies.size());
10,421✔
526

527
  if (!mpi::master) {
10,421✔
528
    // n_domain + 2 MPI messages will be send in total to node mpi::master
529

530
    // To determination of an unique tag for each MPI message (as below)
531
    int mpi_offset = mpi::rank * (vol_tallies.size() + 2) + 2;
5,192✔
532

533
    // Pass root data of the struct
534
    MPI_Send(
5,192✔
535
      (void*)this, 1, mpi::volume_results, 0, mpi_offset - 2, mpi::intracomm);
536

537
    // Pass sizes of domain-wise data
538
    for (int i_domain = 0; i_domain < vol_tallies.size(); i_domain++) {
20,600✔
539
      domain_sizes[i_domain] = vol_tallies[i_domain].size();
15,408✔
540
    }
541

542
    MPI_Send(domain_sizes.data(), domain_sizes.size(), MPI_INT, 0,
5,192✔
543
      mpi_offset - 1, mpi::intracomm);
544

545
    // Pass domain-wise data of struct
546
    for (int i_domain = 0; i_domain < vol_tallies.size(); i_domain++) {
20,600✔
547
      MPI_Send(vol_tallies[i_domain].data(), domain_sizes[i_domain],
15,408✔
548
        mpi::volume_tally, 0, mpi_offset + i_domain, mpi::intracomm);
549
    }
550

551
    this->reset(); // Delete passed to main process data
5,192✔
552

553
  } else {
554

555
    // n_domain + 2 MPI messages will be recieved in total from node mpi::master
556

557
    for (int i_proc = 1; i_proc < mpi::n_procs; i_proc++) {
10,421✔
558

559
      CalcResults res_buff(*this); // temporary storage for recived data
5,192✔
560

561
      // To determination of an unique tag for each MPI message (as above)
562
      int mpi_offset = i_proc * (vol_tallies.size() + 2) + 2;
5,192✔
563

564
      // Pass root data of struct
565
      MPI_Recv(&res_buff, 1, mpi::volume_results, i_proc, mpi_offset - 2,
5,192✔
566
        mpi::intracomm, MPI_STATUS_IGNORE);
567

568
      // Pass sizes of domain-wise data
569
      MPI_Recv(domain_sizes.data(), domain_sizes.size(), MPI_INT, i_proc,
5,192✔
570
        mpi_offset - 1, mpi::intracomm, MPI_STATUS_IGNORE);
571

572
      // Pass domain-wise data of struct
573
      for (int i_domain = 0; i_domain < vol_tallies.size(); i_domain++) {
20,600✔
574
        res_buff.vol_tallies[i_domain].resize(domain_sizes[i_domain]);
15,408✔
575
        MPI_Recv(res_buff.vol_tallies[i_domain].data(), domain_sizes[i_domain],
15,408✔
576
          mpi::volume_tally, i_proc, mpi_offset + i_domain, mpi::intracomm,
577
          MPI_STATUS_IGNORE);
578
      }
579

580
      this->append(res_buff);
5,192✔
581
    }
5,192✔
582
  }
583
}
10,421✔
584
#endif
585

586
// TO REVIEWER: STATISTICS AND TALLY MANIPULATION BLOCK BEGIN
587
VolumeCalculation::CalcResults::CalcResults(const VolumeCalculation& vol_calc)
31,628✔
588
{
589
  n_samples = 0;
31,628✔
590
  iterations = 0;
31,628✔
591
  cost = 0.;
31,628✔
592
  for (int i = 0; i < vol_calc.domain_ids_.size(); i++) {
124,911✔
593
    vector<VolTally> vt_vect; // Tally group for a domain
93,283✔
594
    vt_vect.push_back(
93,283✔
595
      VolTally(_INDEX_TOTAL)); // Zero-element tally for entire domain totals
93,283✔
596
    vol_tallies.push_back(vt_vect);
93,283✔
597
    nuc_results.push_back(NuclResult());
93,283✔
598
  }
93,283✔
599
}
31,628✔
600

601
void VolumeCalculation::CalcResults::reset()
5,192✔
602
{
603
  n_samples = 0;
5,192✔
604
  cost = 0.;
5,192✔
605
  for (auto& vt : vol_tallies) {
20,600✔
606
    std::fill(vt.begin(), vt.end(), VolTally());
46,224✔
607
  }
608
}
5,192✔
609

610
// TO REVIEWER: THIS IS A PART OF THE RELATED TO MPI-INTERCHANGE
611
// BLOCK OF execute()
612
void VolumeCalculation::CalcResults::append(const CalcResults& other)
5,192✔
613
{
614
  n_samples += other.n_samples;
5,192✔
615
  cost += other.cost;
5,192✔
616

617
  // The domain-wise vectors this.vol_tallies and other.vol_tallies are
618
  // conformed each to other by definition
619
  for (auto id = 0; id < vol_tallies.size(); id++) {
20,600✔
620
    // Merging current domain vector from other.vol_tallies into this via
621
    // pair-wise comparisons
622
    for (const auto& vt_other : other.vol_tallies[id]) {
4,590,776✔
623
      bool already_appended = false;
4,575,368✔
624
      for (auto& vt : vol_tallies[id]) {
11,407,896✔
625
        if (vt.index == vt_other.index) {
11,407,848✔
626
          vt.append_tally(vt_other);
4,575,320✔
627
          already_appended = true;
4,575,320✔
628
          break;
4,575,320✔
629
        }
630
      }
631
      if (!already_appended)
4,575,368✔
632
        vol_tallies[id].push_back(vt_other);
48✔
633
    }
634
  }
635
}
5,192✔
636

637
inline void VolumeCalculation::VolTally::finalize_batch(
16,606,833✔
638
  const double batch_size_1)
639
{
640
  if (score != 0.) {
16,606,833✔
641
    score *= batch_size_1;
5,431,410✔
642
    score_acc[0] += score;
5,431,410✔
643
    score_acc[1] += score * score;
5,431,410✔
644
    score = 0.;
5,431,410✔
645
  }
646
}
647

648
inline void VolumeCalculation::VolTally::assign_tally(const VolTally& vol_tally)
649
{
650
  score = vol_tally.score;
651
  score_acc = vol_tally.score_acc;
652
  index = vol_tally.index;
653
}
654

655
inline void VolumeCalculation::VolTally::append_tally(const VolTally& vol_tally)
4,728,198✔
656
{
657
  score += vol_tally.score;
4,728,198✔
658
  score_acc[0] += vol_tally.score_acc[0];
4,728,198✔
659
  score_acc[1] += vol_tally.score_acc[1];
4,728,198✔
660
}
661

662
// TO REVIEWER: IF THIS APPROACH WILL BE FOUND OBFUSCATED, IT CAN BE ADJUSTED TO
663
// LITERAL FORMULAE TRANSLATION WITH REPEATING MULTIPLY CALCULATIONS OF
664
// THRESHOLD SQUARE ROOTS AND DIVISIONS FOR EACH VOLUME DOMAIN IN THE END OF
665
// EACH BATCH
666
inline bool VolumeCalculation::VolTally::trigger_state(
19,516✔
667
  const TriggerMetric trigger_type, const double threshold,
668
  const size_t& n_samples) const
669
{
670
  // For sample contribution to volume fraction limited by 1, the maximal
671
  // allowed n_samples value is around 1.e102, but this is still much larger
672
  // than the size_t limit equal to ~1.8e19
673
  const double ns1 = static_cast<double>(n_samples - 1);
19,516✔
674
  const double ns = static_cast<double>(n_samples);
19,516✔
675
  const double mean_xi_sq = score_acc[0] * score_acc[0];
19,516✔
676

677
  // Adjusted sample variance: s^2 = (\bar{x^2} - \bar{x}^2) / (N-1)
678
  // \bar{x}^2 = mean_xi_sq / N^2, \bar{\x^2} = score_acc[1] / N, N = ns
679
  switch (trigger_type) {
19,516✔
680
  case TriggerMetric::variance:
18,928✔
681
  case TriggerMetric::standard_deviation:
18,928✔
682
    // Condition: s^2 / N < t'
683
    // Equivalent implementation:
684
    // N^2 * (\bar{x^2} - \bar{x}^2) < t' * (N-1) * N^2
685
    return score_acc[1] * ns - mean_xi_sq < threshold * ns1 * ns * ns;
18,928✔
686
  case TriggerMetric::relative_error:
540✔
687
    // Condition: (s^2 / \mu^2) / N < t'
688
    // Equivalent implementation:
689
    // N^2 * (\bar{x^2} - \bar{x}^2) < t' * (N-1) * (N * \bar{x})^2
690
    return score_acc[1] * ns - mean_xi_sq < threshold * ns1 * mean_xi_sq;
540✔
691
  default:
692
    return true;
693
  }
694
}
695

696
array<double, 2> VolumeCalculation::get_tally_results(const size_t& n_samples,
1,002✔
697
  const double coeff_norm, const VolTally& vol_tally) const
698
{
699
  array<double, 2> volume;
1,002✔
700
  const double ns_1 = 1. / static_cast<double>(n_samples);
1,002✔
701
  volume[0] = vol_tally.score_acc[0] * ns_1;
1,002✔
702
  volume[1] = vol_tally.score_acc[1] * ns_1;
1,002✔
703
  volume[1] = std::sqrt(
2,004✔
704
    (volume[1] - volume[0] * volume[0]) / static_cast<double>(n_samples - 1));
1,002✔
705
  volume[0] *= coeff_norm;
1,002✔
706
  volume[1] *= coeff_norm;
1,002✔
707
  return volume;
1,002✔
708
}
709

710
// TO REVIEWER: THIS IS A FULL EQUIVALENT OF THE SWITCH FROM THE HISTORY LOOP
711
void VolumeCalculation::score_hit(const Particle& p, CalcResults& results) const
11,013,117✔
712
{
713
  const auto n_domains = domain_ids_.size();
11,013,117!
714
  const auto id_mat = p.material();
11,013,117✔
715
  const auto score = 1.; // Floating-point score value
11,013,117✔
716

717
  switch (domain_type_) {
11,013,117!
718
  case TallyDomain::MATERIAL:
4,689,853✔
719
    if (id_mat != MATERIAL_VOID) {
4,689,853✔
720
      for (auto i_domain = 0; i_domain < n_domains; i_domain++) {
7,653,891✔
721
        if (model::materials[id_mat]->id_ == domain_ids_[i_domain]) {
7,644,739✔
722
          this->check_hit(id_mat, score, results.vol_tallies[i_domain]);
4,675,718✔
723
          break;
4,675,718✔
724
        }
725
      }
726
    }
727
    break;
728
  case TallyDomain::CELL:
729
    for (auto level = 0; level < p.n_coord_last(); ++level) {
9,368,858✔
730
      for (auto i_domain = 0; i_domain < n_domains; i_domain++) {
5,190,583✔
731
        if (model::cells[p.coord(level).cell()]->id_ == domain_ids_[i_domain]) {
5,166,152✔
732
          this->check_hit(id_mat, score, results.vol_tallies[i_domain]);
4,659,998✔
733
          break;
4,659,998✔
734
        }
735
      }
736
    }
737
    break;
738
  case TallyDomain::UNIVERSE:
739
    for (auto level = 0; level < p.n_coord_last(); ++level) {
3,277,670✔
740
      for (auto i_domain = 0; i_domain < n_domains; ++i_domain) {
1,638,835!
741
        if (model::universes[model::cells[p.coord(level).cell()]->universe_]
1,638,835!
742
              ->id_ == domain_ids_[i_domain]) {
1,638,835!
743
          this->check_hit(id_mat, score, results.vol_tallies[i_domain]);
1,638,835✔
744
          break;
1,638,835✔
745
        }
746
      }
747
    }
748
  }
749
}
11,013,117✔
750

751
void free_memory_volume()
8,272✔
752
{
753
  openmc::model::volume_calcs.clear();
8,272✔
754
}
8,272✔
755

756
} // namespace openmc
757

758
//==============================================================================
759
// OPENMC_CALCULATE_VOLUMES runs each of the stochastic volume calculations
760
// that the user has specified and writes results to HDF5 files
761
//==============================================================================
762

763
int openmc_calculate_volumes()
126✔
764
{
765
  using namespace openmc;
126✔
766

767
  if (mpi::master) {
126✔
768
    header("STOCHASTIC VOLUME CALCULATION", 3);
118✔
769
  }
770
  Timer time_volume;
126✔
771
  time_volume.start();
126✔
772

773
  for (int i = 0; i < model::volume_calcs.size(); ++i) {
429✔
774
    write_message(4, "Running volume calculation {}", i + 1);
330✔
775

776
    // Run volume calculation
777
    const auto& vol_calc {model::volume_calcs[i]};
330✔
778
    VolumeCalculation::CalcResults results(vol_calc);
330✔
779

780
    try {
330✔
781
      vol_calc.execute(results);
330✔
782
    } catch (const std::exception& e) {
27!
783
      set_errmsg(e.what());
27✔
784
      return OPENMC_E_UNASSIGNED;
27✔
785
    }
27✔
786

787
    if (mpi::master) {
303✔
788

789
      // Output volume calculation results and statistics
790
      vol_calc.show_results(results);
247✔
791

792
      // Write volumes to HDF5 file
793
      std::string filename =
247✔
794
        fmt::format("{}volume_{}.h5", settings::path_output, i + 1);
247✔
795
      vol_calc.to_hdf5(filename, results);
247✔
796
    }
247✔
797
  }
330✔
798

799
  // Show elapsed time
800
  time_volume.stop();
99✔
801
  write_message(6, "Elapsed time: {} sec", time_volume.elapsed());
99✔
802

803
  return 0;
99✔
804
}
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