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

openmc-dev / openmc / 34416149688

09 Sep 2026 11:15PM UTC coverage: 81.48% (+0.1%) from 81.347%
34416149688

Pull #4087

github

web-flow
Merge 93138e589 into 5260b9a0f
Pull Request #4087: Compute bounding boxes for general planes and tori in C++

18756 of 27197 branches covered (68.96%)

Branch coverage included in aggregate %.

43 of 46 new or added lines in 2 files covered. (93.48%)

959 existing lines in 29 files now uncovered.

60863 of 70519 relevant lines covered (86.31%)

49704801.91 hits per line

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

78.87
/src/weight_windows.cpp
1
#include "openmc/weight_windows.h"
2

3
#include <algorithm>
4
#include <cassert>
5
#include <cmath>
6
#include <set>
7
#include <string>
8

9
#include "openmc/tensor.h"
10

11
#include "openmc/error.h"
12
#include "openmc/file_utils.h"
13
#include "openmc/hdf5_interface.h"
14
#include "openmc/mesh.h"
15
#include "openmc/message_passing.h"
16
#include "openmc/nuclide.h"
17
#include "openmc/output.h"
18
#include "openmc/particle.h"
19
#include "openmc/particle_data.h"
20
#include "openmc/physics_common.h"
21
#include "openmc/random_ray/flat_source_domain.h"
22
#include "openmc/search.h"
23
#include "openmc/settings.h"
24
#include "openmc/simulation.h"
25
#include "openmc/tallies/filter_energy.h"
26
#include "openmc/tallies/filter_mesh.h"
27
#include "openmc/tallies/filter_particle.h"
28
#include "openmc/tallies/tally.h"
29
#include "openmc/xml_interface.h"
30

31
#include <fmt/core.h>
32

33
namespace openmc {
34

35
//==============================================================================
36
// Global variables
37
//==============================================================================
38

39
namespace variance_reduction {
40

41
std::unordered_map<int32_t, int32_t> ww_map;
42
openmc::vector<unique_ptr<WeightWindows>> weight_windows;
43
openmc::vector<unique_ptr<WeightWindowsGenerator>> weight_windows_generators;
44

45
} // namespace variance_reduction
46

47
//==============================================================================
48
// WeightWindowSettings implementation
49
//==============================================================================
50

51
WeightWindows::WeightWindows(int32_t id)
442 ✔
52
{
53
  index_ = variance_reduction::weight_windows.size();
442 ✔
54
  set_id(id);
442 ✔
55
}
442 ✔
56

57
WeightWindows::WeightWindows(pugi::xml_node node)
305 ✔
58
{
59
  // Make sure required elements are present
60
  const vector<std::string> required_elems {
305 ✔
61
    "id", "particle_type", "lower_ww_bounds", "upper_ww_bounds"};
1,525 !
62
  for (const auto& elem : required_elems) {
1,525 ✔
63
    if (!check_for_node(node, elem.c_str())) {
1,220 !
UNCOV
64
      fatal_error(fmt::format("Must specify <{}> for weight windows.", elem));
×
65
    }
66
  }
67

68
  // Get weight windows ID
69
  int32_t id = std::stoi(get_node_value(node, "id"));
610 ✔
70
  this->set_id(id);
305 ✔
71

72
  // Get the particle type
73
  auto particle_type_str = std::string(get_node_value(node, "particle_type"));
305 ✔
74
  set_particle_type(ParticleType {particle_type_str});
305 ✔
75

76
  // Determine associated mesh
77
  int32_t mesh_id = std::stoi(get_node_value(node, "mesh"));
610 ✔
78
  set_mesh(model::mesh_map.at(mesh_id));
305 ✔
79

80
  // energy bounds
81
  if (check_for_node(node, "energy_bounds"))
305 ✔
82
    energy_bounds_ = get_node_array<double>(node, "energy_bounds");
260 ✔
83

84
  // get the survival value - optional
85
  if (check_for_node(node, "survival_ratio")) {
305 !
86
    survival_ratio_ = std::stod(get_node_value(node, "survival_ratio"));
610 ✔
87
    if (survival_ratio_ <= 1)
305 !
UNCOV
88
      fatal_error("Survival to lower weight window ratio must bigger than 1 "
×
89
                  "and less than the upper to lower weight window ratio.");
90
  }
91

92
  // get the max lower bound ratio - optional
93
  if (check_for_node(node, "max_lower_bound_ratio")) {
305 ✔
94
    max_lb_ratio_ = std::stod(get_node_value(node, "max_lower_bound_ratio"));
300 ✔
95
    if (max_lb_ratio_ < 1.0) {
150 !
UNCOV
96
      fatal_error("Maximum lower bound ratio must be larger than 1");
×
97
    }
98
  }
99

100
  // get the max split - optional
101
  if (check_for_node(node, "max_split")) {
305 !
102
    max_split_ = std::stod(get_node_value(node, "max_split"));
610 ✔
103
    if (max_split_ <= 1)
305 !
UNCOV
104
      fatal_error("max split must be larger than 1");
×
105
  }
106

107
  // weight cutoff - optional
108
  if (check_for_node(node, "weight_cutoff")) {
305 !
109
    weight_cutoff_ = std::stod(get_node_value(node, "weight_cutoff"));
610 ✔
110
    if (weight_cutoff_ <= 0)
305 !
UNCOV
111
      fatal_error("weight_cutoff must be larger than 0");
×
112
    if (weight_cutoff_ > 1)
305 !
UNCOV
113
      fatal_error("weight_cutoff must be less than 1");
×
114
  }
115

116
  // read the lower/upper weight bounds
117
  this->set_bounds(get_node_array<double>(node, "lower_ww_bounds"),
305 ✔
118
    get_node_array<double>(node, "upper_ww_bounds"));
610 ✔
119
}
305 ✔
120

121
WeightWindows::~WeightWindows()
747 ✔
122
{
123
  variance_reduction::ww_map.erase(id());
747 ✔
124
}
2,241 ✔
125

126
WeightWindows* WeightWindows::create(int32_t id)
164 ✔
127
{
128
  variance_reduction::weight_windows.push_back(make_unique<WeightWindows>());
164 ✔
129
  auto wws = variance_reduction::weight_windows.back().get();
164 ✔
130
  variance_reduction::ww_map[wws->id()] =
164 ✔
131
    variance_reduction::weight_windows.size() - 1;
164 ✔
132
  return wws;
164 ✔
133
}
134

135
WeightWindows* WeightWindows::from_hdf5(
37 ✔
136
  hid_t wws_group, const std::string& group_name)
137
{
138
  // collect ID from the name of this group
139
  hid_t ww_group = open_group(wws_group, group_name);
37 ✔
140

141
  auto wws = WeightWindows::create();
37 ✔
142

143
  std::string particle_type;
37 ✔
144
  read_dataset(ww_group, "particle_type", particle_type);
37 ✔
145
  wws->particle_type_ = ParticleType {particle_type};
37 ✔
146

147
  read_dataset<double>(ww_group, "energy_bounds", wws->energy_bounds_);
37 ✔
148

149
  int32_t mesh_id;
37 ✔
150
  read_dataset(ww_group, "mesh", mesh_id);
37 ✔
151

152
  if (model::mesh_map.count(mesh_id) == 0) {
37 !
UNCOV
153
    fatal_error(
×
UNCOV
154
      fmt::format("Mesh {} used in weight windows does not exist.", mesh_id));
×
155
  }
156
  wws->set_mesh(model::mesh_map[mesh_id]);
37 ✔
157

158
  wws->lower_ww_ =
37 ✔
159
    tensor::Tensor<double>({static_cast<size_t>(wws->bounds_size()[0]),
37 ✔
160
      static_cast<size_t>(wws->bounds_size()[1])});
37 ✔
161
  wws->upper_ww_ =
37 ✔
162
    tensor::Tensor<double>({static_cast<size_t>(wws->bounds_size()[0]),
37 ✔
163
      static_cast<size_t>(wws->bounds_size()[1])});
37 ✔
164

165
  read_dataset<double>(ww_group, "lower_ww_bounds", wws->lower_ww_);
37 ✔
166
  read_dataset<double>(ww_group, "upper_ww_bounds", wws->upper_ww_);
37 ✔
167
  read_dataset(ww_group, "survival_ratio", wws->survival_ratio_);
37 ✔
168
  read_dataset(ww_group, "max_lower_bound_ratio", wws->max_lb_ratio_);
37 ✔
169
  read_dataset(ww_group, "max_split", wws->max_split_);
37 ✔
170
  read_dataset(ww_group, "weight_cutoff", wws->weight_cutoff_);
37 ✔
171

172
  close_group(ww_group);
37 ✔
173

174
  return wws;
37 ✔
175
}
37 ✔
176

177
void WeightWindows::set_defaults()
912 ✔
178
{
179
  // set energy bounds to the min/max energy supported by the data
180
  if (energy_bounds_.size() == 0) {
912 ✔
181
    int p_type = particle_type_.transport_index();
561 ✔
182
    if (p_type == C_NONE) {
561 !
UNCOV
183
      fatal_error("Weight windows particle is not supported for transport.");
×
184
    }
185
    energy_bounds_.push_back(data::energy_min[p_type]);
561 ✔
186
    energy_bounds_.push_back(data::energy_max[p_type]);
561 ✔
187
  }
188
}
912 ✔
189

190
void WeightWindows::allocate_ww_bounds()
1,061 ✔
191
{
192
  auto shape = bounds_size();
1,061 ✔
193
  if (shape[0] * shape[1] == 0) {
1,061 !
UNCOV
194
    auto msg = fmt::format(
×
UNCOV
195
      "Size of weight window bounds is zero for WeightWindows {}", id());
×
UNCOV
196
    warning(msg);
×
197
  }
×
198
  lower_ww_ = tensor::Tensor<double>(
1,061 ✔
199
    {static_cast<size_t>(shape[0]), static_cast<size_t>(shape[1])});
1,061 ✔
200
  lower_ww_.fill(-1);
1,061 ✔
201
  upper_ww_ = tensor::Tensor<double>(
1,061 ✔
202
    {static_cast<size_t>(shape[0]), static_cast<size_t>(shape[1])});
1,061 ✔
203
  upper_ww_.fill(-1);
1,061 ✔
204
}
1,061 ✔
205

206
void WeightWindows::set_id(int32_t id)
1,025 ✔
207
{
208
  assert(id >= 0 || id == C_NONE);
1,025 !
209

210
  // Clear entry in mesh map in case one was already assigned
211
  if (id_ != C_NONE) {
1,025 !
212
    variance_reduction::ww_map.erase(id_);
1,025 ✔
213
    id_ = C_NONE;
1,025 ✔
214
  }
215

216
  // Ensure no other mesh has the same ID
217
  if (variance_reduction::ww_map.find(id) != variance_reduction::ww_map.end()) {
1,025 !
UNCOV
218
    throw std::runtime_error {
×
UNCOV
219
      fmt::format("Two weight windows have the same ID: {}", id)};
×
220
  }
221

222
  // If no ID is specified, auto-assign the next ID in the sequence
223
  if (id == C_NONE) {
1,025 ✔
224
    id = 0;
442 ✔
225
    for (const auto& m : variance_reduction::weight_windows) {
497 ✔
226
      id = std::max(id, m->id_);
110 !
227
    }
228
    ++id;
442 ✔
229
  }
230

231
  // Update ID and entry in the mesh map
232
  id_ = id;
1,025 ✔
233
  variance_reduction::ww_map[id] = index_;
1,025 ✔
234
}
1,025 ✔
235

236
void WeightWindows::set_energy_bounds(span<const double> bounds)
314 ✔
237
{
238
  energy_bounds_.clear();
314 ✔
239
  energy_bounds_.insert(energy_bounds_.begin(), bounds.begin(), bounds.end());
314 ✔
240
  // if the mesh is set, allocate space for weight window bounds
241
  if (mesh_idx_ != C_NONE)
314 !
242
    allocate_ww_bounds();
314 ✔
243
}
314 ✔
244

245
void WeightWindows::set_particle_type(ParticleType p_type)
732 ✔
246
{
247
  if (!p_type.is_neutron() && !p_type.is_photon())
732 !
UNCOV
248
    fatal_error(fmt::format(
×
UNCOV
249
      "Particle type '{}' cannot be applied to weight windows.", p_type.str()));
×
250
  particle_type_ = p_type;
732 ✔
251

252
  // The default energy grid is particle dependent, so derive it now that the
253
  // particle type is known
254
  set_defaults();
732 ✔
255
}
732 ✔
256

257
void WeightWindows::set_mesh(int32_t mesh_idx)
747 ✔
258
{
259
  if (mesh_idx < 0 || mesh_idx >= model::meshes.size())
747 !
UNCOV
260
    fatal_error(fmt::format("Could not find a mesh for index {}", mesh_idx));
×
261

262
  mesh_idx_ = mesh_idx;
747 ✔
263
  model::meshes[mesh_idx_]->prepare_for_point_location();
747 ✔
264
  allocate_ww_bounds();
747 ✔
265
}
747 ✔
266

UNCOV
267
void WeightWindows::set_mesh(const std::unique_ptr<Mesh>& mesh)
×
268
{
269
  set_mesh(mesh.get());
×
UNCOV
270
}
×
271

UNCOV
272
void WeightWindows::set_mesh(const Mesh* mesh)
×
273
{
274
  set_mesh(model::mesh_map[mesh->id_]);
×
UNCOV
275
}
×
276

277
std::pair<bool, WeightWindow> WeightWindows::get_weight_window(
463,116,290 ✔
278
  const Particle& p) const
279
{
280
  // check for particle type
281
  if (particle_type_ != p.type()) {
463,116,290 ✔
282
    return {false, {}};
37,130,533 ✔
283
  }
284

285
  // particle energy
286
  double E = p.E();
425,985,757 ✔
287

288
  // check to make sure energy is in range, expects sorted energy values
289
  if (E < energy_bounds_.front() || E > energy_bounds_.back())
425,985,757 !
290
    return {false, {}};
91,982 ✔
291

292
  // Get mesh index for particle's position
293
  const auto& mesh = this->mesh();
425,893,775 ✔
294
  int mesh_bin = mesh->get_bin(p.r());
425,893,775 ✔
295

296
  // particle is outside the weight window mesh
297
  if (mesh_bin < 0)
425,893,775 ✔
298
    return {false, {}};
29,428 ✔
299

300
  // get the mesh bin in energy group
301
  int energy_bin =
425,864,347 ✔
302
    lower_bound_index(energy_bounds_.begin(), energy_bounds_.end(), E);
425,864,347 ✔
303

304
  // mesh_bin += energy_bin * mesh->n_bins();
305
  // Create individual weight window
306
  WeightWindow ww;
425,864,347 ✔
307
  ww.lower_weight = lower_ww_(energy_bin, mesh_bin);
425,864,347 ✔
308
  ww.upper_weight = upper_ww_(energy_bin, mesh_bin);
425,864,347 ✔
309
  ww.survival_weight = ww.lower_weight * survival_ratio_;
425,864,347 ✔
310
  ww.max_lb_ratio = max_lb_ratio_;
425,864,347 ✔
311
  ww.max_split = max_split_;
425,864,347 ✔
312
  ww.weight_cutoff = weight_cutoff_;
425,864,347 ✔
313
  return {true, ww};
425,864,347 ✔
314
}
315

316
std::array<int, 2> WeightWindows::bounds_size() const
1,979 ✔
317
{
318
  int num_spatial_bins = this->mesh()->n_bins();
1,979 ✔
319
  int num_energy_bins =
1,979 ✔
320
    energy_bounds_.size() > 0 ? energy_bounds_.size() - 1 : 1;
1,979 ✔
321
  return {num_energy_bins, num_spatial_bins};
1,979 ✔
322
}
323

324
template<class T>
325
void WeightWindows::check_bounds(const T& lower, const T& upper) const
385 !
326
{
327
  // make sure that the upper and lower bounds have the same size
328
  if (lower.size() != upper.size()) {
385 !
UNCOV
329
    auto msg = fmt::format("The upper and lower weight window lengths do not "
×
330
                           "match.\n Lower size: {}\n Upper size: {}",
331
      lower.size(), upper.size());
×
332
    fatal_error(msg);
×
UNCOV
333
  }
×
334
  this->check_bounds(lower);
385 ✔
335
}
385 ✔
336

337
template<class T>
338
void WeightWindows::check_bounds(const T& bounds) const
385 ✔
339
{
340
  // check that the number of weight window entries is correct
341
  auto dims = this->bounds_size();
385 ✔
342
  if (bounds.size() != dims[0] * dims[1]) {
385 !
UNCOV
343
    auto err_msg =
×
344
      fmt::format("In weight window domain {} the number of spatial "
345
                  "energy/spatial bins ({}) does not match the number "
346
                  "of weight bins ({})",
347
        id_, dims, bounds.size());
×
348
    fatal_error(err_msg);
×
UNCOV
349
  }
×
350
}
385 ✔
351

UNCOV
352
void WeightWindows::set_bounds(const tensor::Tensor<double>& lower_bounds,
×
353
  const tensor::Tensor<double>& upper_bounds)
354
{
355

UNCOV
356
  this->check_bounds(lower_bounds, upper_bounds);
×
357

358
  // set new weight window values
359
  lower_ww_ = lower_bounds;
×
360
  upper_ww_ = upper_bounds;
×
UNCOV
361
}
×
362

UNCOV
363
void WeightWindows::set_bounds(
×
364
  const tensor::Tensor<double>& lower_bounds, double ratio)
365
{
UNCOV
366
  this->check_bounds(lower_bounds);
×
367

368
  // set new weight window values
369
  lower_ww_ = lower_bounds;
×
370
  upper_ww_ = lower_bounds;
×
371
  upper_ww_ *= ratio;
×
UNCOV
372
}
×
373

374
void WeightWindows::set_bounds(
385 ✔
375
  span<const double> lower_bounds, span<const double> upper_bounds)
376
{
377
  check_bounds(lower_bounds, upper_bounds);
385 ✔
378
  auto shape = this->bounds_size();
385 ✔
379
  lower_ww_ = tensor::Tensor<double>(
385 ✔
380
    {static_cast<size_t>(shape[0]), static_cast<size_t>(shape[1])});
385 ✔
381
  upper_ww_ = tensor::Tensor<double>(
385 ✔
382
    {static_cast<size_t>(shape[0]), static_cast<size_t>(shape[1])});
385 ✔
383

384
  // Copy weight window values from input spans into the tensors
385
  std::copy(lower_bounds.data(), lower_bounds.data() + lower_ww_.size(),
385 ✔
386
    lower_ww_.data());
387
  std::copy(upper_bounds.data(), upper_bounds.data() + upper_ww_.size(),
385 ✔
388
    upper_ww_.data());
389
}
385 ✔
390

UNCOV
391
void WeightWindows::set_bounds(span<const double> lower_bounds, double ratio)
×
392
{
UNCOV
393
  this->check_bounds(lower_bounds);
×
394

395
  auto shape = this->bounds_size();
×
396
  lower_ww_ = tensor::Tensor<double>(
×
397
    {static_cast<size_t>(shape[0]), static_cast<size_t>(shape[1])});
×
398
  upper_ww_ = tensor::Tensor<double>(
×
UNCOV
399
    {static_cast<size_t>(shape[0]), static_cast<size_t>(shape[1])});
×
400

401
  // Copy lower bounds into both arrays, then scale upper by ratio
UNCOV
402
  std::copy(lower_bounds.data(), lower_bounds.data() + lower_ww_.size(),
×
403
    lower_ww_.data());
UNCOV
404
  std::copy(lower_bounds.data(), lower_bounds.data() + upper_ww_.size(),
×
405
    upper_ww_.data());
406
  upper_ww_ *= ratio;
×
UNCOV
407
}
×
408

409
void WeightWindows::update_weights(const Tally* tally, const std::string& value,
331 ✔
410
  double threshold, double ratio, WeightWindowUpdateMethod method)
411
{
412
  ///////////////////////////
413
  // Setup and checks
414
  ///////////////////////////
415
  this->check_tally_update_compatibility(tally);
331 ✔
416

417
  // Dimensions of weight window arrays
418
  int e_bins = lower_ww_.shape(0);
331 !
419
  int64_t mesh_bins = lower_ww_.shape(1);
331 !
420

421
  // Initialize weight window arrays to -1.0 by default
422
#pragma omp parallel for collapse(2) schedule(static)
188 ✔
423
  for (int e = 0; e < e_bins; e++) {
1,056 ✔
424
    for (int64_t m = 0; m < mesh_bins; m++) {
1,400,491 ✔
425
      lower_ww_(e, m) = -1.0;
1,399,578 ✔
426
      upper_ww_(e, m) = -1.0;
1,399,578 ✔
427
    }
428
  }
429

430
  // determine which value to use
431
  const std::set<std::string> allowed_values = {"mean", "rel_err"};
993 !
432
  if (allowed_values.count(value) == 0) {
331 !
UNCOV
433
    fatal_error(fmt::format("Invalid value '{}' specified for weight window "
×
434
                            "generation. Must be one of: 'mean' or 'rel_err'",
435
      value));
436
  }
437

438
  // determine the index of the specified score
439
  int score_index = tally->score_index("flux");
331 ✔
440
  if (score_index == C_NONE) {
331 !
441
    fatal_error(
×
UNCOV
442
      fmt::format("A 'flux' score required for weight window generation "
×
443
                  "is not present on tally {}.",
UNCOV
444
        tally->id()));
×
445
  }
446

447
  ///////////////////////////
448
  // Extract tally data
449
  //
450
  // At the end of this section, mean and rel_err are
451
  // 2D tensors of tally data (n_e_groups, n_mesh_bins)
452
  //
453
  ///////////////////////////
454

455
  // build a shape for the tally results, this will always be
456
  // dimension 5 (3 filter dimensions, 1 score dimension, 1 results dimension)
457
  // Look for the size of the last dimension of the results tensor
458
  const auto& results = tally->results();
331 !
459
  const int results_dim = static_cast<int>(results.shape(2));
331 !
460
  std::array<int, 5> shape = {1, 1, 1, tally->n_scores(), results_dim};
331 ✔
461

462
  // set the shape for the filters applied on the tally
463
  for (int i = 0; i < tally->filters().size(); i++) {
1,280 ✔
464
    const auto& filter = model::tally_filters[tally->filters(i)];
949 ✔
465
    shape[i] = filter->n_bins();
949 ✔
466
  }
467

468
  // build the transpose information to re-order data according to filter type
469
  std::array<int, 5> transpose = {0, 1, 2, 3, 4};
331 ✔
470

471
  // track our filter types and where we've added new ones
472
  std::vector<FilterType> filter_types = tally->filter_types();
331 ✔
473

474
  // assign other filter types to dummy positions if needed
475
  if (!tally->has_filter(FilterType::PARTICLE))
331 ✔
476
    filter_types.push_back(FilterType::PARTICLE);
22 ✔
477

478
  if (!tally->has_filter(FilterType::ENERGY))
331 ✔
479
    filter_types.push_back(FilterType::ENERGY);
22 ✔
480

481
  // particle axis mapping
482
  transpose[0] =
331 ✔
483
    std::find(filter_types.begin(), filter_types.end(), FilterType::PARTICLE) -
331 ✔
484
    filter_types.begin();
331 ✔
485

486
  // energy axis mapping
487
  transpose[1] =
331 ✔
488
    std::find(filter_types.begin(), filter_types.end(), FilterType::ENERGY) -
331 ✔
489
    filter_types.begin();
331 ✔
490

491
  // mesh axis mapping
492
  transpose[2] =
331 ✔
493
    std::find(filter_types.begin(), filter_types.end(), FilterType::MESH) -
331 ✔
494
    filter_types.begin();
331 ✔
495

496
  // determine the index of the particle within its filter
497
  int particle_idx = 0;
331 ✔
498
  if (tally->has_filter(FilterType::PARTICLE)) {
331 ✔
499
    auto pf = tally->get_filter<ParticleFilter>();
309 ✔
500
    const auto& particles = pf->particles();
309 !
501

502
    auto p_it =
309 ✔
503
      std::find(particles.begin(), particles.end(), this->particle_type_);
309 !
504
    if (p_it == particles.end()) {
309 !
UNCOV
505
      auto msg = fmt::format("Particle type '{}' not present on Filter {} for "
×
506
                             "Tally {} used to update WeightWindows {}",
507
        this->particle_type_.str(), pf->id(), tally->id(), this->id());
×
508
      fatal_error(msg);
×
UNCOV
509
    }
×
510

511
    particle_idx = p_it - particles.begin();
309 ✔
512
  }
513

514
  // The tally results array is 3D: (n_filter_combos, n_scores, n_result_types).
515
  // The first dimension is a row-major flattening of up to 3 filter dimensions
516
  // (particle, energy, mesh) whose storage order depends on which filters the
517
  // tally has. We need to map our desired indices (particle, energy, mesh)
518
  // into the correct flat filter combination index.
519
  //
520
  // transpose[i] tells us which storage position holds dimension i:
521
  //   i=0 -> particle, i=1 -> energy, i=2 -> mesh
522
  // shape[j] gives the number of bins for filter storage position j.
523

524
  // Row-major strides for the 3 filter dimensions
525
  const int stride0 = shape[1] * shape[2];
331 ✔
526
  const int stride1 = shape[2];
331 ✔
527

528
  tensor::Tensor<double> sum(
331 ✔
529
    {static_cast<size_t>(e_bins), static_cast<size_t>(mesh_bins)});
331 ✔
530
  tensor::Tensor<double> sum_sq(
331 ✔
531
    {static_cast<size_t>(e_bins), static_cast<size_t>(mesh_bins)});
331 ✔
532

533
  const int i_sum = static_cast<int>(TallyResult::SUM);
331 ✔
534
  const int i_sum_sq = static_cast<int>(TallyResult::SUM_SQ);
331 ✔
535

536
  for (int e = 0; e < e_bins; e++) {
2,356 ✔
537
    for (int64_t m = 0; m < mesh_bins; m++) {
3,107,537 ✔
538
      // Place particle, energy, and mesh indices into their storage positions
539
      std::array<int, 3> idx = {0, 0, 0};
3,105,512 ✔
540
      idx[transpose[0]] = particle_idx;
3,105,512 ✔
541
      idx[transpose[1]] = e;
3,105,512 ✔
542
      idx[transpose[2]] = static_cast<int>(m);
3,105,512 ✔
543

544
      // Compute flat filter combination index (row-major over filter dims)
545
      int flat = idx[0] * stride0 + idx[1] * stride1 + idx[2];
3,105,512 ✔
546

547
      sum(e, m) = results(flat, score_index, i_sum);
3,105,512 ✔
548
      sum_sq(e, m) = results(flat, score_index, i_sum_sq);
3,105,512 ✔
549
    }
550
  }
551
  int n = tally->n_realizations_;
331 ✔
552

553
  //////////////////////////////////////////////
554
  //
555
  // Assign new weight windows
556
  //
557
  // Use references to the existing weight window data
558
  // to store and update the values
559
  //
560
  //////////////////////////////////////////////
561

562
  // up to this point the data arrays are views into the tally results (no
563
  // computation has been performed) now we'll switch references to the tally's
564
  // bounds to avoid allocating additional memory
565
  auto& new_bounds = this->lower_ww_;
331 ✔
566
  auto& rel_err = this->upper_ww_;
331 ✔
567

568
  // get mesh volumes
569
  auto mesh_vols = this->mesh()->volumes();
331 ✔
570

571
  // Calculate mean (new_bounds) and relative error
572
#pragma omp parallel for collapse(2) schedule(static)
188 ✔
573
  for (int e = 0; e < e_bins; e++) {
1,056 ✔
574
    for (int64_t m = 0; m < mesh_bins; m++) {
1,400,491 ✔
575
      // Calculate mean
576
      new_bounds(e, m) = sum(e, m) / n;
1,399,578 ✔
577
      // Calculate relative error
578
      if (sum(e, m) > 0.0) {
1,399,578 ✔
579
        double mean_val = new_bounds(e, m);
324,515 ✔
580
        double variance = (sum_sq(e, m) / n - mean_val * mean_val) / (n - 1);
324,515 ✔
581
        rel_err(e, m) = std::sqrt(variance) / mean_val;
324,515 ✔
582
      } else {
583
        rel_err(e, m) = INFTY;
1,075,063 ✔
584
      }
585
      if (value == "rel_err") {
1,399,578 ✔
586
        new_bounds(e, m) = 1.0 / rel_err(e, m);
345,000 ✔
587
      }
588
    }
589
  }
590

591
  // Divide by volume of mesh elements
592
#pragma omp parallel for collapse(2) schedule(static)
188 ✔
593
  for (int e = 0; e < e_bins; e++) {
1,056 ✔
594
    for (int64_t m = 0; m < mesh_bins; m++) {
1,400,491 ✔
595
      new_bounds(e, m) /= mesh_vols[m];
1,399,578 ✔
596
    }
597
  }
598

599
  if (method == WeightWindowUpdateMethod::MAGIC) {
331 ✔
600
    // For MAGIC, weight windows are proportional to the forward fluxes.
601
    // We normalize weight windows independently for each energy group.
602

603
    // Find group maximum and normalize (per energy group)
604
    for (int e = 0; e < e_bins; e++) {
1,892 ✔
605
      double group_max = 0.0;
942 ✔
606

607
      // Find maximum value across all elements in this energy group
608
#pragma omp parallel for schedule(static) reduction(max : group_max)
942 ✔
609
      for (int64_t m = 0; m < mesh_bins; m++) {
1,093,135 ✔
610
        if (new_bounds(e, m) > group_max) {
1,092,350 ✔
611
          group_max = new_bounds(e, m);
2,545 ✔
612
        }
613
      }
614

615
      // Normalize values in this energy group by the maximum value
616
      if (group_max > 0.0) {
1,727 ✔
617
        double norm_factor = 1.0 / (2.0 * group_max);
1,694 ✔
618
#pragma omp parallel for schedule(static)
924 ✔
619
        for (int64_t m = 0; m < mesh_bins; m++) {
1,092,220 ✔
620
          new_bounds(e, m) *= norm_factor;
1,091,450 ✔
621
        }
622
      }
623
    }
624
  } else {
625
    // For (FW-)CADIS, weight windows are inversely proportional to the adjoint
626
    // fluxes. We normalize the weight windows across all energy groups.
627
#pragma omp parallel for collapse(2) schedule(static)
98 ✔
628
    for (int e = 0; e < e_bins; e++) {
196 ✔
629
      for (int64_t m = 0; m < mesh_bins; m++) {
307,356 ✔
630
        // Take the inverse, but are careful not to divide by zero
631
        if (new_bounds(e, m) != 0.0) {
307,228 ✔
632
          new_bounds(e, m) = 1.0 / new_bounds(e, m);
292,610 ✔
633
        } else {
634
          new_bounds(e, m) = 0.0;
14,618 ✔
635
        }
636
      }
637
    }
638

639
    // Find the maximum value across all elements
640
    double max_val = 0.0;
98 ✔
641
#pragma omp parallel for collapse(2) schedule(static) reduction(max : max_val)
98 ✔
642
    for (int e = 0; e < e_bins; e++) {
196 ✔
643
      for (int64_t m = 0; m < mesh_bins; m++) {
307,356 ✔
644
        if (new_bounds(e, m) > max_val) {
307,228 ✔
645
          max_val = new_bounds(e, m);
915 ✔
646
        }
647
      }
648
    }
649

650
    // Parallel normalization
651
    if (max_val > 0.0) {
166 ✔
652
      double norm_factor = 1.0 / (2.0 * max_val);
134 ✔
653
#pragma omp parallel for collapse(2) schedule(static)
74 ✔
654
      for (int e = 0; e < e_bins; e++) {
180 ✔
655
        for (int64_t m = 0; m < mesh_bins; m++) {
292,730 ✔
656
          new_bounds(e, m) *= norm_factor;
292,610 ✔
657
        }
658
      }
659
    }
660
  }
661

662
  // Final processing
663
#pragma omp parallel for collapse(2) schedule(static)
188 ✔
664
  for (int e = 0; e < e_bins; e++) {
1,056 ✔
665
    for (int64_t m = 0; m < mesh_bins; m++) {
1,400,491 ✔
666
      // Values where the mean is zero should be ignored
667
      if (sum(e, m) <= 0.0) {
1,399,578 ✔
668
        new_bounds(e, m) = -1.0;
1,075,063 ✔
669
      }
670
      // Values where the relative error is higher than the threshold should be
671
      // ignored
672
      else if (rel_err(e, m) > threshold) {
324,515 ✔
673
        new_bounds(e, m) = -1.0;
1,420 ✔
674
      }
675
      // Set the upper bounds
676
      upper_ww_(e, m) = ratio * lower_ww_(e, m);
1,399,578 ✔
677
    }
678
  }
679
}
993 ✔
680

681
void WeightWindows::check_tally_update_compatibility(const Tally* tally)
331 ✔
682
{
683
  // define the set of allowed filters for the tally
684
  const std::set<FilterType> allowed_filters = {
331 ✔
685
    FilterType::MESH, FilterType::ENERGY, FilterType::PARTICLE};
331 ✔
686

687
  // retrieve a mapping of filter type to filter index for the tally
688
  auto filter_indices = tally->filter_indices();
331 ✔
689

690
  // a mesh filter is required for a tally used to update weight windows
691
  if (!filter_indices.count(FilterType::MESH)) {
331 !
UNCOV
692
    fatal_error(
×
693
      "A mesh filter is required for a tally to update weight window bounds");
694
  }
695

696
  // ensure the mesh filter is using the same mesh as this weight window object
697
  auto mesh_filter = tally->get_filter<MeshFilter>();
331 ✔
698

699
  // make sure that all of the filters present on the tally are allowed
700
  for (auto filter_pair : filter_indices) {
1,280 ✔
701
    if (allowed_filters.find(filter_pair.first) == allowed_filters.end()) {
949 !
UNCOV
702
      fatal_error(fmt::format("Invalid filter type '{}' found on tally "
×
703
                              "used for weight window generation.",
UNCOV
704
        model::tally_filters[tally->filters(filter_pair.second)]->type_str()));
×
705
    }
706
  }
707

708
  if (mesh_filter->mesh() != mesh_idx_) {
331 !
709
    int32_t mesh_filter_id = model::meshes[mesh_filter->mesh()]->id();
×
710
    int32_t ww_mesh_id = model::meshes[this->mesh_idx_]->id();
×
UNCOV
711
    fatal_error(fmt::format("Mesh filter {} uses a different mesh ({}) than "
×
712
                            "weight window {} mesh ({})",
UNCOV
713
      mesh_filter->id(), mesh_filter_id, id_, ww_mesh_id));
×
714
  }
715

716
  // if an energy filter exists, make sure the energy grid matches that of this
717
  // weight window object
718
  if (auto energy_filter = tally->get_filter<EnergyFilter>()) {
331 ✔
719
    std::vector<double> filter_bins = energy_filter->bins();
309 ✔
720
    std::set<double> filter_e_bounds(
309 ✔
721
      energy_filter->bins().begin(), energy_filter->bins().end());
309 ✔
722
    if (filter_e_bounds.size() != energy_bounds().size()) {
309 !
723
      fatal_error(
×
UNCOV
724
        fmt::format("Energy filter {} does not have the same number of energy "
×
725
                    "bounds ({}) as weight window object {} ({})",
726
          energy_filter->id(), filter_e_bounds.size(), id_,
×
UNCOV
727
          energy_bounds().size()));
×
728
    }
729

730
    for (auto e : energy_bounds()) {
2,621 ✔
731
      if (filter_e_bounds.count(e) == 0) {
2,312 !
UNCOV
732
        fatal_error(fmt::format(
×
733
          "Energy bounds of filter {} and weight windows {} do not match",
UNCOV
734
          energy_filter->id(), id_));
×
735
      }
736
    }
737
  }
309 ✔
738
}
331 ✔
739

740
void WeightWindows::to_hdf5(hid_t group) const
180 ✔
741
{
742
  hid_t ww_group = create_group(group, fmt::format("weight_windows_{}", id()));
180 ✔
743

744
  write_dataset(ww_group, "mesh", this->mesh()->id());
180 ✔
745
  write_dataset(ww_group, "particle_type", particle_type_.str());
180 ✔
746
  write_dataset(ww_group, "energy_bounds", energy_bounds_);
180 ✔
747
  write_dataset(ww_group, "lower_ww_bounds", lower_ww_);
180 ✔
748
  write_dataset(ww_group, "upper_ww_bounds", upper_ww_);
180 ✔
749
  write_dataset(ww_group, "survival_ratio", survival_ratio_);
180 ✔
750
  write_dataset(ww_group, "max_lower_bound_ratio", max_lb_ratio_);
180 ✔
751
  write_dataset(ww_group, "max_split", max_split_);
180 ✔
752
  write_dataset(ww_group, "weight_cutoff", weight_cutoff_);
180 ✔
753

754
  close_group(ww_group);
180 ✔
755
}
180 ✔
756

757
WeightWindowsGenerator::WeightWindowsGenerator(pugi::xml_node node)
127 ✔
758
{
759
  // read information from the XML node
760
  int32_t mesh_id = std::stoi(get_node_value(node, "mesh"));
254 ✔
761
  int32_t mesh_idx = model::mesh_map[mesh_id];
127 ✔
762
  max_realizations_ = std::stoi(get_node_value(node, "max_realizations"));
254 ✔
763

764
  int32_t active_batches = settings::n_batches - settings::n_inactive;
127 ✔
765
  if (max_realizations_ > active_batches) {
127 ✔
766
    auto msg =
53 ✔
767
      fmt::format("The maximum number of specified tally realizations ({}) is "
768
                  "greater than the number of active batches ({}).",
769
        max_realizations_, active_batches);
53 ✔
770
    warning(msg);
53 ✔
771
  }
53 ✔
772
  auto tmp_str = get_node_value(node, "particle_type", false, true);
127 ✔
773
  auto particle_type = ParticleType {tmp_str};
127 ✔
774

775
  update_interval_ = std::stoi(get_node_value(node, "update_interval"));
254 ✔
776
  on_the_fly_ = get_node_value_bool(node, "on_the_fly");
127 ✔
777

778
  std::vector<double> e_bounds;
127 ✔
779
  if (check_for_node(node, "energy_bounds")) {
127 ✔
780
    e_bounds = get_node_array<double>(node, "energy_bounds");
90 ✔
781
  } else {
782
    int p_type = particle_type.transport_index();
82 ✔
783
    if (p_type == C_NONE) {
82 !
UNCOV
784
      fatal_error("Weight windows particle is not supported for transport.");
×
785
    }
786
    e_bounds.push_back(data::energy_min[p_type]);
82 ✔
787
    e_bounds.push_back(data::energy_max[p_type]);
82 ✔
788
  }
789

790
  // set method
791
  std::string method_string = get_node_value(node, "method");
127 ✔
792
  if (method_string == "magic") {
127 ✔
793
    method_ = WeightWindowUpdateMethod::MAGIC;
44 ✔
794
    if (settings::solver_type == SolverType::RANDOM_RAY &&
44 !
795
        FlatSourceDomain::adjoint_requested_) {
UNCOV
796
      fatal_error("Random ray weight window generation with MAGIC cannot be "
×
797
                  "done in adjoint mode.");
798
    }
799
  } else if (method_string == "fw_cadis") {
83 !
800
    method_ = WeightWindowUpdateMethod::FW_CADIS;
83 ✔
801
    if (settings::solver_type != SolverType::RANDOM_RAY) {
83 !
UNCOV
802
      fatal_error("FW-CADIS can only be run in random ray solver mode.");
×
803
    }
804
    FlatSourceDomain::adjoint_requested_ = true;
83 ✔
805
    if (check_for_node(node, "targets")) {
83 ✔
806
      FlatSourceDomain::fw_cadis_local_ = true;
15 ✔
807
      targets_ = get_node_array<size_t>(node, "targets");
15 ✔
808
      FlatSourceDomain::fw_cadis_local_targets_.insert(
15 ✔
809
        std::end(FlatSourceDomain::fw_cadis_local_targets_),
15 ✔
810
        std::begin(targets_), std::end(targets_));
15 ✔
811
    }
812
  } else {
UNCOV
813
    fatal_error(fmt::format(
×
814
      "Unknown weight window update method '{}' specified", method_string));
815
  }
816

817
  // parse non-default update parameters if specified
818
  if (check_for_node(node, "update_parameters")) {
127 ✔
819
    pugi::xml_node params_node = node.child("update_parameters");
22 ✔
820
    if (check_for_node(params_node, "value"))
22 !
821
      tally_value_ = get_node_value(params_node, "value");
22 ✔
822
    if (check_for_node(params_node, "threshold"))
22 !
823
      threshold_ = std::stod(get_node_value(params_node, "threshold"));
44 ✔
824
    if (check_for_node(params_node, "ratio")) {
22 !
825
      ratio_ = std::stod(get_node_value(params_node, "ratio"));
44 ✔
826
    }
827
  }
828

829
  // check update parameter values
830
  if (tally_value_ != "mean" && tally_value_ != "rel_err") {
127 !
UNCOV
831
    fatal_error(fmt::format("Unsupported tally value '{}' specified for "
×
832
                            "weight window generation.",
UNCOV
833
      tally_value_));
×
834
  }
835
  if (threshold_ <= 0.0)
127 !
UNCOV
836
    fatal_error(fmt::format("Invalid relative error threshold '{}' (<= 0.0) "
×
837
                            "specified for weight window generation",
UNCOV
838
      ratio_));
×
839
  if (ratio_ <= 1.0)
127 !
UNCOV
840
    fatal_error(fmt::format("Invalid weight window ratio '{}' (<= 1.0) "
×
841
                            "specified for weight window generation",
UNCOV
842
      ratio_));
×
843

844
  // create a matching weight windows object
845
  auto wws = WeightWindows::create();
127 ✔
846
  ww_idx_ = wws->index();
127 ✔
847
  wws->set_mesh(mesh_idx);
127 ✔
848
  if (e_bounds.size() > 0)
127 !
849
    wws->set_energy_bounds(e_bounds);
127 ✔
850
  wws->set_particle_type(particle_type);
127 ✔
851
}
127 ✔
852

853
void WeightWindowsGenerator::create_tally()
127 ✔
854
{
855
  const auto& wws = variance_reduction::weight_windows[ww_idx_];
127 ✔
856

857
  // create a tally based on the WWG information
858
  Tally* ww_tally = Tally::create();
127 ✔
859
  tally_idx_ = model::tally_map[ww_tally->id()];
127 ✔
860
  ww_tally->set_scores({"flux"});
254 !
861

862
  int32_t mesh_id = wws->mesh()->id();
127 ✔
863
  int32_t mesh_idx = model::mesh_map.at(mesh_id);
127 ✔
864
  // see if there's already a mesh filter using this mesh
865
  bool found_mesh_filter = false;
127 ✔
866
  for (const auto& f : model::tally_filters) {
336 ✔
867
    if (f->type() == FilterType::MESH) {
231 ✔
868
      const auto* mesh_filter = dynamic_cast<MeshFilter*>(f.get());
22 !
869
      if (mesh_filter->mesh() == mesh_idx && !mesh_filter->translated() &&
44 !
870
          !mesh_filter->rotated()) {
22 ✔
871
        ww_tally->add_filter(f.get());
22 ✔
872
        found_mesh_filter = true;
873
        break;
874
      }
875
    }
876
  }
877

878
  if (!found_mesh_filter) {
105 ✔
879
    auto mesh_filter = Filter::create("mesh");
105 ✔
880
    openmc_mesh_filter_set_mesh(mesh_filter->index(), model::mesh_map[mesh_id]);
105 ✔
881
    ww_tally->add_filter(mesh_filter);
105 ✔
882
  }
883

884
  const auto& e_bounds = wws->energy_bounds();
127 !
885
  if (e_bounds.size() > 0) {
127 !
886
    auto energy_filter = Filter::create("energy");
127 ✔
887
    openmc_energy_filter_set_bins(
127 ✔
888
      energy_filter->index(), e_bounds.size(), e_bounds.data());
127 ✔
889
    ww_tally->add_filter(energy_filter);
127 ✔
890
  }
891

892
  // add a particle filter
893
  auto particle_type = wws->particle_type();
127 ✔
894
  auto particle_filter = Filter::create("particle");
127 ✔
895
  auto pf = dynamic_cast<ParticleFilter*>(particle_filter);
127 !
896
  pf->set_particles({&particle_type, 1});
127 ✔
897
  ww_tally->add_filter(particle_filter);
127 ✔
898
}
127 ✔
899

900
void WeightWindowsGenerator::update() const
3,512 ✔
901
{
902
  const auto& wws = variance_reduction::weight_windows[ww_idx_];
3,512 ✔
903

904
  Tally* tally = model::tallies[tally_idx_].get();
3,512 ✔
905

906
  // If in random ray mode, only update on the last batch
907
  if (settings::solver_type == SolverType::RANDOM_RAY) {
3,512 ✔
908
    if (simulation::current_batch != settings::n_batches) {
3,292 ✔
909
      return;
910
    }
911
    // If in Monte Carlo mode and beyond the number of max realizations or
912
    // not at the correct update interval, skip the update
913
  } else if (max_realizations_ < tally->n_realizations_ ||
220 ✔
914
             tally->n_realizations_ % update_interval_ != 0) {
44 !
915
    return;
916
  }
917

918
  wws->update_weights(tally, tally_value_, threshold_, ratio_, method_);
210 ✔
919

920
  // if we're not doing on the fly generation, reset the tally results once
921
  // we're done with the update
922
  if (!on_the_fly_)
210 !
923
    tally->reset();
×
924

925
  // TODO: deactivate or remove tally once weight window generation is
926
  // complete
927
}
928

929
//==============================================================================
930
// Non-member functions
931
//==============================================================================
932

933
std::pair<bool, WeightWindow> search_weight_window(const Particle& p)
441,348,984 ✔
934
{
935
  // TODO: this is a linear search - should do something more clever
936
  for (const auto& ww : variance_reduction::weight_windows) {
478,600,927 ✔
937
    auto [ww_found, weight_window] = ww->get_weight_window(p);
463,116,290 ✔
938
    if (ww_found)
463,116,290 ✔
939
      return {true, weight_window};
425,864,347 ✔
940
  }
941
  return {false, {}};
15,484,637 ✔
942
}
943

944
void apply_weight_windows(Particle& p)
193,576,624 ✔
945
{
946
  if (!settings::weight_windows_on)
193,576,624 ✔
947
    return;
190,682,015 ✔
948

949
  // Random ray rays are not Monte Carlo particles and must not be biased by
950
  // weight windows; the solver generates weight windows but never applies them
951
  if (settings::solver_type == SolverType::RANDOM_RAY)
14,113,151 ✔
952
    return;
953

954
  // WW on photon and neutron only
955
  if (!p.type().is_neutron() && !p.type().is_photon())
4,877,155 !
956
    return;
957

958
  // skip dead or no energy
959
  if (p.E() <= 0 || !p.alive())
4,877,155 !
960
    return;
961

962
  auto [ww_found, ww] = search_weight_window(p);
2,894,609 ✔
963
  if (ww_found && ww.is_valid()) {
2,894,609 ✔
964
    apply_weight_window(p, ww);
2,772,362 ✔
965
  } else {
966
    if (p.wgt_ww_born() == -1.0)
122,247 ✔
967
      p.wgt_ww_born() = 1.0;
62,458 ✔
968
  }
969
}
970

971
void apply_weight_window(Particle& p, WeightWindow weight_window)
441,149,683 ✔
972
{
973
  if (!weight_window.is_valid())
441,149,683 ✔
974
    return;
975

976
  // skip dead or no energy
977
  if (p.E() <= 0 || !p.alive())
386,943,377 ✔
978
    return;
979

980
  // If particle has not yet had its birth weight window value set, set it to
981
  // the current weight window.
982
  if (p.wgt_ww_born() == -1.0)
378,930,423 ✔
983
    p.wgt_ww_born() =
875,422 ✔
984
      (weight_window.lower_weight + weight_window.upper_weight) / 2;
875,422 ✔
985

986
  // Normalize weight windows based on particle's starting weight
987
  // and the value of the weight window the particle was born in.
988
  weight_window.scale(p.wgt_born() / p.wgt_ww_born());
378,930,423 ✔
989

990
  // get the paramters
991
  double weight = p.wgt();
378,930,423 ✔
992

993
  // first check to see if particle should be killed for weight cutoff
994
  if (p.wgt() < weight_window.weight_cutoff) {
378,930,423 ✔
995
    p.wgt() = 0.0;
473 ✔
996
    return;
473 ✔
997
  }
998

999
  // check if particle is far above current weight window
1000
  // only do this if the factor is not already set on the particle and a
1001
  // maximum lower bound ratio is specified
1002
  if (p.ww_factor() == 0.0 && weight_window.max_lb_ratio > 1.0 &&
378,929,950 ✔
1003
      p.wgt() > weight_window.lower_weight * weight_window.max_lb_ratio) {
136,510 ✔
1004
    p.ww_factor() =
112,343 ✔
1005
      p.wgt() / (weight_window.lower_weight * weight_window.max_lb_ratio);
112,343 ✔
1006
  }
1007

1008
  // move weight window closer to the particle weight if needed
1009
  if (p.ww_factor() > 1.0)
378,929,950 ✔
1010
    weight_window.scale(p.ww_factor());
21,130,384 ✔
1011

1012
  // If the particle's weight is above the weight window, split it until the
1013
  // resulting particles are within the window. The comparisons use a relative
1014
  // dead band so that the branch taken is insensitive to bit-level differences
1015
  // in the window bounds (see WEIGHT_WINDOW_REL_TOL).
1016
  if (weight > weight_window.upper_weight * (1.0 + WEIGHT_WINDOW_REL_TOL)) {
378,929,950 ✔
1017
    // do not further split the particle if above the limit
1018
    if (p.n_split() >= settings::max_history_splits)
21,470,180 ✔
1019
      return;
12,011,934 ✔
1020

1021
    // Dividing by the same dead-banded bound used in the branch condition
1022
    // keeps the number of splits stable when the weight-to-bound ratio sits
1023
    // within rounding of an exact integer, which the weight window arithmetic
1024
    // itself can produce (e.g., a roulette survivor assigned weight *
1025
    // max_split, later split against an upper bound that is an exact multiple
1026
    // of the same lower bound). Ratios within the dead band of an integer
1027
    // consistently round down, and the branch condition guarantees the ratio
1028
    // exceeds one; the lower clamp of 2 makes the always-splits invariant
1029
    // explicit.
1030
    double n_split = std::max(2.0,
18,916,492 ✔
1031
      std::ceil(
9,458,246 ✔
1032
        weight / ((1.0 + WEIGHT_WINDOW_REL_TOL) * weight_window.upper_weight)));
9,458,246 ✔
1033
    double max_split = weight_window.max_split;
9,458,246 ✔
1034
    n_split = std::min(n_split, max_split);
9,458,246 ✔
1035

1036
    p.n_split() += n_split;
9,458,246 ✔
1037

1038
    // Create secondaries and divide weight among all particles
1039
    int i_split = std::round(n_split);
9,458,246 ✔
1040
    for (int l = 0; l < i_split - 1; l++) {
28,490,162 ✔
1041
      p.split(weight / n_split);
19,031,916 ✔
1042
    }
1043
    // remaining weight is applied to current particle
1044
    p.wgt() = weight / n_split;
9,458,246 ✔
1045

1046
  } else if (weight <
357,459,770 ✔
1047
             weight_window.lower_weight * (1.0 - WEIGHT_WINDOW_REL_TOL)) {
357,459,770 ✔
1048
    // if the particle weight is below the window, play Russian roulette
1049
    double weight_survive =
12,704,412 ✔
1050
      std::min(weight * weight_window.max_split, weight_window.survival_weight);
12,704,412 ✔
1051
    russian_roulette(p, weight_survive);
12,704,412 ✔
1052
  } // else particle is in the window, continue as normal
1053
}
1054

1055
void free_memory_weight_windows()
9,430 ✔
1056
{
1057
  variance_reduction::ww_map.clear();
9,430 ✔
1058
  variance_reduction::weight_windows.clear();
9,430 ✔
1059
  variance_reduction::weight_windows_generators.clear();
9,430 ✔
1060
}
9,430 ✔
1061

1062
void finalize_variance_reduction()
9,273 ✔
1063
{
1064
  for (const auto& wwg : variance_reduction::weight_windows_generators) {
9,400 ✔
1065
    wwg->create_tally();
127 ✔
1066
  }
1067
}
9,273 ✔
1068

1069
//==============================================================================
1070
// C API
1071
//==============================================================================
1072

1073
int verify_ww_index(int32_t index)
3,231 ✔
1074
{
1075
  if (index < 0 || index >= variance_reduction::weight_windows.size()) {
3,231 !
1076
    set_errmsg(fmt::format("Index '{}' for weight windows is invalid", index));
×
1077
    return OPENMC_E_OUT_OF_BOUNDS;
×
1078
  }
1079
  return 0;
1080
}
1081

1082
extern "C" int openmc_get_weight_windows_index(int32_t id, int32_t* idx)
413 ✔
1083
{
1084
  auto it = variance_reduction::ww_map.find(id);
413 ✔
1085
  if (it == variance_reduction::ww_map.end()) {
413 ✔
1086
    set_errmsg(fmt::format("No weight windows exist with ID={}", id));
124 ✔
1087
    return OPENMC_E_INVALID_ID;
124 ✔
1088
  }
1089

1090
  *idx = it->second;
289 ✔
1091
  return 0;
289 ✔
1092
}
1093

1094
extern "C" int openmc_weight_windows_get_id(int32_t index, int32_t* id)
900 ✔
1095
{
1096
  if (int err = verify_ww_index(index))
900 !
1097
    return err;
1098

1099
  const auto& wws = variance_reduction::weight_windows.at(index);
900 ✔
1100
  *id = wws->id();
900 ✔
1101
  return 0;
900 ✔
1102
}
1103

1104
extern "C" int openmc_weight_windows_set_id(int32_t index, int32_t id)
278 ✔
1105
{
1106
  if (int err = verify_ww_index(index))
278 !
1107
    return err;
1108

1109
  const auto& wws = variance_reduction::weight_windows.at(index);
278 ✔
1110
  wws->set_id(id);
278 ✔
1111
  return 0;
278 ✔
1112
}
1113

1114
extern "C" int openmc_weight_windows_update_magic(int32_t ww_idx,
121 ✔
1115
  int32_t tally_idx, const char* value, double threshold, double ratio)
1116
{
1117
  if (int err = verify_ww_index(ww_idx))
121 !
1118
    return err;
1119

1120
  if (tally_idx < 0 || tally_idx >= model::tallies.size()) {
121 !
1121
    set_errmsg(fmt::format("Index '{}' for tally is invalid", tally_idx));
×
1122
    return OPENMC_E_OUT_OF_BOUNDS;
×
1123
  }
1124

1125
  // get the requested tally
1126
  const Tally* tally = model::tallies.at(tally_idx).get();
121 ✔
1127

1128
  // get the WeightWindows object
1129
  const auto& wws = variance_reduction::weight_windows.at(ww_idx);
121 ✔
1130

1131
  wws->update_weights(tally, value, threshold, ratio);
121 ✔
1132

1133
  return 0;
121 ✔
1134
}
1135

1136
extern "C" int openmc_weight_windows_set_mesh(int32_t ww_idx, int32_t mesh_idx)
278 ✔
1137
{
1138
  if (int err = verify_ww_index(ww_idx))
278 !
1139
    return err;
1140
  const auto& wws = variance_reduction::weight_windows.at(ww_idx);
278 ✔
1141
  wws->set_mesh(mesh_idx);
278 ✔
1142
  return 0;
278 ✔
1143
}
1144

1145
extern "C" int openmc_weight_windows_get_mesh(int32_t ww_idx, int32_t* mesh_idx)
22 ✔
1146
{
1147
  if (int err = verify_ww_index(ww_idx))
22 !
1148
    return err;
1149
  const auto& wws = variance_reduction::weight_windows.at(ww_idx);
22 ✔
1150
  *mesh_idx = model::mesh_map.at(wws->mesh()->id());
22 ✔
1151
  return 0;
22 ✔
1152
}
1153

1154
extern "C" int openmc_weight_windows_set_energy_bounds(
187 ✔
1155
  int32_t ww_idx, double* e_bounds, size_t e_bounds_size)
1156
{
1157
  if (int err = verify_ww_index(ww_idx))
187 !
1158
    return err;
1159
  const auto& wws = variance_reduction::weight_windows.at(ww_idx);
187 ✔
1160
  wws->set_energy_bounds({e_bounds, e_bounds_size});
187 ✔
1161
  return 0;
187 ✔
1162
}
1163

1164
extern "C" int openmc_weight_windows_get_energy_bounds(
77 ✔
1165
  int32_t ww_idx, const double** e_bounds, size_t* e_bounds_size)
1166
{
1167
  if (int err = verify_ww_index(ww_idx))
77 !
1168
    return err;
1169
  const auto& wws = variance_reduction::weight_windows[ww_idx].get();
77 ✔
1170
  *e_bounds = wws->energy_bounds().data();
77 ✔
1171
  *e_bounds_size = wws->energy_bounds().size();
77 ✔
1172
  return 0;
77 ✔
1173
}
1174

1175
extern "C" int openmc_weight_windows_set_particle(
300 ✔
1176
  int32_t index, int32_t particle)
1177
{
1178
  if (int err = verify_ww_index(index))
300 !
1179
    return err;
1180

1181
  const auto& wws = variance_reduction::weight_windows.at(index);
300 ✔
1182
  wws->set_particle_type(ParticleType {particle});
300 ✔
1183
  return 0;
300 ✔
1184
}
1185

1186
extern "C" int openmc_weight_windows_get_particle(
55 ✔
1187
  int32_t index, int32_t* particle)
1188
{
1189
  if (int err = verify_ww_index(index))
55 !
1190
    return err;
1191

1192
  const auto& wws = variance_reduction::weight_windows.at(index);
55 ✔
1193
  *particle = wws->particle_type().pdg_number();
55 ✔
1194
  return 0;
55 ✔
1195
}
1196

1197
extern "C" int openmc_weight_windows_get_bounds(int32_t index,
528 ✔
1198
  const double** lower_bounds, const double** upper_bounds, size_t* size)
1199
{
1200
  if (int err = verify_ww_index(index))
528 !
1201
    return err;
1202

1203
  const auto& wws = variance_reduction::weight_windows[index];
528 ✔
1204
  *size = wws->lower_ww_bounds().size();
528 ✔
1205
  *lower_bounds = wws->lower_ww_bounds().data();
528 ✔
1206
  *upper_bounds = wws->upper_ww_bounds().data();
528 ✔
1207
  return 0;
528 ✔
1208
}
1209

1210
extern "C" int openmc_weight_windows_set_bounds(int32_t index,
80 ✔
1211
  const double* lower_bounds, const double* upper_bounds, size_t size)
1212
{
1213
  if (int err = verify_ww_index(index))
80 !
1214
    return err;
1215

1216
  const auto& wws = variance_reduction::weight_windows[index];
80 ✔
1217
  wws->set_bounds(span<const double>(lower_bounds, size),
80 ✔
1218
    span<const double>(upper_bounds, size));
1219
  return 0;
80 ✔
1220
}
1221

1222
extern "C" int openmc_weight_windows_get_survival_ratio(
44 ✔
1223
  int32_t index, double* ratio)
1224
{
1225
  if (int err = verify_ww_index(index))
44 !
1226
    return err;
1227
  const auto& wws = variance_reduction::weight_windows[index];
44 ✔
1228
  *ratio = wws->survival_ratio();
44 ✔
1229
  return 0;
44 ✔
1230
}
1231

1232
extern "C" int openmc_weight_windows_set_survival_ratio(
69 ✔
1233
  int32_t index, double ratio)
1234
{
1235
  if (int err = verify_ww_index(index))
69 !
1236
    return err;
1237
  const auto& wws = variance_reduction::weight_windows[index];
69 ✔
1238
  wws->survival_ratio() = ratio;
69 ✔
1239
  std::cout << "Survival ratio: " << wws->survival_ratio() << std::endl;
69 ✔
1240
  return 0;
69 ✔
1241
}
1242

1243
extern "C" int openmc_weight_windows_get_max_lower_bound_ratio(
44 ✔
1244
  int32_t index, double* lb_ratio)
1245
{
1246
  if (int err = verify_ww_index(index))
44 !
1247
    return err;
1248
  const auto& wws = variance_reduction::weight_windows[index];
44 ✔
1249
  *lb_ratio = wws->max_lower_bound_ratio();
44 ✔
1250
  return 0;
44 ✔
1251
}
1252

1253
extern "C" int openmc_weight_windows_set_max_lower_bound_ratio(
22 ✔
1254
  int32_t index, double lb_ratio)
1255
{
1256
  if (int err = verify_ww_index(index))
22 !
1257
    return err;
1258
  const auto& wws = variance_reduction::weight_windows[index];
22 ✔
1259
  wws->max_lower_bound_ratio() = lb_ratio;
22 ✔
1260
  return 0;
22 ✔
1261
}
1262

1263
extern "C" int openmc_weight_windows_get_weight_cutoff(
44 ✔
1264
  int32_t index, double* cutoff)
1265
{
1266
  if (int err = verify_ww_index(index))
44 !
1267
    return err;
1268
  const auto& wws = variance_reduction::weight_windows[index];
44 ✔
1269
  *cutoff = wws->weight_cutoff();
44 ✔
1270
  return 0;
44 ✔
1271
}
1272

1273
extern "C" int openmc_weight_windows_set_weight_cutoff(
69 ✔
1274
  int32_t index, double cutoff)
1275
{
1276
  if (int err = verify_ww_index(index))
69 !
1277
    return err;
1278
  const auto& wws = variance_reduction::weight_windows[index];
69 ✔
1279
  wws->weight_cutoff() = cutoff;
69 ✔
1280
  return 0;
69 ✔
1281
}
1282

1283
extern "C" int openmc_weight_windows_get_max_split(
44 ✔
1284
  int32_t index, int* max_split)
1285
{
1286
  if (int err = verify_ww_index(index))
44 !
1287
    return err;
1288
  const auto& wws = variance_reduction::weight_windows[index];
44 ✔
1289
  *max_split = wws->max_split();
44 ✔
1290
  return 0;
44 ✔
1291
}
1292

1293
extern "C" int openmc_weight_windows_set_max_split(int32_t index, int max_split)
69 ✔
1294
{
1295
  if (int err = verify_ww_index(index))
69 !
1296
    return err;
1297
  const auto& wws = variance_reduction::weight_windows[index];
69 ✔
1298
  wws->max_split() = max_split;
69 ✔
1299
  return 0;
69 ✔
1300
}
1301

1302
extern "C" int openmc_extend_weight_windows(
278 ✔
1303
  int32_t n, int32_t* index_start, int32_t* index_end)
1304
{
1305
  if (index_start)
278 !
1306
    *index_start = variance_reduction::weight_windows.size();
278 ✔
1307
  if (index_end)
278 !
1308
    *index_end = variance_reduction::weight_windows.size() + n - 1;
×
1309
  for (int i = 0; i < n; ++i)
556 ✔
1310
    variance_reduction::weight_windows.push_back(make_unique<WeightWindows>());
278 ✔
1311
  return 0;
278 ✔
1312
}
1313

1314
extern "C" size_t openmc_weight_windows_size()
154 ✔
1315
{
1316
  return variance_reduction::weight_windows.size();
154 ✔
1317
}
1318

1319
extern "C" int openmc_weight_windows_export(const char* filename)
185 ✔
1320
{
1321

1322
  if (!mpi::master)
185 ✔
1323
    return 0;
1324

1325
  std::string name = filename ? filename : "weight_windows.h5";
280 ✔
1326

1327
  write_message(fmt::format("Exporting weight windows to {}...", name), 5);
201 ✔
1328

1329
  hid_t ww_file = file_open(name, 'w');
169 ✔
1330

1331
  // Write file type
1332
  write_attribute(ww_file, "filetype", "weight_windows");
169 ✔
1333

1334
  // Write revisiion number for state point file
1335
  write_attribute(ww_file, "version", VERSION_WEIGHT_WINDOWS);
169 ✔
1336

1337
  hid_t weight_windows_group = create_group(ww_file, "weight_windows");
169 ✔
1338

1339
  hid_t mesh_group = create_group(ww_file, "meshes");
169 ✔
1340

1341
  std::vector<int32_t> mesh_ids;
169 ✔
1342
  std::vector<int32_t> ww_ids;
169 ✔
1343
  for (const auto& ww : variance_reduction::weight_windows) {
349 ✔
1344
    // Backstop for objects built through the C API whose particle type was
1345
    // never set explicitly, so an empty energy grid is never written out
1346
    ww->set_defaults();
180 ✔
1347

1348
    ww->to_hdf5(weight_windows_group);
180 ✔
1349
    ww_ids.push_back(ww->id());
180 ✔
1350

1351
    // if the mesh has already been written, move on
1352
    int32_t mesh_id = ww->mesh()->id();
180 !
1353
    if (std::find(mesh_ids.begin(), mesh_ids.end(), mesh_id) != mesh_ids.end())
180 !
UNCOV
1354
      continue;
×
1355

1356
    mesh_ids.push_back(mesh_id);
180 ✔
1357
    ww->mesh()->to_hdf5(mesh_group);
180 ✔
1358
  }
1359

1360
  write_attribute(mesh_group, "n_meshes", mesh_ids.size());
169 ✔
1361
  write_attribute(mesh_group, "ids", mesh_ids);
169 ✔
1362
  close_group(mesh_group);
169 ✔
1363

1364
  write_attribute(weight_windows_group, "n_weight_windows", ww_ids.size());
169 ✔
1365
  write_attribute(weight_windows_group, "ids", ww_ids);
169 ✔
1366
  close_group(weight_windows_group);
169 ✔
1367

1368
  file_close(ww_file);
169 ✔
1369

1370
  return 0;
169 ✔
1371
}
354 ✔
1372

1373
extern "C" int openmc_weight_windows_import(const char* filename)
37 ✔
1374
{
1375
  std::string name = filename ? filename : "weight_windows.h5";
37 !
1376

1377
  if (mpi::master)
37 ✔
1378
    write_message(fmt::format("Importing weight windows from {}...", name), 5);
72 ✔
1379

1380
  if (!file_exists(name)) {
37 !
UNCOV
1381
    set_errmsg(fmt::format("File '{}' does not exist", name));
×
1382
  }
1383

1384
  hid_t ww_file = file_open(name, 'r');
37 ✔
1385

1386
  // Check that filetype is correct
1387
  std::string filetype;
37 ✔
1388
  read_attribute(ww_file, "filetype", filetype);
37 ✔
1389
  if (filetype != "weight_windows") {
37 !
UNCOV
1390
    file_close(ww_file);
×
UNCOV
1391
    set_errmsg(fmt::format("File '{}' is not a weight windows file.", name));
×
UNCOV
1392
    return OPENMC_E_INVALID_ARGUMENT;
×
1393
  }
1394

1395
  // Check that the file version is compatible
1396
  std::array<int, 2> file_version;
37 ✔
1397
  read_attribute(ww_file, "version", file_version);
37 ✔
1398
  if (file_version[0] != VERSION_WEIGHT_WINDOWS[0]) {
37 !
1399
    std::string err_msg =
×
1400
      fmt::format("File '{}' has version {} which is incompatible with the "
1401
                  "expected version ({}).",
1402
        name, file_version, VERSION_WEIGHT_WINDOWS);
×
UNCOV
1403
    set_errmsg(err_msg);
×
UNCOV
1404
    return OPENMC_E_INVALID_ARGUMENT;
×
UNCOV
1405
  }
×
1406

1407
  hid_t weight_windows_group = open_group(ww_file, "weight_windows");
37 ✔
1408

1409
  hid_t mesh_group = open_group(ww_file, "meshes");
37 ✔
1410

1411
  read_meshes(mesh_group);
37 ✔
1412

1413
  std::vector<std::string> names = group_names(weight_windows_group);
37 ✔
1414

1415
  for (const auto& name : names) {
74 ✔
1416
    WeightWindows::from_hdf5(weight_windows_group, name);
37 ✔
1417
  }
1418

1419
  close_group(weight_windows_group);
37 ✔
1420

1421
  file_close(ww_file);
37 ✔
1422

1423
  return 0;
37 ✔
1424
}
74 ✔
1425

1426
} // namespace openmc
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc