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

openmc-dev / openmc / 19512343411

19 Nov 2025 06:33PM UTC coverage: 82.131% (+0.06%) from 82.071%
19512343411

Pull #3645

github

web-flow
Merge 763573d12 into f544d02e4
Pull Request #3645: Ray tracing volume calculation and geometry testing

17095 of 23670 branches covered (72.22%)

Branch coverage included in aggregate %.

417 of 428 new or added lines in 4 files covered. (97.43%)

1 existing line in 1 file now uncovered.

55140 of 64281 relevant lines covered (85.78%)

42024814.99 hits per line

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

93.13
/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/geometry.h"
9
#include "openmc/hdf5_interface.h"
10
#include "openmc/material.h"
11
#include "openmc/message_passing.h"
12
#include "openmc/mgxs_interface.h"
13
#include "openmc/nuclide.h"
14
#include "openmc/openmp_interface.h"
15
#include "openmc/output.h"
16
#include "openmc/plot.h"
17
#include "openmc/random_dist.h"
18
#include "openmc/random_lcg.h"
19
#include "openmc/settings.h"
20
#include "openmc/timer.h"
21
#include "openmc/xml_interface.h"
22

23
#include "xtensor/xadapt.hpp"
24
#include "xtensor/xview.hpp"
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
#ifdef OPENMC_MPI
42
static MPI_Datatype mpi_vol_results;  //!< MPI struct for CalcResults
43
static MPI_Datatype mpi_volume_tally; //!< MPI struct for VolTally
44
#endif
45

46
//==============================================================================
47
// VolumeCalculation implementation
48
//==============================================================================
49

50
VolumeCalculation::VolumeCalculation(pugi::xml_node node)
358✔
51
{
52
  // Read domain type (cell, material or universe)
53
  std::string domain_type = get_node_value(node, "domain_type");
358✔
54
  if (domain_type == "cell") {
358✔
55
    domain_type_ = TallyDomain::CELL;
158✔
56
  } else if (domain_type == "material") {
200✔
57
    domain_type_ = TallyDomain::MATERIAL;
148✔
58
  } else if (domain_type == "universe") {
52!
59
    domain_type_ = TallyDomain::UNIVERSE;
52✔
60
  } else {
61
    fatal_error(std::string("Unrecognized domain type for stochastic "
×
62
                            "volume calculation: " +
63
                            domain_type));
64
  }
65

66
  // Read domain IDs, bounding corodinates and number of samples
67
  domain_ids_ = get_node_array<int>(node, "domain_ids");
358✔
68
  lower_left_ = get_node_array<double>(node, "lower_left");
358✔
69
  upper_right_ = get_node_array<double>(node, "upper_right");
358✔
70
  n_samples_ = std::stoull(get_node_value(node, "samples"));
358✔
71

72
  // Determine volume of bounding box
73
  const Position d {upper_right_ - lower_left_};
358✔
74
  volume_sample_ = d.x * d.y * d.z;
358✔
75

76
  if (check_for_node(node, "threshold")) {
358✔
77
    pugi::xml_node threshold_node = node.child("threshold");
128✔
78

79
    threshold_ = std::stod(get_node_value(threshold_node, "threshold"));
128✔
80
    if (threshold_ <= 0.0) {
128!
81
      fatal_error(fmt::format("Invalid error threshold {} provided for a "
×
82
                              "volume calculation.",
83
        threshold_));
×
84
    }
85

86
    std::string tmp = get_node_value(threshold_node, "type");
128✔
87
    if (tmp == "variance") {
128✔
88
      trigger_type_ = TriggerMetric::variance;
32✔
89
      // t' = Var{\xi}/(N-1), in sq. volume fraction units
90
      threshold_cnd_ = threshold_ / std::pow(volume_sample_, 2);
32✔
91
      ;
92
    } else if (tmp == "std_dev") {
96✔
93
      trigger_type_ = TriggerMetric::standard_deviation;
32✔
94
      // t' = Var{\xi}/(N-1), in sq. volume fraction units
95
      threshold_cnd_ = std::pow(threshold_ / volume_sample_, 2);
32✔
96
    } else if (tmp == "rel_err") {
64!
97
      trigger_type_ = TriggerMetric::relative_error;
64✔
98
      // t' = (Var{\xi}/(N-1)) / (E{\xi})^2 < t^2, in relative units
99
      threshold_cnd_ = threshold_ * threshold_;
64✔
100
    } else {
101
      fatal_error(fmt::format(
×
102
        "Invalid volume calculation trigger type '{}' provided.", tmp));
103
    }
104

105
    if (check_for_node(threshold_node, "max_iterations")) {
128✔
106
      max_iterations_ =
32✔
107
        std::stoi(get_node_value(threshold_node, "max_iterations"));
32✔
108
      if (max_iterations_ <= 0) {
32!
NEW
109
        fatal_error(fmt::format(
×
NEW
110
          "Invalid error max_iterations {} provided.", max_iterations_));
×
111
      }
112
    }
113
  }
128✔
114

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

123
  // Type of volume estimator
124
  std::string tmp = get_node_value(node, "estimator_type");
358✔
125
  if (tmp == "hit") {
358✔
126
    mode_ = EstMode::REJECTION;
262✔
127
  } else if (tmp == "ray") {
96!
128
    mode_ = EstMode::RAYTRACE;
96✔
129
  } else {
NEW
130
    fatal_error(
×
NEW
131
      fmt::format("Invalid volume calculation mode '{}' provided.", tmp));
×
132
  }
133
}
358✔
134

135
void VolumeCalculation::execute(CalcResults& master_results) const
344✔
136
{
137
#ifdef OPENMC_MPI
138
  // MPI types are commited in the beginning of calculation and freed on the
139
  // return, remaining a MPI type after the return produces a MPICH warning for
140
  // memory leak
141
  this->initialize_MPI_struct();
197✔
142
#endif
143

144
  // Check to make sure domain IDs are valid
145
  for (auto uid : domain_ids_) {
982✔
146
    switch (domain_type_) {
665!
147
    case TallyDomain::CELL:
365✔
148
      if (model::cell_map.find(uid) == model::cell_map.end()) {
365✔
149
        throw std::runtime_error {fmt::format(
18!
150
          "Cell {} in volume calculation does not exist in geometry.", uid)};
18✔
151
      }
152
      break;
356✔
153
    case TallyDomain::MATERIAL:
248✔
154
      if (model::material_map.find(uid) == model::material_map.end()) {
248✔
155
        throw std::runtime_error {fmt::format(
18!
156
          "Material {} in volume calculation does not exist in geometry.",
157
          uid)};
18✔
158
      }
159
      break;
239✔
160
    case TallyDomain::UNIVERSE:
52✔
161
      if (model::universe_map.find(uid) == model::universe_map.end()) {
52✔
162
        throw std::runtime_error {fmt::format(
18!
163
          "Universe {} in volume calculation does not exist in geometry.",
164
          uid)};
18✔
165
      }
166
    }
167
  }
168

169
  // Shared data that is collected from all threads
170
  const int n_domains = domain_ids_.size();
317✔
171

172
  // Divide work over MPI processes
173
  uint64_t min_samples = n_samples_ / mpi::n_procs;
317✔
174
  uint64_t remainder = n_samples_ % mpi::n_procs;
317✔
175
  uint64_t i_start, i_end;
176
  if (mpi::rank < remainder) {
317!
177
    i_start = (min_samples + 1) * mpi::rank;
×
178
    i_end = i_start + min_samples + 1;
×
179
  } else {
180
    i_start =
317✔
181
      (min_samples + 1) * remainder + (mpi::rank - remainder) * min_samples;
317✔
182
    i_end = i_start + min_samples;
317✔
183
  }
184

185
  while (true) {
186

187
#pragma omp parallel
5,867✔
188
    {
189
      // Temporary variables that are private to each thread
190
      CalcResults results(*this);
4,562✔
191
      results.sampling_time.start();
4,562✔
192

193
      // Sample locations and count scores
194
#pragma omp for
195
      for (size_t i = i_start; i < i_end; i++) {
3,359,612✔
196
        uint64_t id = master_results.iterations * n_samples_ + i;
3,355,050✔
197
        uint64_t seed = init_seed(id, STREAM_VOLUME);
3,355,050✔
198

199
        Position r {uniform_distribution(lower_left_.x, upper_right_.x, &seed),
3,355,050✔
200
          uniform_distribution(lower_left_.y, upper_right_.y, &seed),
3,355,050✔
201
          uniform_distribution(lower_left_.z, upper_right_.z, &seed)};
3,355,050✔
202

203
        results.n_samples++;
3,355,050✔
204

205
        switch (mode_) {
3,355,050!
206
        case EstMode::REJECTION: {
3,044,050✔
207
          constexpr double flt3 = 1. / std::sqrt(3.);
3,044,050✔
208
          Direction u {flt3, flt3, flt3};
3,044,050✔
209

210
          // Create zero-length ray, it is a bit excessive due to undemanded
211
          // internal ray variables initialization
212
          VolEstRay ray_hit(r, u, 0., 1., *this, results);
3,044,050✔
213
          ray_hit.score_hit();
3,044,050✔
214
        } break;
3,044,050✔
215
        case EstMode::RAYTRACE: {
311,000✔
216
          Direction u = isotropic_direction(&seed);
311,000✔
217

218
          // Compute lengths of the bounding box's chord segments
219
          const auto chord_len = get_box_chord(r, u);
311,000✔
220
          const double ch_len_tot = -chord_len.first + chord_len.second;
311,000✔
221

222
          r += chord_len.first * u;
311,000✔
223
          VolEstRay ray(r, u, ch_len_tot, 1. / ch_len_tot, *this, results);
311,000✔
224
          ray.trace(); // Trace from a boundary to another
311,000✔
225
        }
311,000✔
226
        }
227

228
        // This passing by all tallies after each sample can be
229
        // inefficient for case of large number of domains and small
230
        // size of batch, but the batch size is currently assigned to 1
231
        // for keep the input format unchanged
232
        const double batch_size_1 = 1. / static_cast<double>(1);
3,355,050✔
233
        for (auto& vt : results.vol_tallies) {
10,335,200✔
234
          for (auto& vol_tally : vt) {
21,880,615✔
235
            vol_tally.finalize_batch(batch_size_1);
14,900,465✔
236
          }
237
        }
238
      } // sample/batch loop
239

240
      results.sampling_time.stop();
4,562✔
241
      results.cost = results.sampling_time.elapsed();
4,562✔
242

243
      // At this point, each thread has its own volume tallies lists and we
244
      // now need to reduce them. OpenMP is not nearly smart enough to do this
245
      // on its own, so we have to manually reduce them
246
      reduce_results(results, master_results);
4,562✔
247
    } // omp parallel
4,562✔
248

249
    // bump iteration counter
250
    master_results.iterations++;
10,429✔
251

252
#ifdef OPENMC_MPI
253
    master_results.collect_MPI(); // collect results to master process
6,505✔
254
#endif
255
    // Process volume estimation results in master process for trigger state
256
    // determination
257
    bool stop_calc =
10,429✔
258
      mpi::master && (trigger_type_ == TriggerMetric::not_active ||
17,469!
259
                       master_results.iterations == max_iterations_);
7,040✔
260

261
    if (!stop_calc) {
10,429✔
262
      // Compute current trigger state among totals (0th elements) only
263
      for (const auto& vt : master_results.vol_tallies) {
10,484✔
264
        stop_calc = vt[0].trigger_state(
10,388✔
265
          trigger_type_, threshold_cnd_, master_results.n_samples);
10,388✔
266
        if (!stop_calc)
10,388✔
267
          break;
10,152✔
268
      }
269
    }
270

271
#ifdef OPENMC_MPI
272
    // Send the state of calculation continuation just obtained in master
273
    // process to all processes
274
    MPI_Bcast(&stop_calc, 1, MPI_CXX_BOOL, 0, mpi::intracomm);
6,505✔
275
#endif
276

277
    if (!stop_calc)
10,429✔
278
      continue; // while loop
10,112✔
279
    // No trigger is applied or the trigger condition is satisfied, we're
280
    // done
281

282
    if (mpi::master) {
317✔
283
      // Normalize all results on bounding primitive volume and compute
284
      // stddev
285
      for (auto& vt : master_results.vol_tallies) {
725✔
286
        for (auto& vol_tally : vt) {
1,488✔
287
          vol_tally.score_acc = get_tally_results(
1,010✔
288
            master_results.n_samples, volume_sample_, vol_tally);
1,010✔
289
        }
290
      }
291

292
      // Compute nuclides
293
      for (int i_domain = 0; i_domain < n_domains; ++i_domain) {
725✔
294
        // Create 2D array to store atoms/uncertainty for each nuclide. Later
295
        // this is compressed into vectors storing only those nuclides that are
296
        // non-zero
297
        auto n_nuc =
298
          settings::run_CE ? data::nuclides.size() : data::mg.nuclides_.size();
478✔
299
        xt::xtensor<double, 2> atoms({n_nuc, 2}, 0.0);
478✔
300

301
        for (int j = 0; j < master_results.vol_tallies[i_domain].size(); ++j) {
1,488✔
302
          const int i_material = master_results.vol_tallies[i_domain][j].index;
1,010✔
303
          if (i_material == MATERIAL_VOID || i_material == _INDEX_TOTAL)
1,010✔
304
            continue;
489✔
305

306
          const auto& mat = model::materials[i_material];
521✔
307
          for (int k = 0; k < mat->nuclide_.size(); ++k) {
2,040✔
308
            auto& volume = master_results.vol_tallies[i_domain][j].score_acc;
1,519✔
309
            // Collect calculated nuclide amounts N [atoms] and stddev as
310
            // N = V [cm^3] * \ro [atoms/b-cm] * 1.e24 [b-cm/cm^3]
311
            atoms(mat->nuclide_[k], 0) +=
1,519✔
312
              volume[0] * mat->atom_density(k) * 1.0e24;
1,519✔
313
            atoms(mat->nuclide_[k], 1) +=
1,519✔
314
              volume[1] * mat->atom_density(k) * 1.0e24;
1,519✔
315
          }
316
        }
317

318
        // Get reference to result for this domain
319
        auto& result {master_results.nuc_results[i_domain]};
478✔
320
        // Convert full arrays to vectors
321
        for (int j = 0; j < n_nuc; ++j) {
3,440✔
322
          if (atoms(j, 0) > 0.0) {
2,962✔
323
            result.nuclides.push_back(j);
1,399✔
324
            result.atoms.push_back(atoms(j, 0));
1,399✔
325
            result.uncertainty.push_back(atoms(j, 1));
1,399✔
326
          }
327
        }
328
      } // end domains loop
478✔
329
    }
330

331
#ifdef OPENMC_MPI
332
    // MPI types are commited in the beginning of calculation and freed on
333
    // return, lefting MPI types after return produce MPICH warnings about
334
    // memory leak
335
    this->delete_MPI_struct();
185✔
336
#endif
337

338
    return;
317✔
339

340
  } // end while
10,112✔
341
}
342

343
void VolumeCalculation::show_vol_stat(
873✔
344
  const std::string label, const std::string units, const double value) const
345
{
346
  fmt::print("{0:<20} = {2:10.4e} {1:<}\n", label, units, value);
873✔
347
}
873✔
348

349
void VolumeCalculation::show_volume(const std::string domain_type,
478✔
350
  const int domain_id, const std::string region_name, const double mean,
351
  const double stddev) const
352
{
353
  fmt::print(" {0:>9}{1:>6}: {2:10.4e} +/- {3:10.4e} cm^3", domain_type,
394✔
354
    domain_id, mean, stddev);
355
  if (!region_name.empty()) {
478✔
356
    fmt::print("   //{:<}", region_name);
80✔
357
  }
358
  fmt::print("\n");
478✔
359
}
478✔
360

361
void VolumeCalculation::show_results(const CalcResults& results) const
247✔
362
{
363
  // Show tracing statistics
364
  write_message(5, " ");
247✔
365
  show_vol_stat(
247✔
366
    "Total sample size", "", static_cast<double>(results.n_samples));
247✔
367
  show_vol_stat("Running cost", "thread-sec", results.cost);
247✔
368

369
  switch (mode_) {
247!
370
  case EstMode::REJECTION:
181✔
371
    show_vol_stat("Cost of hitting", "thread-sec/hit",
181✔
372
      static_cast<double>(results.cost) /
181✔
373
        static_cast<double>(results.n_samples));
181✔
374
    break;
181✔
375
  case EstMode::RAYTRACE:
66✔
376
    show_vol_stat("Rays traced", "", results.n_rays);
66✔
377
    show_vol_stat("Average ray length", "segments/ray",
66✔
378
      static_cast<double>(results.n_segs) /
66✔
379
        static_cast<double>(results.n_rays));
66✔
380
    show_vol_stat("Cost of tracing", "thread-sec/segment",
66✔
381
      static_cast<double>(results.cost) / static_cast<double>(results.n_segs));
66✔
382
    if (results.n_errors != 0)
66!
NEW
383
      show_vol_stat("Error rate", "errors/ray",
×
NEW
384
        static_cast<double>(results.n_errors) /
×
NEW
385
          static_cast<double>(results.n_rays));
×
386
  }
387
  write_message(5, " ");
247✔
388

389
  std::string domain_type;
247✔
390
  if (domain_type_ == TallyDomain::CELL) {
247✔
391
    domain_type = "Cell";
112✔
392
  } else if (domain_type_ == TallyDomain::MATERIAL) {
135✔
393
    domain_type = "Material";
102✔
394
  } else {
395
    domain_type = "Universe";
33✔
396
  }
397

398
  // Display domain volumes
399
  for (int j = 0; j < domain_ids_.size(); j++) {
725✔
400
    std::string region_name {""};
478✔
401
    if (domain_type_ == TallyDomain::CELL) {
478✔
402
      int cell_idx = model::cell_map[domain_ids_[j]];
266✔
403
      region_name = model::cells[cell_idx]->name();
266✔
404
    } else if (domain_type_ == TallyDomain::MATERIAL) {
212✔
405
      int mat_idx = model::material_map[domain_ids_[j]];
179✔
406
      region_name = model::materials[mat_idx]->name();
179✔
407
    }
408
    if (region_name.size())
478✔
409
      region_name.insert(0, " "); // prepend space for formatting
80✔
410

411
    show_volume(domain_type, domain_ids_[j], region_name,
478✔
412
      results.vol_tallies[j][0].score_acc[0],
478✔
413
      results.vol_tallies[j][0].score_acc[1]);
478✔
414
  }
478✔
415
  write_message(4, " "); // Blank line afer results printed
247✔
416
}
247✔
417

418
void VolumeCalculation::to_hdf5(
247✔
419
  const std::string& filename, const CalcResults& results) const
420
{
421
  // Create HDF5 file
422
  hid_t file_id = file_open(filename, 'w');
247✔
423

424
  // Write header info
425
  write_attribute(file_id, "filetype", "volume");
247✔
426
  write_attribute(file_id, "version", VERSION_VOLUME);
247✔
427
  write_attribute(file_id, "openmc_version", VERSION);
247✔
428
#ifdef GIT_SHA1
429
  write_attribute(file_id, "git_sha1", GIT_SHA1);
430
#endif
431

432
  // Write current date and time
433
  write_attribute(file_id, "date_and_time", time_stamp());
247✔
434

435
  // Write basic metadata
436
  write_attribute(file_id, "samples", n_samples_);
247✔
437
  write_attribute(file_id, "lower_left", lower_left_);
247✔
438
  write_attribute(file_id, "upper_right", upper_right_);
247✔
439
  // Write estimator info
440
  std::string estimator_str;
247✔
441
  switch (mode_) {
247!
442
  case EstMode::REJECTION:
181✔
443
    estimator_str = "hit";
181✔
444
    break;
181✔
445
  case EstMode::RAYTRACE:
66✔
446
    estimator_str = "ray";
66✔
447
    break;
66✔
448
  }
449
  write_attribute(file_id, "estimator_type", estimator_str);
247✔
450
  // Write trigger info
451
  if (trigger_type_ != TriggerMetric::not_active) {
247✔
452
    write_attribute(file_id, "iterations", results.iterations);
88✔
453
    write_attribute(file_id, "threshold", threshold_);
88✔
454
    std::string trigger_str;
88✔
455
    switch (trigger_type_) {
88!
456
    case TriggerMetric::variance:
22✔
457
      trigger_str = "variance";
22✔
458
      break;
22✔
459
    case TriggerMetric::standard_deviation:
22✔
460
      trigger_str = "std_dev";
22✔
461
      break;
22✔
462
    case TriggerMetric::relative_error:
44✔
463
      trigger_str = "rel_err";
44✔
464
      break;
44✔
465
    default:
×
466
      break;
×
467
    }
468
    write_attribute(file_id, "trigger_type", trigger_str);
88✔
469
    // check max_iterations on default value
470
    if (max_iterations_ !=
88✔
471
        std::numeric_limits<decltype(max_iterations_)>::max())
88✔
472
      write_attribute(file_id, "max_iterations", max_iterations_);
22✔
473
  } else {
88✔
474
    write_attribute(file_id, "iterations", 1);
159✔
475
  }
476

477
  if (domain_type_ == TallyDomain::CELL) {
247✔
478
    write_attribute(file_id, "domain_type", "cell");
112✔
479
  } else if (domain_type_ == TallyDomain::MATERIAL) {
135✔
480
    write_attribute(file_id, "domain_type", "material");
102✔
481
  } else if (domain_type_ == TallyDomain::UNIVERSE) {
33!
482
    write_attribute(file_id, "domain_type", "universe");
33✔
483
  }
484

485
  for (int i = 0; i < domain_ids_.size(); ++i) {
725✔
486
    hid_t group_id =
487
      create_group(file_id, fmt::format("domain_{}", domain_ids_[i]));
956✔
488

489
    // Write volume for domain
490
    const auto& result {results.nuc_results[i]};
478✔
491
    write_dataset(group_id, "volume", results.vol_tallies[i][0].score_acc);
478✔
492

493
    // Create array of nuclide names from the vector
494
    auto n_nuc = result.nuclides.size();
478✔
495

496
    vector<std::string> nucnames;
478✔
497
    for (int i_nuc : result.nuclides) {
1,877✔
498
      nucnames.push_back(settings::run_CE ? data::nuclides[i_nuc]->name_
1,883✔
499
                                          : data::mg.nuclides_[i_nuc].name);
484✔
500
    }
501

502
    // Create array of total # of atoms with uncertainty for each nuclide
503
    xt::xtensor<double, 2> atom_data({n_nuc, 2});
478✔
504
    xt::view(atom_data, xt::all(), 0) = xt::adapt(result.atoms);
478✔
505
    xt::view(atom_data, xt::all(), 1) = xt::adapt(result.uncertainty);
478✔
506

507
    // Write results
508
    write_dataset(group_id, "nuclides", nucnames);
478✔
509
    write_dataset(group_id, "atoms", atom_data);
478✔
510

511
    close_group(group_id);
478✔
512
  }
478✔
513

514
  file_close(file_id);
247✔
515
}
247✔
516

517
void VolumeCalculation::check_hit(const int32_t i_material,
10,687,121✔
518
  const double contrib, vector<VolTally>& vol_tallies) const
519
{
520
  // Contribute to entire domain result tally
521
  vol_tallies[0].score += contrib;
10,687,121✔
522

523
  // Check if this material was previously hit and if so, contribute score
524
  for (int j = 1; j < vol_tallies.size(); j++) {
11,335,821✔
525
    if (vol_tallies[j].index == i_material) {
11,288,587✔
526
      vol_tallies[j].score += contrib;
10,639,887✔
527
      return;
10,639,887✔
528
    }
529
  }
530

531
  // The material was not previously hit, append an entry to the material
532
  // indices and hits lists
533
  vol_tallies.push_back(VolTally(i_material, contrib));
47,234✔
534
}
535

536
void VolumeCalculation::reduce_results(
16,296✔
537
  const CalcResults& local_results, CalcResults& results) const
538
{
539
  auto n_threads = num_threads();
16,296✔
540

541
  // Collect scalar variables
542
#pragma omp for ordered schedule(static, 1)
11,734!
543
  for (int i = 0; i < n_threads; ++i) {
9,124✔
544
#pragma omp ordered
545
    {
546
      results.n_samples += local_results.n_samples;
16,296✔
547
      results.n_rays += local_results.n_rays;
16,296✔
548
      results.n_segs += local_results.n_segs;
16,296✔
549
      results.n_errors += local_results.n_errors;
16,296✔
550
      results.cost += local_results.cost;
16,296!
551
    }
552
  }
553

554
  // Collect vectored domain-wise results
555
  for (int i_domain = 0; i_domain < domain_ids_.size(); ++i_domain) {
64,048✔
556
    const vector<VolTally>& local_vol_tall =
557
      local_results.vol_tallies[i_domain];
47,752✔
558
    vector<VolTally>& vol_tall = results.vol_tallies[i_domain];
47,752✔
559

560
#pragma omp for ordered schedule(static, 1)
34,388!
561
    for (int i = 0; i < n_threads; ++i) {
26,728✔
562
#pragma omp ordered
563
      for (int j = 0; j < local_vol_tall.size(); ++j) {
142,738✔
564
        // Check if this material has been added to the master list and if
565
        // so, accumulate scores
566
        const auto ind {local_vol_tall[j].index};
94,986✔
567
        const auto it = std::find_if(vol_tall.begin(), vol_tall.end(),
94,986✔
568
          [ind](const VolTally& vt) { return vt.index == ind; });
3,340,387✔
569
        if (it == vol_tall.end()) {
94,986✔
570
          vol_tall.push_back(local_vol_tall[j]);
13,172✔
571
        } else {
572
          vol_tall[it - vol_tall.begin()].append_tally(local_vol_tall[j]);
81,814✔
573
        }
574
      }
575
    }
576
  }
577
}
16,296✔
578

579
#ifdef OPENMC_MPI
580
void VolumeCalculation::initialize_MPI_struct() const
197✔
581
{
582
  // This code is a slightly modified replica of initialize_mpi() from
583
  // initialize.cpp. It works under GCC in the Release configuration, but not
584
  // sure that the adress offsets of structure's memebrs should be necessary the
585
  // same everywhere as using an optimizing compiler.
586
  CalcResults cr(*this);
197✔
587
  MPI_Aint cr_disp[5], cr_d;
588
  MPI_Get_address(&cr, &cr_d);
197✔
589
  MPI_Get_address(&cr.n_errors, &cr_disp[0]);
197✔
590
  MPI_Get_address(&cr.n_rays, &cr_disp[1]);
197✔
591
  MPI_Get_address(&cr.n_segs, &cr_disp[2]);
197✔
592
  MPI_Get_address(&cr.n_samples, &cr_disp[3]);
197✔
593
  MPI_Get_address(&cr.cost, &cr_disp[4]);
197✔
594
  for (int i = 0; i < 5; i++) {
1,182✔
595
    cr_disp[i] -= cr_d;
985✔
596
  }
597

598
  int cr_blocks[] {1, 1, 1, 1, 1};
197✔
599
  MPI_Datatype cr_types[] {
197✔
600
    MPI_UINT64_T, MPI_UINT64_T, MPI_UINT64_T, MPI_UINT64_T, MPI_DOUBLE};
601
  MPI_Type_create_struct(5, cr_blocks, cr_disp, cr_types, &mpi_vol_results);
197✔
602
  MPI_Type_commit(&mpi_vol_results);
197✔
603

604
  VolTally vt;
197✔
605
  MPI_Aint vt_disp[3], vt_d;
606
  MPI_Get_address(&vt, &vt_d);
197✔
607
  MPI_Get_address(&vt.score, &vt_disp[0]);
197✔
608
  MPI_Get_address(&vt.score_acc, &vt_disp[1]);
197✔
609
  MPI_Get_address(&vt.index, &vt_disp[2]);
197✔
610
  for (int i = 0; i < 3; i++) {
788✔
611
    vt_disp[i] -= vt_d;
591✔
612
  }
613

614
  int vt_blocks[] {1, 2, 1};
197✔
615
  MPI_Datatype vt_types[] {MPI_DOUBLE, MPI_DOUBLE, MPI_DOUBLE, MPI_INT};
197✔
616
  MPI_Type_create_struct(3, vt_blocks, vt_disp, vt_types, &mpi_volume_tally);
197✔
617
  MPI_Type_commit(&mpi_volume_tally);
197✔
618
}
197✔
619

620
void VolumeCalculation::delete_MPI_struct() const
185✔
621
{
622
  MPI_Type_free(&mpi_vol_results);
185✔
623
  MPI_Type_free(&mpi_volume_tally);
185✔
624
}
185✔
625

626
void VolumeCalculation::CalcResults::collect_MPI()
6,505✔
627
{
628
  vector<int> domain_sizes(vol_tallies.size());
6,505✔
629

630
  if (!mpi::master) {
6,505✔
631
    // n_domain + 2 MPI messages will be send in total to node mpi::master
632

633
    // To determination of an unique tag for each MPI message (as below)
634
    int mpi_offset = mpi::rank * (vol_tallies.size() + 2) + 2;
3,230✔
635

636
    // Pass root data of the struct
637
    MPI_Send(
3,230✔
638
      (void*)this, 1, mpi_vol_results, 0, mpi_offset - 2, mpi::intracomm);
639

640
    // Pass sizes of domain-wise data
641
    for (int i_domain = 0; i_domain < vol_tallies.size(); i_domain++) {
12,740✔
642
      domain_sizes[i_domain] = vol_tallies[i_domain].size();
9,510✔
643
    }
644

645
    MPI_Send(domain_sizes.data(), domain_sizes.size(), MPI_INT, 0,
3,230✔
646
      mpi_offset - 1, mpi::intracomm);
647

648
    // Pass domain-wise data of struct
649
    for (int i_domain = 0; i_domain < vol_tallies.size(); i_domain++) {
12,740✔
650
      MPI_Send(vol_tallies[i_domain].data(), domain_sizes[i_domain],
9,510✔
651
        mpi_volume_tally, 0, mpi_offset + i_domain, mpi::intracomm);
652
    }
653

654
    this->reset(); // Delete passed to main process data
3,230✔
655

656
  } else {
657

658
    // n_domain + 2 MPI messages will be recieved in total on node mpi::master
659

660
    for (int i_proc = 1; i_proc < mpi::n_procs; i_proc++) {
6,505✔
661

662
      CalcResults res_buff(*this); // temporary storage for recived data
3,230✔
663

664
      // To determination of an unique tag for each MPI message (as above)
665
      int mpi_offset = i_proc * (vol_tallies.size() + 2) + 2;
3,230✔
666

667
      // Pass root data of struct
668
      MPI_Recv(&res_buff, 1, mpi_vol_results, i_proc, mpi_offset - 2,
3,230✔
669
        mpi::intracomm, MPI_STATUS_IGNORE);
670

671
      // Pass sizes of domain-wise data
672
      MPI_Recv(domain_sizes.data(), domain_sizes.size(), MPI_INT, i_proc,
3,230✔
673
        mpi_offset - 1, mpi::intracomm, MPI_STATUS_IGNORE);
674

675
      // Pass domain-wise data of struct
676
      for (int i_domain = 0; i_domain < vol_tallies.size(); i_domain++) {
12,740✔
677
        res_buff.vol_tallies[i_domain].resize(domain_sizes[i_domain]);
9,510✔
678
        MPI_Recv(res_buff.vol_tallies[i_domain].data(), domain_sizes[i_domain],
9,510✔
679
          mpi_volume_tally, i_proc, mpi_offset + i_domain, mpi::intracomm,
680
          MPI_STATUS_IGNORE);
681
      }
682

683
      this->append(res_buff);
3,230✔
684
    }
3,230✔
685
  }
686
}
6,505✔
687
#endif
688

689
VolumeCalculation::CalcResults::CalcResults(const VolumeCalculation& vol_calc)
16,837✔
690
{
691
  n_samples = 0;
16,837✔
692
  n_rays = 0;
16,837✔
693
  n_segs = 0;
16,837✔
694
  n_errors = 0;
16,837✔
695
  iterations = 0;
16,837✔
696
  cost = 0.;
16,837✔
697
  for (int i = 0; i < vol_calc.domain_ids_.size(); i++) {
65,652✔
698
    vector<VolTally> vt_vect; // Tally group for a domain
48,815✔
699
    vt_vect.push_back(
48,815✔
700
      VolTally(_INDEX_TOTAL)); // Zero-element tally for entire domain totals
48,815✔
701
    vol_tallies.push_back(vt_vect);
48,815✔
702
    nuc_results.push_back(NuclResult());
48,815✔
703
  }
48,815✔
704
}
16,837✔
705

706
void VolumeCalculation::CalcResults::reset()
3,230✔
707
{
708
  n_samples = 0;
3,230✔
709
  n_rays = 0;
3,230✔
710
  n_segs = 0;
3,230✔
711
  n_errors = 0;
3,230✔
712
  cost = 0.;
3,230✔
713
  for (auto& vt : vol_tallies) {
12,740✔
714
    std::fill(vt.begin(), vt.end(), VolTally());
9,510!
715
  }
716
}
3,230✔
717

718
void VolumeCalculation::CalcResults::append(const CalcResults& other)
3,230✔
719
{
720
  n_samples += other.n_samples;
3,230✔
721
  n_rays += other.n_rays;
3,230✔
722
  n_segs += other.n_segs;
3,230✔
723
  n_errors += other.n_errors;
3,230✔
724
  cost += other.cost;
3,230✔
725

726
  // The domain-wise vectors this.vol_tallies and other.vol_tallies are
727
  // conformed each to other by definition
728
  for (auto id = 0; id < vol_tallies.size(); id++) {
12,740✔
729
    // Merging current domain vector from other.vol_tallies into this via
730
    // pair-wise comparisons
731
    for (const auto& vt_other : other.vol_tallies[id]) {
1,366,310✔
732
      bool already_appended = false;
1,356,800✔
733
      for (auto& vt : vol_tallies[id]) {
3,373,240✔
734
        if (vt.index == vt_other.index) {
3,373,200✔
735
          vt.append_tally(vt_other);
1,356,760✔
736
          already_appended = true;
1,356,760✔
737
          break;
1,356,760✔
738
        }
739
      }
740
      if (!already_appended)
1,356,800✔
741
        vol_tallies[id].push_back(vt_other);
40!
742
    }
743
  }
744
}
3,230✔
745

746
inline VolumeCalculation::VolTally::VolTally(const int i_material,
1,440,406✔
747
  const double contrib, const double score_acc_, const double score2_acc_)
1,440,406✔
748
{
749
  score = contrib;
1,440,406✔
750
  score_acc[0] = score_acc_;
1,440,406✔
751
  score_acc[1] = score2_acc_;
1,440,406✔
752
  index = i_material;
1,440,406✔
753
}
1,440,406✔
754

755
inline void VolumeCalculation::VolTally::finalize_batch(
56,700,445✔
756
  const double batch_size_1)
757
{
758
  if (score != 0.) {
56,700,445✔
759
    score *= batch_size_1;
21,374,198✔
760
    score_acc[0] += score;
21,374,198✔
761
    score_acc[1] += score * score;
21,374,198✔
762
    score = 0.;
21,374,198✔
763
  }
764
}
56,700,445✔
765

766
inline void VolumeCalculation::VolTally::assign_tally(const VolTally& vol_tally)
767
{
768
  score = vol_tally.score;
769
  score_acc = vol_tally.score_acc;
770
  index = vol_tally.index;
771
}
772

773
inline void VolumeCalculation::VolTally::append_tally(const VolTally& vol_tally)
1,438,574✔
774
{
775
  score += vol_tally.score;
1,438,574✔
776
  score_acc[0] += vol_tally.score_acc[0];
1,438,574✔
777
  score_acc[1] += vol_tally.score_acc[1];
1,438,574✔
778
}
1,438,574✔
779

780
inline bool VolumeCalculation::VolTally::trigger_state(
10,388✔
781
  const TriggerMetric trigger_type, const double threshold,
782
  const size_t& n_samples) const
783
{
784
  // For sample contribution to volume fraction limited by 1, maximal allowed
785
  // n_samples value is around 1.e102 but this is still much larger than the
786
  // size_t limit equal to ~1.8e19
787
  const double ns1 = static_cast<double>(n_samples - 1);
10,388✔
788
  const double ns = static_cast<double>(n_samples);
10,388✔
789
  const double mean_xi_sq = score_acc[0] * score_acc[0];
10,388✔
790

791
  // score_acc[0] = N * E[\xi], score_acc[1] = N * E[\xi^2]
792
  switch (trigger_type) {
10,388✔
793
  case TriggerMetric::variance:
9,848✔
794
  case TriggerMetric::standard_deviation:
795
    // N^2 * Var[\xi] < t' * (N-1) * N^2
796
    return score_acc[1] * ns - mean_xi_sq < threshold * ns1 * ns * ns;
9,848✔
797
  case TriggerMetric::relative_error:
480✔
798
    // N^2 * Var[\xi] < t' * (N-1) * (N * E[\xi])^2
799
    return score_acc[1] * ns - mean_xi_sq < threshold * ns1 * mean_xi_sq;
480✔
800
  default:
60✔
801
    return true;
60✔
802
  }
803
}
804

805
array<double, 2> VolumeCalculation::get_tally_results(const size_t& n_samples,
1,010✔
806
  const double coeff_norm, const VolTally& vol_tally) const
807
{
808
  array<double, 2> volume;
809
  const double ns_1 = 1. / static_cast<double>(n_samples);
1,010✔
810
  volume[0] = vol_tally.score_acc[0] * ns_1;
1,010✔
811
  volume[1] = vol_tally.score_acc[1] * ns_1;
1,010✔
812
  volume[1] = std::sqrt(
2,020✔
813
    (volume[1] - volume[0] * volume[0]) / static_cast<double>(n_samples - 1));
1,010✔
814
  volume[0] *= coeff_norm;
1,010✔
815
  volume[1] *= coeff_norm;
1,010✔
816
  return volume;
1,010✔
817
}
818

819
std::pair<double, double> VolumeCalculation::get_box_chord(
684,200✔
820
  const Position& r, const Direction& u) const
821
{
822
  // Compute distanses to each box plane orthogonal to an axis
823
  Direction u_1 = {1., 1., 1.};
684,200✔
824
  u_1 = u_1 / u;
684,200✔
825
  Position xi = (lower_left_ - r) * u_1;
684,200✔
826
  const array<double, 3> dist1 = {xi.x, xi.y, xi.z};
684,200✔
827
  xi = (upper_right_ - r) * u_1;
684,200✔
828
  const array<double, 3> dist2 = {xi.x, xi.y, xi.z};
684,200✔
829

830
  // Find minimal forward (positive values) and backward (negative values)
831
  // distances among the computed ones (probably there is some STL
832
  // alternative)
833
  std::pair<double, double> chord_lengths {std::minmax(dist1[0], dist2[0])};
684,200✔
834
  for (int i = 1; i < 3; i++) {
2,052,600✔
835
    const std::pair<double, double> dist_mm = std::minmax(dist1[i], dist2[i]);
1,368,400✔
836
    chord_lengths.first = std::max(chord_lengths.first, dist_mm.first);
1,368,400✔
837
    chord_lengths.second = std::min(chord_lengths.second, dist_mm.second);
1,368,400✔
838
  }
839
  return chord_lengths;
684,200✔
840
}
841

842
void VolEstRay::on_intersection()
1,371,656✔
843
{
844
  if (traversal_distance_ == 0.) {
1,371,656✔
845
    return; // No crossing model
620,972✔
846
  }
847

848
  results_.n_segs++;
750,684✔
849
  if (traversal_distance_last_ == 0.) {
750,684✔
850
    results_.n_rays++; // First segment of new ray
620,972✔
851
  }
852

853
  // At this point, current GeometryState parameters represent the cell behind
854
  // crossed surface but the segment length is known for the previous
855
  // cell only, therefore we use below the last cell keept in GeometryState
856
  const auto score = (std::min(traversal_distance_, traversal_distance_max_) -
750,684✔
857
                       traversal_distance_last_) *
750,684✔
858
                     coeff_mult_;
750,684✔
859

860
  //----------------------------------------------------------------------------
861
  // Tracing error diagnostic
862
  // TODO: those can be implemented here clever diagnostic and guidance for
863
  // user to fix input mistakes
864
  //----------------------------------------------------------------------------
865

866
  if (traversal_distance_ >= traversal_distance_max_) {
750,684!
NEW
867
    stop();
×
868
  } else {
869
    traversal_distance_last_ = traversal_distance_;
750,684✔
870
  }
871

872
  // In a case of single-segment ray (leakage after 1st surface crossing),
873
  // current segment material ID seems to be contained in material() but in
874
  // other cases it is in material_last(). Due to this unclear behavior, it is
875
  // used below more fundamental way for material definition -- via the stable
876
  // last cell record.
877
  vol_scoring(VolumeCalculation::EstMode::RAYTRACE, score,
1,501,368✔
878
    (current_cell(VolumeCalculation::EstMode::RAYTRACE, n_coord_last() - 1)
750,684✔
879
        ->material(n_coord_last() - 1)));
750,684✔
880
}
881

882
void VolEstRay::score_hit()
11,696,910✔
883
{
884
  if (exhaustive_find_cell(*this))
11,696,910✔
885
    vol_scoring(VolumeCalculation::EstMode::REJECTION, 1.,
9,975,003✔
886
      material()); // One hit score
9,975,003✔
887
}
11,696,910✔
888

889
void VolEstRay::vol_scoring(
10,725,687✔
890
  const VolumeCalculation::EstMode mode, const double score, const int id_mat)
891
{
892
  const auto n_domains = vol_calc_.domain_ids_.size();
10,725,687✔
893

894
  switch (vol_calc_.domain_type_) {
10,725,687!
895
  case VolumeCalculation::TallyDomain::MATERIAL:
4,689,875✔
896
    if (id_mat != MATERIAL_VOID) {
4,689,875✔
897
      for (auto i_domain = 0; i_domain < n_domains; i_domain++) {
7,652,901✔
898
        if (model::materials[id_mat]->id_ == vol_calc_.domain_ids_[i_domain]) {
7,643,749✔
899
          vol_calc_.check_hit(id_mat, score, results_.vol_tallies[i_domain]);
4,675,740✔
900
          break;
4,675,740✔
901
        }
902
      }
903
    }
904
    break;
4,689,875✔
905
  case VolumeCalculation::TallyDomain::CELL:
4,396,977✔
906
    for (auto level = 0; level < n_coord_last(); ++level) {
8,793,954✔
907
      for (auto i_domain = 0; i_domain < n_domains; i_domain++) {
4,964,291✔
908
        if (current_cell(mode, level)->id_ == vol_calc_.domain_ids_[i_domain]) {
4,939,860✔
909
          vol_calc_.check_hit(id_mat, score, results_.vol_tallies[i_domain]);
4,372,546✔
910
          break;
4,372,546✔
911
        }
912
      }
913
    }
914
    break;
4,396,977✔
915
  case VolumeCalculation::TallyDomain::UNIVERSE:
1,638,835✔
916
    for (auto level = 0; level < n_coord_last(); ++level) {
3,277,670✔
917
      for (auto i_domain = 0; i_domain < n_domains; ++i_domain) {
1,638,835!
918
        if (model::universes[current_cell(mode, level)->universe_]->id_ ==
3,277,670!
919
            vol_calc_.domain_ids_[i_domain]) {
1,638,835✔
920
          vol_calc_.check_hit(id_mat, score, results_.vol_tallies[i_domain]);
1,638,835✔
921
          break;
1,638,835✔
922
        }
923
      }
924
    }
925
  }
926
}
10,725,687✔
927

928
inline std::unique_ptr<Cell>& VolEstRay::current_cell(
7,329,379✔
929
  VolumeCalculation::EstMode mode, int level)
930
{
931
  switch (mode) {
7,329,379!
932
  case VolumeCalculation::EstMode::REJECTION:
5,594,635✔
933
    return model::cells[coord(level).cell()]; // Current position
5,594,635✔
934
  case VolumeCalculation::EstMode::RAYTRACE:
1,734,744✔
935
    return model::cells[cell_last(level)]; // Previous segment
1,734,744✔
936
  }
UNCOV
937
}
×
938

939
void free_memory_volume()
7,490✔
940
{
941
  openmc::model::volume_calcs.clear();
7,490✔
942
}
7,490✔
943

944
} // namespace openmc
945

946
//==============================================================================
947
// OPENMC_CALCULATE_VOLUMES runs each of the stochastic volume calculations
948
// that the user has specified and writes results to HDF5 files
949
//==============================================================================
950

951
int openmc_calculate_volumes()
128✔
952
{
953
  using namespace openmc;
954

955
  if (mpi::master) {
128✔
956
    header("STOCHASTIC VOLUME CALCULATION", 3);
118✔
957
  }
958
  Timer time_volume;
128✔
959
  time_volume.start();
128✔
960

961
  for (int i = 0; i < model::volume_calcs.size(); ++i) {
445✔
962
    write_message(4, "Running volume calculation {}", i + 1);
344✔
963

964
    // Run volume calculation
965
    const auto& vol_calc {model::volume_calcs[i]};
344✔
966
    VolumeCalculation::CalcResults results(vol_calc);
344✔
967
    try {
968
      vol_calc.execute(results);
344✔
969
    } catch (const std::exception& e) {
27!
970
      set_errmsg(e.what());
27✔
971
      return OPENMC_E_UNASSIGNED;
27✔
972
    }
27✔
973

974
    if (mpi::master) {
317✔
975

976
      // Output volume calculation results and statistics
977
      vol_calc.show_results(results);
247✔
978

979
      // Write volumes to HDF5 file
980
      std::string filename =
981
        fmt::format("{}volume_{}.h5", settings::path_output, i + 1);
450✔
982
      vol_calc.to_hdf5(filename, results);
247✔
983
    }
247✔
984
  }
344✔
985

986
  // Show elapsed time
987
  time_volume.stop();
101✔
988
  write_message(6, "Elapsed time: {} sec", time_volume.elapsed());
101✔
989

990
  return 0;
101✔
991
}
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

© 2025 Coveralls, Inc