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

openmc-dev / openmc / 21960815265

12 Feb 2026 07:17PM UTC coverage: 81.875% (+0.02%) from 81.851%
21960815265

Pull #3751

github

web-flow
Merge 942f5e16f into 8198e6021
Pull Request #3751: Resolve conflict with weight windows and global russian roulette

17421 of 24368 branches covered (71.49%)

Branch coverage included in aggregate %.

85 of 90 new or added lines in 5 files covered. (94.44%)

121 existing lines in 4 files now uncovered.

56462 of 65871 relevant lines covered (85.72%)

44576043.34 hits per line

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

79.0
/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 "xtensor/xdynamic_view.hpp"
10
#include "xtensor/xindex_view.hpp"
11
#include "xtensor/xio.hpp"
12
#include "xtensor/xmasked_view.hpp"
13
#include "xtensor/xnoalias.hpp"
14
#include "xtensor/xview.hpp"
15

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

36
#include <fmt/core.h>
37

38
namespace openmc {
39

40
//==============================================================================
41
// Global variables
42
//==============================================================================
43

44
namespace variance_reduction {
45

46
std::unordered_map<int32_t, int32_t> ww_map;
47
openmc::vector<unique_ptr<WeightWindows>> weight_windows;
48
openmc::vector<unique_ptr<WeightWindowsGenerator>> weight_windows_generators;
49

50
} // namespace variance_reduction
51

52
//==============================================================================
53
// WeightWindowSettings implementation
54
//==============================================================================
55

56
WeightWindows::WeightWindows(int32_t id)
223✔
57
{
58
  index_ = variance_reduction::weight_windows.size();
223✔
59
  set_id(id);
223✔
60
  set_defaults();
223✔
61
}
223✔
62

63
WeightWindows::WeightWindows(pugi::xml_node node)
97✔
64
{
65
  // Make sure required elements are present
66
  const vector<std::string> required_elems {
67
    "id", "particle_type", "lower_ww_bounds", "upper_ww_bounds"};
679✔
68
  for (const auto& elem : required_elems) {
485✔
69
    if (!check_for_node(node, elem.c_str())) {
388!
70
      fatal_error(fmt::format("Must specify <{}> for weight windows.", elem));
×
71
    }
72
  }
73

74
  // Get weight windows ID
75
  int32_t id = std::stoi(get_node_value(node, "id"));
97✔
76
  this->set_id(id);
97✔
77

78
  // get the particle type
79
  auto particle_type_str = std::string(get_node_value(node, "particle_type"));
97✔
80
  particle_type_ = ParticleType {particle_type_str};
97✔
81

82
  // Determine associated mesh
83
  int32_t mesh_id = std::stoi(get_node_value(node, "mesh"));
97✔
84
  set_mesh(model::mesh_map.at(mesh_id));
97✔
85

86
  // energy bounds
87
  if (check_for_node(node, "energy_bounds"))
97✔
88
    energy_bounds_ = get_node_array<double>(node, "energy_bounds");
69✔
89

90
  // get the survival value - optional
91
  if (check_for_node(node, "survival_ratio")) {
97!
92
    survival_ratio_ = std::stod(get_node_value(node, "survival_ratio"));
97✔
93
    if (survival_ratio_ <= 1)
97!
94
      fatal_error("Survival to lower weight window ratio must bigger than 1 "
×
95
                  "and less than the upper to lower weight window ratio.");
96
  }
97

98
  // get the max lower bound ratio - optional
99
  if (check_for_node(node, "max_lower_bound_ratio")) {
97✔
100
    max_lb_ratio_ = std::stod(get_node_value(node, "max_lower_bound_ratio"));
30✔
101
    if (max_lb_ratio_ < 1.0) {
30!
102
      fatal_error("Maximum lower bound ratio must be larger than 1");
×
103
    }
104
  }
105

106
  // get the max split - optional
107
  if (check_for_node(node, "max_split")) {
97!
108
    max_split_ = std::stod(get_node_value(node, "max_split"));
97✔
109
    if (max_split_ <= 1)
97!
110
      fatal_error("max split must be larger than 1");
×
111
  }
112

113
  // weight cutoff - optional
114
  if (check_for_node(node, "weight_cutoff")) {
97!
115
    weight_cutoff_ = std::stod(get_node_value(node, "weight_cutoff"));
97✔
116
    if (weight_cutoff_ <= 0)
97!
117
      fatal_error("weight_cutoff must be larger than 0");
×
118
    if (weight_cutoff_ > 1)
97!
119
      fatal_error("weight_cutoff must be less than 1");
×
120
  }
121

122
  // read the lower/upper weight bounds
123
  this->set_bounds(get_node_array<double>(node, "lower_ww_bounds"),
97✔
124
    get_node_array<double>(node, "upper_ww_bounds"));
194✔
125

126
  set_defaults();
97✔
127
}
97✔
128

129
WeightWindows::~WeightWindows()
320✔
130
{
131
  variance_reduction::ww_map.erase(id());
320✔
132
}
320✔
133

134
WeightWindows* WeightWindows::create(int32_t id)
83✔
135
{
136
  variance_reduction::weight_windows.push_back(make_unique<WeightWindows>());
83✔
137
  auto wws = variance_reduction::weight_windows.back().get();
83✔
138
  variance_reduction::ww_map[wws->id()] =
83✔
139
    variance_reduction::weight_windows.size() - 1;
83✔
140
  return wws;
83✔
141
}
142

143
WeightWindows* WeightWindows::from_hdf5(
10✔
144
  hid_t wws_group, const std::string& group_name)
145
{
146
  // collect ID from the name of this group
147
  hid_t ww_group = open_group(wws_group, group_name);
10✔
148

149
  auto wws = WeightWindows::create();
10✔
150

151
  std::string particle_type;
10✔
152
  read_dataset(ww_group, "particle_type", particle_type);
10✔
153
  wws->particle_type_ = ParticleType {particle_type};
10✔
154

155
  read_dataset<double>(ww_group, "energy_bounds", wws->energy_bounds_);
10✔
156

157
  int32_t mesh_id;
158
  read_dataset(ww_group, "mesh", mesh_id);
10✔
159

160
  if (model::mesh_map.count(mesh_id) == 0) {
10!
161
    fatal_error(
×
162
      fmt::format("Mesh {} used in weight windows does not exist.", mesh_id));
×
163
  }
164
  wws->set_mesh(model::mesh_map[mesh_id]);
10✔
165

166
  wws->lower_ww_ = xt::empty<double>(wws->bounds_size());
10✔
167
  wws->upper_ww_ = xt::empty<double>(wws->bounds_size());
10✔
168

169
  read_dataset<double>(ww_group, "lower_ww_bounds", wws->lower_ww_);
10✔
170
  read_dataset<double>(ww_group, "upper_ww_bounds", wws->upper_ww_);
10✔
171
  read_dataset(ww_group, "survival_ratio", wws->survival_ratio_);
10✔
172
  read_dataset(ww_group, "max_lower_bound_ratio", wws->max_lb_ratio_);
10✔
173
  read_dataset(ww_group, "max_split", wws->max_split_);
10✔
174
  read_dataset(ww_group, "weight_cutoff", wws->weight_cutoff_);
10✔
175

176
  close_group(ww_group);
10✔
177

178
  return wws;
10✔
179
}
10✔
180

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

194
void WeightWindows::allocate_ww_bounds()
513✔
195
{
196
  auto shape = bounds_size();
513✔
197
  if (shape[0] * shape[1] == 0) {
513!
198
    auto msg = fmt::format(
199
      "Size of weight window bounds is zero for WeightWindows {}", id());
×
200
    warning(msg);
×
201
  }
×
202
  lower_ww_ = xt::empty<double>(shape);
513✔
203
  lower_ww_.fill(-1);
513✔
204
  upper_ww_ = xt::empty<double>(shape);
513✔
205
  upper_ww_.fill(-1);
513✔
206
}
513✔
207

208
void WeightWindows::set_id(int32_t id)
460✔
209
{
210
  assert(id >= 0 || id == C_NONE);
366!
211

212
  // Clear entry in mesh map in case one was already assigned
213
  if (id_ != C_NONE) {
460!
214
    variance_reduction::ww_map.erase(id_);
460✔
215
    id_ = C_NONE;
460✔
216
  }
217

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

224
  // If no ID is specified, auto-assign the next ID in the sequence
225
  if (id == C_NONE) {
460✔
226
    id = 0;
223✔
227
    for (const auto& m : variance_reduction::weight_windows) {
243✔
228
      id = std::max(id, m->id_);
20✔
229
    }
230
    ++id;
223✔
231
  }
232

233
  // Update ID and entry in the mesh map
234
  id_ = id;
460✔
235
  variance_reduction::ww_map[id] = index_;
460✔
236
}
460✔
237

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

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

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

260
  mesh_idx_ = mesh_idx;
320✔
261
  model::meshes[mesh_idx_]->prepare_for_point_location();
320✔
262
  allocate_ww_bounds();
320✔
263
}
320✔
264

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

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

275
std::pair<bool, WeightWindow> WeightWindows::get_weight_window(
86,419,269✔
276
  const Particle& p) const
277
{
278
  // check for particle type
279
  if (particle_type_ != p.type()) {
86,419,269✔
280
    return {false, {}};
18,174,617✔
281
  }
282

283
  // particle energy
284
  double E = p.E();
68,244,652✔
285

286
  // check to make sure energy is in range, expects sorted energy values
287
  if (E < energy_bounds_.front() || E > energy_bounds_.back())
68,244,652!
288
    return {false, {}};
84,380✔
289

290
  // Get mesh index for particle's position
291
  const auto& mesh = this->mesh();
68,160,272✔
292
  int mesh_bin = mesh->get_bin(p.r());
68,160,272✔
293

294
  // particle is outside the weight window mesh
295
  if (mesh_bin < 0)
68,160,272✔
296
    return {false, {}};
51,854✔
297

298
  // get the mesh bin in energy group
299
  int energy_bin =
300
    lower_bound_index(energy_bounds_.begin(), energy_bounds_.end(), E);
68,108,418✔
301

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

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

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

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

350
void WeightWindows::set_bounds(const xt::xtensor<double, 2>& lower_bounds,
×
351
  const xt::xtensor<double, 2>& upper_bounds)
352
{
353

354
  this->check_bounds(lower_bounds, upper_bounds);
×
355

356
  // set new weight window values
357
  lower_ww_ = lower_bounds;
×
358
  upper_ww_ = upper_bounds;
×
359
}
×
360

361
void WeightWindows::set_bounds(
×
362
  const xt::xtensor<double, 2>& lower_bounds, double ratio)
363
{
364
  this->check_bounds(lower_bounds);
×
365

366
  // set new weight window values
367
  lower_ww_ = lower_bounds;
×
368
  upper_ww_ = lower_bounds;
×
369
  upper_ww_ *= ratio;
×
370
}
×
371

372
void WeightWindows::set_bounds(
107✔
373
  span<const double> lower_bounds, span<const double> upper_bounds)
374
{
375
  check_bounds(lower_bounds, upper_bounds);
107✔
376
  auto shape = this->bounds_size();
107✔
377
  lower_ww_ = xt::empty<double>(shape);
107✔
378
  upper_ww_ = xt::empty<double>(shape);
107✔
379

380
  // set new weight window values
381
  xt::view(lower_ww_, xt::all()) =
214✔
382
    xt::adapt(lower_bounds.data(), lower_ww_.shape());
321✔
383
  xt::view(upper_ww_, xt::all()) =
214✔
384
    xt::adapt(upper_bounds.data(), upper_ww_.shape());
321✔
385
}
107✔
386

387
void WeightWindows::set_bounds(span<const double> lower_bounds, double ratio)
×
388
{
389
  this->check_bounds(lower_bounds);
×
390

391
  auto shape = this->bounds_size();
×
392
  lower_ww_ = xt::empty<double>(shape);
×
393
  upper_ww_ = xt::empty<double>(shape);
×
394

395
  // set new weight window values
396
  xt::view(lower_ww_, xt::all()) =
×
397
    xt::adapt(lower_bounds.data(), lower_ww_.shape());
×
398
  xt::view(upper_ww_, xt::all()) =
×
399
    xt::adapt(lower_bounds.data(), upper_ww_.shape());
×
400
  upper_ww_ *= ratio;
×
401
}
×
402

403
void WeightWindows::update_weights(const Tally* tally, const std::string& value,
226✔
404
  double threshold, double ratio, WeightWindowUpdateMethod method)
405
{
406
  ///////////////////////////
407
  // Setup and checks
408
  ///////////////////////////
409
  this->check_tally_update_compatibility(tally);
226✔
410

411
  // Dimensions of weight window arrays
412
  int e_bins = lower_ww_.shape()[0];
226✔
413
  int64_t mesh_bins = lower_ww_.shape()[1];
226✔
414

415
  // Initialize weight window arrays to -1.0 by default
416
#pragma omp parallel for collapse(2) schedule(static)
140✔
417
  for (int e = 0; e < e_bins; e++) {
740✔
418
    for (int64_t m = 0; m < mesh_bins; m++) {
943,694✔
419
      lower_ww_(e, m) = -1.0;
943,040✔
420
      upper_ww_(e, m) = -1.0;
943,040✔
421
    }
422
  }
423

424
  // determine which value to use
425
  const std::set<std::string> allowed_values = {"mean", "rel_err"};
1,130✔
426
  if (allowed_values.count(value) == 0) {
226!
427
    fatal_error(fmt::format("Invalid value '{}' specified for weight window "
×
428
                            "generation. Must be one of: 'mean' or 'rel_err'",
429
      value));
430
  }
431

432
  // determine the index of the specified score
433
  int score_index = tally->score_index("flux");
226✔
434
  if (score_index == C_NONE) {
226!
435
    fatal_error(
×
436
      fmt::format("A 'flux' score required for weight window generation "
×
437
                  "is not present on tally {}.",
438
        tally->id()));
×
439
  }
440

441
  ///////////////////////////
442
  // Extract tally data
443
  //
444
  // At the end of this section, the mean and rel_err array
445
  // is a 2D view of tally data (n_e_groups, n_mesh_bins)
446
  //
447
  ///////////////////////////
448

449
  // build a shape for a view of the tally results, this will always be
450
  // dimension 5 (3 filter dimensions, 1 score dimension, 1 results dimension)
451
  // Look for the size of the last dimension of the results array
452
  const auto& results_arr = tally->results();
226✔
453
  const int results_dim = static_cast<int>(results_arr.shape()[2]);
226✔
454
  std::array<int, 5> shape = {1, 1, 1, tally->n_scores(), results_dim};
226✔
455

456
  // set the shape for the filters applied on the tally
457
  for (int i = 0; i < tally->filters().size(); i++) {
864✔
458
    const auto& filter = model::tally_filters[tally->filters(i)];
638✔
459
    shape[i] = filter->n_bins();
638✔
460
  }
461

462
  // build the transpose information to re-order data according to filter type
463
  std::array<int, 5> transpose = {0, 1, 2, 3, 4};
226✔
464

465
  // track our filter types and where we've added new ones
466
  std::vector<FilterType> filter_types = tally->filter_types();
226✔
467

468
  // assign other filter types to dummy positions if needed
469
  if (!tally->has_filter(FilterType::PARTICLE))
226✔
470
    filter_types.push_back(FilterType::PARTICLE);
20✔
471

472
  if (!tally->has_filter(FilterType::ENERGY))
226✔
473
    filter_types.push_back(FilterType::ENERGY);
20✔
474

475
  // particle axis mapping
476
  transpose[0] =
226✔
477
    std::find(filter_types.begin(), filter_types.end(), FilterType::PARTICLE) -
226✔
478
    filter_types.begin();
226✔
479

480
  // energy axis mapping
481
  transpose[1] =
226✔
482
    std::find(filter_types.begin(), filter_types.end(), FilterType::ENERGY) -
226✔
483
    filter_types.begin();
226✔
484

485
  // mesh axis mapping
486
  transpose[2] =
226✔
487
    std::find(filter_types.begin(), filter_types.end(), FilterType::MESH) -
226✔
488
    filter_types.begin();
226✔
489

490
  // get a fully reshaped view of the tally according to tally ordering of
491
  // filters
492
  auto tally_values = xt::reshape_view(results_arr, shape);
226✔
493

494
  // get a that is (particle, energy, mesh, scores, values)
495
  auto transposed_view = xt::transpose(tally_values, transpose);
226✔
496

497
  // determine the dimension and index of the particle data
498
  int particle_idx = 0;
226✔
499
  if (tally->has_filter(FilterType::PARTICLE)) {
226✔
500
    // get the particle filter
501
    auto pf = tally->get_filter<ParticleFilter>();
206✔
502
    const auto& particles = pf->particles();
206✔
503

504
    // find the index of the particle that matches these weight windows
505
    auto p_it =
506
      std::find(particles.begin(), particles.end(), this->particle_type_);
206✔
507
    // if the particle filter doesn't have particle data for the particle
508
    // used on this weight windows instance, report an error
509
    if (p_it == particles.end()) {
206!
510
      auto msg = fmt::format("Particle type '{}' not present on Filter {} for "
511
                             "Tally {} used to update WeightWindows {}",
512
        this->particle_type_.str(), pf->id(), tally->id(), this->id());
×
513
      fatal_error(msg);
×
514
    }
×
515

516
    // use the index of the particle in the filter to down-select data later
517
    particle_idx = p_it - particles.begin();
206✔
518
  }
519

520
  // down-select data based on particle and score
521
  auto sum = xt::dynamic_view(
1,130✔
522
    transposed_view, {particle_idx, xt::all(), xt::all(), score_index,
452✔
523
                       static_cast<int>(TallyResult::SUM)});
904✔
524
  auto sum_sq = xt::dynamic_view(
1,130✔
525
    transposed_view, {particle_idx, xt::all(), xt::all(), score_index,
452✔
526
                       static_cast<int>(TallyResult::SUM_SQ)});
904✔
527
  int n = tally->n_realizations_;
226✔
528

529
  //////////////////////////////////////////////
530
  //
531
  // Assign new weight windows
532
  //
533
  // Use references to the existing weight window data
534
  // to store and update the values
535
  //
536
  //////////////////////////////////////////////
537

538
  // up to this point the data arrays are views into the tally results (no
539
  // computation has been performed) now we'll switch references to the tally's
540
  // bounds to avoid allocating additional memory
541
  auto& new_bounds = this->lower_ww_;
226✔
542
  auto& rel_err = this->upper_ww_;
226✔
543

544
  // get mesh volumes
545
  auto mesh_vols = this->mesh()->volumes();
226✔
546

547
  // Calculate mean (new_bounds) and relative error
548
#pragma omp parallel for collapse(2) schedule(static)
140✔
549
  for (int e = 0; e < e_bins; e++) {
740✔
550
    for (int64_t m = 0; m < mesh_bins; m++) {
943,694✔
551
      // Calculate mean
552
      new_bounds(e, m) = sum(e, m) / n;
943,040✔
553
      // Calculate relative error
554
      if (sum(e, m) > 0.0) {
943,040✔
555
        double mean_val = new_bounds(e, m);
81,184✔
556
        double variance = (sum_sq(e, m) / n - mean_val * mean_val) / (n - 1);
81,184✔
557
        rel_err(e, m) = std::sqrt(variance) / mean_val;
81,184✔
558
      } else {
559
        rel_err(e, m) = INFTY;
861,856✔
560
      }
561
      if (value == "rel_err") {
943,040✔
562
        new_bounds(e, m) = 1.0 / rel_err(e, m);
276,000✔
563
      }
564
    }
565
  }
566

567
  // Divide by volume of mesh elements
568
#pragma omp parallel for collapse(2) schedule(static)
140✔
569
  for (int e = 0; e < e_bins; e++) {
740✔
570
    for (int64_t m = 0; m < mesh_bins; m++) {
943,694✔
571
      new_bounds(e, m) /= mesh_vols[m];
943,040✔
572
    }
573
  }
574

575
  if (method == WeightWindowUpdateMethod::MAGIC) {
226✔
576
    // For MAGIC, weight windows are proportional to the forward fluxes.
577
    // We normalize weight windows independently for each energy group.
578

579
    // Find group maximum and normalize (per energy group)
580
    for (int e = 0; e < e_bins; e++) {
1,700✔
581
      double group_max = 0.0;
1,560✔
582

583
      // Find maximum value across all elements in this energy group
584
#pragma omp parallel for schedule(static) reduction(max : group_max)
936✔
585
      for (int64_t m = 0; m < mesh_bins; m++) {
874,004✔
586
        if (new_bounds(e, m) > group_max) {
873,380✔
587
          group_max = new_bounds(e, m);
2,016✔
588
        }
589
      }
590

591
      // Normalize values in this energy group by the maximum value
592
      if (group_max > 0.0) {
1,560✔
593
        double norm_factor = 1.0 / (2.0 * group_max);
1,530✔
594
#pragma omp parallel for schedule(static)
918✔
595
        for (int64_t m = 0; m < mesh_bins; m++) {
873,272✔
596
          new_bounds(e, m) *= norm_factor;
872,660✔
597
        }
598
      }
599
    }
600
  } else {
601
    // For FW-CADIS, weight windows are inversely proportional to the adjoint
602
    // fluxes. We normalize the weight windows across all energy groups.
603
#pragma omp parallel for collapse(2) schedule(static)
56✔
604
    for (int e = 0; e < e_bins; e++) {
60✔
605
      for (int64_t m = 0; m < mesh_bins; m++) {
69,690✔
606
        // Take the inverse, but are careful not to divide by zero
607
        if (new_bounds(e, m) != 0.0) {
69,660✔
608
          new_bounds(e, m) = 1.0 / new_bounds(e, m);
55,728✔
609
        } else {
610
          new_bounds(e, m) = 0.0;
13,932!
611
        }
612
      }
613
    }
614

615
    // Find the maximum value across all elements
616
    double max_val = 0.0;
86✔
617
#pragma omp parallel for collapse(2) schedule(static) reduction(max : max_val)
56✔
618
    for (int e = 0; e < e_bins; e++) {
60✔
619
      for (int64_t m = 0; m < mesh_bins; m++) {
69,690✔
620
        if (new_bounds(e, m) > max_val) {
69,660✔
621
          max_val = new_bounds(e, m);
324✔
622
        }
623
      }
624
    }
625

626
    // Parallel normalization
627
    if (max_val > 0.0) {
86✔
628
      double norm_factor = 1.0 / (2.0 * max_val);
62✔
629
#pragma omp parallel for collapse(2) schedule(static)
38✔
630
      for (int e = 0; e < e_bins; e++) {
48✔
631
        for (int64_t m = 0; m < mesh_bins; m++) {
55,752✔
632
          new_bounds(e, m) *= norm_factor;
55,728✔
633
        }
634
      }
635
    }
636
  }
637

638
  // Final processing
639
#pragma omp parallel for collapse(2) schedule(static)
140✔
640
  for (int e = 0; e < e_bins; e++) {
740✔
641
    for (int64_t m = 0; m < mesh_bins; m++) {
943,694✔
642
      // Values where the mean is zero should be ignored
643
      if (sum(e, m) <= 0.0) {
943,040✔
644
        new_bounds(e, m) = -1.0;
861,856✔
645
      }
646
      // Values where the relative error is higher than the threshold should be
647
      // ignored
648
      else if (rel_err(e, m) > threshold) {
81,184✔
649
        new_bounds(e, m) = -1.0;
1,136✔
650
      }
651
      // Set the upper bounds
652
      upper_ww_(e, m) = ratio * lower_ww_(e, m);
943,040✔
653
    }
654
  }
655
}
226✔
656

657
void WeightWindows::check_tally_update_compatibility(const Tally* tally)
226✔
658
{
659
  // define the set of allowed filters for the tally
660
  const std::set<FilterType> allowed_filters = {
661
    FilterType::MESH, FilterType::ENERGY, FilterType::PARTICLE};
226✔
662

663
  // retrieve a mapping of filter type to filter index for the tally
664
  auto filter_indices = tally->filter_indices();
226✔
665

666
  // a mesh filter is required for a tally used to update weight windows
667
  if (!filter_indices.count(FilterType::MESH)) {
226!
668
    fatal_error(
×
669
      "A mesh filter is required for a tally to update weight window bounds");
670
  }
671

672
  // ensure the mesh filter is using the same mesh as this weight window object
673
  auto mesh_filter = tally->get_filter<MeshFilter>();
226✔
674

675
  // make sure that all of the filters present on the tally are allowed
676
  for (auto filter_pair : filter_indices) {
864✔
677
    if (allowed_filters.find(filter_pair.first) == allowed_filters.end()) {
638!
678
      fatal_error(fmt::format("Invalid filter type '{}' found on tally "
×
679
                              "used for weight window generation.",
680
        model::tally_filters[tally->filters(filter_pair.second)]->type_str()));
×
681
    }
682
  }
683

684
  if (mesh_filter->mesh() != mesh_idx_) {
226!
685
    int32_t mesh_filter_id = model::meshes[mesh_filter->mesh()]->id();
×
686
    int32_t ww_mesh_id = model::meshes[this->mesh_idx_]->id();
×
687
    fatal_error(fmt::format("Mesh filter {} uses a different mesh ({}) than "
×
688
                            "weight window {} mesh ({})",
689
      mesh_filter->id(), mesh_filter_id, id_, ww_mesh_id));
×
690
  }
691

692
  // if an energy filter exists, make sure the energy grid matches that of this
693
  // weight window object
694
  if (auto energy_filter = tally->get_filter<EnergyFilter>()) {
226✔
695
    std::vector<double> filter_bins = energy_filter->bins();
206✔
696
    std::set<double> filter_e_bounds(
697
      energy_filter->bins().begin(), energy_filter->bins().end());
206✔
698
    if (filter_e_bounds.size() != energy_bounds().size()) {
206!
699
      fatal_error(
×
700
        fmt::format("Energy filter {} does not have the same number of energy "
×
701
                    "bounds ({}) as weight window object {} ({})",
702
          energy_filter->id(), filter_e_bounds.size(), id_,
×
703
          energy_bounds().size()));
×
704
    }
705

706
    for (auto e : energy_bounds()) {
2,038✔
707
      if (filter_e_bounds.count(e) == 0) {
1,832!
708
        fatal_error(fmt::format(
×
709
          "Energy bounds of filter {} and weight windows {} do not match",
710
          energy_filter->id(), id_));
×
711
      }
712
    }
713
  }
206✔
714
}
226✔
715

716
void WeightWindows::to_hdf5(hid_t group) const
122✔
717
{
718
  hid_t ww_group = create_group(group, fmt::format("weight_windows_{}", id()));
244✔
719

720
  write_dataset(ww_group, "mesh", this->mesh()->id());
122✔
721
  write_dataset(ww_group, "particle_type", particle_type_.str());
122✔
722
  write_dataset(ww_group, "energy_bounds", energy_bounds_);
122✔
723
  write_dataset(ww_group, "lower_ww_bounds", lower_ww_);
122✔
724
  write_dataset(ww_group, "upper_ww_bounds", upper_ww_);
122✔
725
  write_dataset(ww_group, "survival_ratio", survival_ratio_);
122✔
726
  write_dataset(ww_group, "max_lower_bound_ratio", max_lb_ratio_);
122✔
727
  write_dataset(ww_group, "max_split", max_split_);
122✔
728
  write_dataset(ww_group, "weight_cutoff", weight_cutoff_);
122✔
729

730
  close_group(ww_group);
122✔
731
}
122✔
732

733
WeightWindowsGenerator::WeightWindowsGenerator(pugi::xml_node node)
73✔
734
{
735
  // read information from the XML node
736
  int32_t mesh_id = std::stoi(get_node_value(node, "mesh"));
73✔
737
  int32_t mesh_idx = model::mesh_map[mesh_id];
73✔
738
  max_realizations_ = std::stoi(get_node_value(node, "max_realizations"));
73✔
739

740
  int32_t active_batches = settings::n_batches - settings::n_inactive;
73✔
741
  if (max_realizations_ > active_batches) {
73✔
742
    auto msg =
743
      fmt::format("The maximum number of specified tally realizations ({}) is "
744
                  "greater than the number of active batches ({}).",
745
        max_realizations_, active_batches);
27✔
746
    warning(msg);
15✔
747
  }
15✔
748
  auto tmp_str = get_node_value(node, "particle_type", false, true);
73✔
749
  auto particle_type = ParticleType {tmp_str};
73✔
750

751
  update_interval_ = std::stoi(get_node_value(node, "update_interval"));
73✔
752
  on_the_fly_ = get_node_value_bool(node, "on_the_fly");
73✔
753

754
  std::vector<double> e_bounds;
73✔
755
  if (check_for_node(node, "energy_bounds")) {
73✔
756
    e_bounds = get_node_array<double>(node, "energy_bounds");
21✔
757
  } else {
758
    int p_type = particle_type.transport_index();
52✔
759
    if (p_type == C_NONE) {
52!
760
      fatal_error("Weight windows particle is not supported for transport.");
×
761
    }
762
    e_bounds.push_back(data::energy_min[p_type]);
52✔
763
    e_bounds.push_back(data::energy_max[p_type]);
52✔
764
  }
765

766
  // set method
767
  std::string method_string = get_node_value(node, "method");
73✔
768
  if (method_string == "magic") {
73✔
769
    method_ = WeightWindowUpdateMethod::MAGIC;
30✔
770
    if (settings::solver_type == SolverType::RANDOM_RAY &&
30!
771
        FlatSourceDomain::adjoint_) {
772
      fatal_error("Random ray weight window generation with MAGIC cannot be "
×
773
                  "done in adjoint mode.");
774
    }
775
  } else if (method_string == "fw_cadis") {
43!
776
    method_ = WeightWindowUpdateMethod::FW_CADIS;
43✔
777
    if (settings::solver_type != SolverType::RANDOM_RAY) {
43!
778
      fatal_error("FW-CADIS can only be run in random ray solver mode.");
×
779
    }
780
    FlatSourceDomain::adjoint_ = true;
43✔
781
  } else {
782
    fatal_error(fmt::format(
×
783
      "Unknown weight window update method '{}' specified", method_string));
784
  }
785

786
  // parse non-default update parameters if specified
787
  if (check_for_node(node, "update_parameters")) {
73✔
788
    pugi::xml_node params_node = node.child("update_parameters");
20✔
789
    if (check_for_node(params_node, "value"))
20!
790
      tally_value_ = get_node_value(params_node, "value");
20✔
791
    if (check_for_node(params_node, "threshold"))
20!
792
      threshold_ = std::stod(get_node_value(params_node, "threshold"));
20✔
793
    if (check_for_node(params_node, "ratio")) {
20!
794
      ratio_ = std::stod(get_node_value(params_node, "ratio"));
20✔
795
    }
796
  }
797

798
  // check update parameter values
799
  if (tally_value_ != "mean" && tally_value_ != "rel_err") {
73!
800
    fatal_error(fmt::format("Unsupported tally value '{}' specified for "
×
801
                            "weight window generation.",
802
      tally_value_));
×
803
  }
804
  if (threshold_ <= 0.0)
73!
805
    fatal_error(fmt::format("Invalid relative error threshold '{}' (<= 0.0) "
×
806
                            "specified for weight window generation",
807
      ratio_));
×
808
  if (ratio_ <= 1.0)
73!
809
    fatal_error(fmt::format("Invalid weight window ratio '{}' (<= 1.0) "
×
810
                            "specified for weight window generation"));
811

812
  // create a matching weight windows object
813
  auto wws = WeightWindows::create();
73✔
814
  ww_idx_ = wws->index();
73✔
815
  wws->set_mesh(mesh_idx);
73✔
816
  if (e_bounds.size() > 0)
73!
817
    wws->set_energy_bounds(e_bounds);
73✔
818
  wws->set_particle_type(particle_type);
73✔
819
  wws->set_defaults();
73✔
820
}
73✔
821

822
void WeightWindowsGenerator::create_tally()
73✔
823
{
824
  const auto& wws = variance_reduction::weight_windows[ww_idx_];
73✔
825

826
  // create a tally based on the WWG information
827
  Tally* ww_tally = Tally::create();
73✔
828
  tally_idx_ = model::tally_map[ww_tally->id()];
73✔
829
  ww_tally->set_scores({"flux"});
146!
830

831
  int32_t mesh_id = wws->mesh()->id();
73✔
832
  int32_t mesh_idx = model::mesh_map.at(mesh_id);
73✔
833
  // see if there's already a mesh filter using this mesh
834
  bool found_mesh_filter = false;
73✔
835
  for (const auto& f : model::tally_filters) {
229✔
836
    if (f->type() == FilterType::MESH) {
166✔
837
      const auto* mesh_filter = dynamic_cast<MeshFilter*>(f.get());
10!
838
      if (mesh_filter->mesh() == mesh_idx && !mesh_filter->translated() &&
20!
839
          !mesh_filter->rotated()) {
10!
840
        ww_tally->add_filter(f.get());
10✔
841
        found_mesh_filter = true;
10✔
842
        break;
10✔
843
      }
844
    }
845
  }
846

847
  if (!found_mesh_filter) {
73✔
848
    auto mesh_filter = Filter::create("mesh");
63✔
849
    openmc_mesh_filter_set_mesh(mesh_filter->index(), model::mesh_map[mesh_id]);
63✔
850
    ww_tally->add_filter(mesh_filter);
63✔
851
  }
852

853
  const auto& e_bounds = wws->energy_bounds();
73✔
854
  if (e_bounds.size() > 0) {
73!
855
    auto energy_filter = Filter::create("energy");
73✔
856
    openmc_energy_filter_set_bins(
146✔
857
      energy_filter->index(), e_bounds.size(), e_bounds.data());
73✔
858
    ww_tally->add_filter(energy_filter);
73✔
859
  }
860

861
  // add a particle filter
862
  auto particle_type = wws->particle_type();
73✔
863
  auto particle_filter = Filter::create("particle");
73✔
864
  auto pf = dynamic_cast<ParticleFilter*>(particle_filter);
73!
865
  pf->set_particles({&particle_type, 1});
73✔
866
  ww_tally->add_filter(particle_filter);
73✔
867
}
73✔
868

869
void WeightWindowsGenerator::update() const
2,122✔
870
{
871
  const auto& wws = variance_reduction::weight_windows[ww_idx_];
2,122✔
872

873
  Tally* tally = model::tallies[tally_idx_].get();
2,122✔
874

875
  // If in random ray mode, only update on the last batch
876
  if (settings::solver_type == SolverType::RANDOM_RAY) {
2,122✔
877
    if (simulation::current_batch != settings::n_batches) {
1,972✔
878
      return;
1,886✔
879
    }
880
    // If in Monte Carlo mode and beyond the number of max realizations or
881
    // not at the correct update interval, skip the update
882
  } else if (max_realizations_ < tally->n_realizations_ ||
150✔
883
             tally->n_realizations_ % update_interval_ != 0) {
30!
884
    return;
120✔
885
  }
886

887
  wws->update_weights(tally, tally_value_, threshold_, ratio_, method_);
116✔
888

889
  // if we're not doing on the fly generation, reset the tally results once
890
  // we're done with the update
891
  if (!on_the_fly_)
116!
892
    tally->reset();
×
893

894
  // TODO: deactivate or remove tally once weight window generation is
895
  // complete
896
}
897

898
//==============================================================================
899
// Non-member functions
900
//==============================================================================
901

902
std::pair<bool, WeightWindow> search_weight_window(const Particle& p)
79,858,889✔
903
{
904
  // TODO: this is a linear search - should do something more clever
905
  for (const auto& ww : variance_reduction::weight_windows) {
98,169,740✔
906
    auto [ww_found, weight_window] = ww->get_weight_window(p);
86,419,269✔
907
    if (ww_found)
86,419,269✔
908
      return {true, weight_window};
68,108,418✔
909
  }
910
  return {false, {}};
11,750,471✔
911
}
912

913
void apply_weight_windows(Particle& p)
153,385,502✔
914
{
915
  if (!settings::weight_windows_on)
153,385,502✔
916
    return;
152,641,181✔
917

918
  // WW on photon and neutron only
919
  if (!p.type().is_neutron() && !p.type().is_photon())
748,358!
NEW
920
    return;
×
921

922
  // skip dead or no energy
923
  if (p.E() <= 0 || !p.alive())
748,358!
924
    return;
4,037✔
925

926
  auto [ww_found, ww] = search_weight_window(p);
744,321✔
927
  if (ww_found && ww.is_valid()) {
744,321✔
928
    apply_weight_window(p, ww);
646,800✔
929
  } else {
930
    if (p.wgt_ww_born() == -1.0)
97,521✔
931
      p.wgt_ww_born() = 1.0;
45,820✔
932
  }
933
}
934

935
void apply_weight_window(Particle& p, WeightWindow weight_window)
79,699,245✔
936
{
937
  if (!weight_window.is_valid())
79,699,245✔
938
    return;
23,168,070✔
939

940
  // skip dead or no energy
941
  if (p.E() <= 0 || !p.alive())
56,531,175✔
942
    return;
3,277,940✔
943

944
  // If particle has not yet had its birth weight window value set, set it to
945
  // the current weight window.
946
  if (p.wgt_ww_born() == -1.0)
53,253,235✔
947
    p.wgt_ww_born() =
633,460✔
948
      (weight_window.lower_weight + weight_window.upper_weight) / 2;
633,460✔
949

950
  // Normalize weight windows based on particle's starting weight
951
  // and the value of the weight window the particle was born in.
952
  weight_window.scale(p.wgt_born() / p.wgt_ww_born());
53,253,235✔
953

954
  // get the paramters
955
  double weight = p.wgt();
53,253,235✔
956

957
  // first check to see if particle should be killed for weight cutoff
958
  if (p.wgt() < weight_window.weight_cutoff) {
53,253,235✔
959
    p.wgt() = 0.0;
270✔
960
    return;
270✔
961
  }
962

963
  // check if particle is far above current weight window
964
  // only do this if the factor is not already set on the particle and a
965
  // maximum lower bound ratio is specified
966
  if (p.ww_factor() == 0.0 && weight_window.max_lb_ratio > 1.0 &&
53,262,965✔
967
      p.wgt() > weight_window.lower_weight * weight_window.max_lb_ratio) {
10,000!
968
    p.ww_factor() =
10,000✔
969
      p.wgt() / (weight_window.lower_weight * weight_window.max_lb_ratio);
10,000✔
970
  }
971

972
  // move weight window closer to the particle weight if needed
973
  if (p.ww_factor() > 1.0)
53,252,965✔
974
    weight_window.scale(p.ww_factor());
2,903,310✔
975

976
  // if particle's weight is above the weight window split until they are within
977
  // the window
978
  if (weight > weight_window.upper_weight) {
53,252,965✔
979
    // do not further split the particle if above the limit
980
    if (p.n_split() >= settings::max_history_splits)
11,175,293✔
981
      return;
9,888,416✔
982

983
    double n_split = std::ceil(weight / weight_window.upper_weight);
1,286,877✔
984
    double max_split = weight_window.max_split;
1,286,877✔
985
    n_split = std::min(n_split, max_split);
1,286,877✔
986

987
    p.n_split() += n_split;
1,286,877✔
988

989
    // Create secondaries and divide weight among all particles
990
    int i_split = std::round(n_split);
1,286,877✔
991
    for (int l = 0; l < i_split - 1; l++) {
5,150,946✔
992
      p.split(weight / n_split);
3,864,069✔
993
    }
994
    // remaining weight is applied to current particle
995
    p.wgt() = weight / n_split;
1,286,877✔
996

997
  } else if (weight <= weight_window.lower_weight) {
42,077,672✔
998
    // if the particle weight is below the window, play Russian roulette
999
    double weight_survive =
1000
      std::min(weight * weight_window.max_split, weight_window.survival_weight);
1,329,289✔
1001
    russian_roulette(p, weight_survive);
1,329,289✔
1002
  } // else particle is in the window, continue as normal
1003
}
1004

1005
void free_memory_weight_windows()
7,519✔
1006
{
1007
  variance_reduction::ww_map.clear();
7,519✔
1008
  variance_reduction::weight_windows.clear();
7,519✔
1009
}
7,519✔
1010

1011
void finalize_variance_reduction()
7,374✔
1012
{
1013
  for (const auto& wwg : variance_reduction::weight_windows_generators) {
7,447✔
1014
    wwg->create_tally();
73✔
1015
  }
1016
}
7,374✔
1017

1018
//==============================================================================
1019
// C API
1020
//==============================================================================
1021

1022
int verify_ww_index(int32_t index)
1,810✔
1023
{
1024
  if (index < 0 || index >= variance_reduction::weight_windows.size()) {
1,810!
1025
    set_errmsg(fmt::format("Index '{}' for weight windows is invalid", index));
×
1026
    return OPENMC_E_OUT_OF_BOUNDS;
×
1027
  }
1028
  return 0;
1,810✔
1029
}
1030

1031
extern "C" int openmc_get_weight_windows_index(int32_t id, int32_t* idx)
150✔
1032
{
1033
  auto it = variance_reduction::ww_map.find(id);
150✔
1034
  if (it == variance_reduction::ww_map.end()) {
150!
1035
    set_errmsg(fmt::format("No weight windows exist with ID={}", id));
×
1036
    return OPENMC_E_INVALID_ID;
×
1037
  }
1038

1039
  *idx = it->second;
150✔
1040
  return 0;
150✔
1041
}
1042

1043
extern "C" int openmc_weight_windows_get_id(int32_t index, int32_t* id)
470✔
1044
{
1045
  if (int err = verify_ww_index(index))
470!
1046
    return err;
×
1047

1048
  const auto& wws = variance_reduction::weight_windows.at(index);
470✔
1049
  *id = wws->id();
470✔
1050
  return 0;
470✔
1051
}
1052

1053
extern "C" int openmc_weight_windows_set_id(int32_t index, int32_t id)
140✔
1054
{
1055
  if (int err = verify_ww_index(index))
140!
1056
    return err;
×
1057

1058
  const auto& wws = variance_reduction::weight_windows.at(index);
140✔
1059
  wws->set_id(id);
140✔
1060
  return 0;
140✔
1061
}
1062

1063
extern "C" int openmc_weight_windows_update_magic(int32_t ww_idx,
110✔
1064
  int32_t tally_idx, const char* value, double threshold, double ratio)
1065
{
1066
  if (int err = verify_ww_index(ww_idx))
110!
1067
    return err;
×
1068

1069
  if (tally_idx < 0 || tally_idx >= model::tallies.size()) {
110!
1070
    set_errmsg(fmt::format("Index '{}' for tally is invalid", tally_idx));
×
1071
    return OPENMC_E_OUT_OF_BOUNDS;
×
1072
  }
1073

1074
  // get the requested tally
1075
  const Tally* tally = model::tallies.at(tally_idx).get();
110✔
1076

1077
  // get the WeightWindows object
1078
  const auto& wws = variance_reduction::weight_windows.at(ww_idx);
110✔
1079

1080
  wws->update_weights(tally, value, threshold, ratio);
110✔
1081

1082
  return 0;
110✔
1083
}
1084

1085
extern "C" int openmc_weight_windows_set_mesh(int32_t ww_idx, int32_t mesh_idx)
140✔
1086
{
1087
  if (int err = verify_ww_index(ww_idx))
140!
1088
    return err;
×
1089
  const auto& wws = variance_reduction::weight_windows.at(ww_idx);
140✔
1090
  wws->set_mesh(mesh_idx);
140✔
1091
  return 0;
140✔
1092
}
1093

1094
extern "C" int openmc_weight_windows_get_mesh(int32_t ww_idx, int32_t* mesh_idx)
10✔
1095
{
1096
  if (int err = verify_ww_index(ww_idx))
10!
1097
    return err;
×
1098
  const auto& wws = variance_reduction::weight_windows.at(ww_idx);
10✔
1099
  *mesh_idx = model::mesh_map.at(wws->mesh()->id());
10✔
1100
  return 0;
10✔
1101
}
1102

1103
extern "C" int openmc_weight_windows_set_energy_bounds(
120✔
1104
  int32_t ww_idx, double* e_bounds, size_t e_bounds_size)
1105
{
1106
  if (int err = verify_ww_index(ww_idx))
120!
1107
    return err;
×
1108
  const auto& wws = variance_reduction::weight_windows.at(ww_idx);
120✔
1109
  wws->set_energy_bounds({e_bounds, e_bounds_size});
120✔
1110
  return 0;
120✔
1111
}
1112

1113
extern "C" int openmc_weight_windows_get_energy_bounds(
10✔
1114
  int32_t ww_idx, const double** e_bounds, size_t* e_bounds_size)
1115
{
1116
  if (int err = verify_ww_index(ww_idx))
10!
1117
    return err;
×
1118
  const auto& wws = variance_reduction::weight_windows[ww_idx].get();
10✔
1119
  *e_bounds = wws->energy_bounds().data();
10✔
1120
  *e_bounds_size = wws->energy_bounds().size();
10✔
1121
  return 0;
10✔
1122
}
1123

1124
extern "C" int openmc_weight_windows_set_particle(
160✔
1125
  int32_t index, int32_t particle)
1126
{
1127
  if (int err = verify_ww_index(index))
160!
1128
    return err;
×
1129

1130
  const auto& wws = variance_reduction::weight_windows.at(index);
160✔
1131
  wws->set_particle_type(ParticleType {particle});
160✔
1132
  return 0;
160✔
1133
}
1134

1135
extern "C" int openmc_weight_windows_get_particle(
40✔
1136
  int32_t index, int32_t* particle)
1137
{
1138
  if (int err = verify_ww_index(index))
40!
1139
    return err;
×
1140

1141
  const auto& wws = variance_reduction::weight_windows.at(index);
40✔
1142
  *particle = wws->particle_type().pdg_number();
40✔
1143
  return 0;
40✔
1144
}
1145

1146
extern "C" int openmc_weight_windows_get_bounds(int32_t index,
440✔
1147
  const double** lower_bounds, const double** upper_bounds, size_t* size)
1148
{
1149
  if (int err = verify_ww_index(index))
440!
1150
    return err;
×
1151

1152
  const auto& wws = variance_reduction::weight_windows[index];
440✔
1153
  *size = wws->lower_ww_bounds().size();
440✔
1154
  *lower_bounds = wws->lower_ww_bounds().data();
440✔
1155
  *upper_bounds = wws->upper_ww_bounds().data();
440✔
1156
  return 0;
440✔
1157
}
1158

1159
extern "C" int openmc_weight_windows_set_bounds(int32_t index,
10✔
1160
  const double* lower_bounds, const double* upper_bounds, size_t size)
1161
{
1162
  if (int err = verify_ww_index(index))
10!
1163
    return err;
×
1164

1165
  const auto& wws = variance_reduction::weight_windows[index];
10✔
1166
  wws->set_bounds({lower_bounds, size}, {upper_bounds, size});
10✔
1167
  return 0;
10✔
1168
}
1169

1170
extern "C" int openmc_weight_windows_get_survival_ratio(
30✔
1171
  int32_t index, double* ratio)
1172
{
1173
  if (int err = verify_ww_index(index))
30!
1174
    return err;
×
1175
  const auto& wws = variance_reduction::weight_windows[index];
30✔
1176
  *ratio = wws->survival_ratio();
30✔
1177
  return 0;
30✔
1178
}
1179

1180
extern "C" int openmc_weight_windows_set_survival_ratio(
10✔
1181
  int32_t index, double ratio)
1182
{
1183
  if (int err = verify_ww_index(index))
10!
1184
    return err;
×
1185
  const auto& wws = variance_reduction::weight_windows[index];
10✔
1186
  wws->survival_ratio() = ratio;
10✔
1187
  std::cout << "Survival ratio: " << wws->survival_ratio() << std::endl;
10✔
1188
  return 0;
10✔
1189
}
1190

1191
extern "C" int openmc_weight_windows_get_max_lower_bound_ratio(
30✔
1192
  int32_t index, double* lb_ratio)
1193
{
1194
  if (int err = verify_ww_index(index))
30!
1195
    return err;
×
1196
  const auto& wws = variance_reduction::weight_windows[index];
30✔
1197
  *lb_ratio = wws->max_lower_bound_ratio();
30✔
1198
  return 0;
30✔
1199
}
1200

1201
extern "C" int openmc_weight_windows_set_max_lower_bound_ratio(
10✔
1202
  int32_t index, double lb_ratio)
1203
{
1204
  if (int err = verify_ww_index(index))
10!
1205
    return err;
×
1206
  const auto& wws = variance_reduction::weight_windows[index];
10✔
1207
  wws->max_lower_bound_ratio() = lb_ratio;
10✔
1208
  return 0;
10✔
1209
}
1210

1211
extern "C" int openmc_weight_windows_get_weight_cutoff(
30✔
1212
  int32_t index, double* cutoff)
1213
{
1214
  if (int err = verify_ww_index(index))
30!
1215
    return err;
×
1216
  const auto& wws = variance_reduction::weight_windows[index];
30✔
1217
  *cutoff = wws->weight_cutoff();
30✔
1218
  return 0;
30✔
1219
}
1220

1221
extern "C" int openmc_weight_windows_set_weight_cutoff(
10✔
1222
  int32_t index, double cutoff)
1223
{
1224
  if (int err = verify_ww_index(index))
10!
1225
    return err;
×
1226
  const auto& wws = variance_reduction::weight_windows[index];
10✔
1227
  wws->weight_cutoff() = cutoff;
10✔
1228
  return 0;
10✔
1229
}
1230

1231
extern "C" int openmc_weight_windows_get_max_split(
30✔
1232
  int32_t index, int* max_split)
1233
{
1234
  if (int err = verify_ww_index(index))
30!
1235
    return err;
×
1236
  const auto& wws = variance_reduction::weight_windows[index];
30✔
1237
  *max_split = wws->max_split();
30✔
1238
  return 0;
30✔
1239
}
1240

1241
extern "C" int openmc_weight_windows_set_max_split(int32_t index, int max_split)
10✔
1242
{
1243
  if (int err = verify_ww_index(index))
10!
1244
    return err;
×
1245
  const auto& wws = variance_reduction::weight_windows[index];
10✔
1246
  wws->max_split() = max_split;
10✔
1247
  return 0;
10✔
1248
}
1249

1250
extern "C" int openmc_extend_weight_windows(
140✔
1251
  int32_t n, int32_t* index_start, int32_t* index_end)
1252
{
1253
  if (index_start)
140!
1254
    *index_start = variance_reduction::weight_windows.size();
140✔
1255
  if (index_end)
140!
1256
    *index_end = variance_reduction::weight_windows.size() + n - 1;
×
1257
  for (int i = 0; i < n; ++i)
280✔
1258
    variance_reduction::weight_windows.push_back(make_unique<WeightWindows>());
140✔
1259
  return 0;
140✔
1260
}
1261

1262
extern "C" size_t openmc_weight_windows_size()
140✔
1263
{
1264
  return variance_reduction::weight_windows.size();
140✔
1265
}
1266

1267
extern "C" int openmc_weight_windows_export(const char* filename)
146✔
1268
{
1269

1270
  if (!mpi::master)
146✔
1271
    return 0;
24✔
1272

1273
  std::string name = filename ? filename : "weight_windows.h5";
244✔
1274

1275
  write_message(fmt::format("Exporting weight windows to {}...", name), 5);
122✔
1276

1277
  hid_t ww_file = file_open(name, 'w');
122✔
1278

1279
  // Write file type
1280
  write_attribute(ww_file, "filetype", "weight_windows");
122✔
1281

1282
  // Write revisiion number for state point file
1283
  write_attribute(ww_file, "version", VERSION_WEIGHT_WINDOWS);
122✔
1284

1285
  hid_t weight_windows_group = create_group(ww_file, "weight_windows");
122✔
1286

1287
  hid_t mesh_group = create_group(ww_file, "meshes");
122✔
1288

1289
  std::vector<int32_t> mesh_ids;
122✔
1290
  std::vector<int32_t> ww_ids;
122✔
1291
  for (const auto& ww : variance_reduction::weight_windows) {
244✔
1292

1293
    ww->to_hdf5(weight_windows_group);
122✔
1294
    ww_ids.push_back(ww->id());
122✔
1295

1296
    // if the mesh has already been written, move on
1297
    int32_t mesh_id = ww->mesh()->id();
122✔
1298
    if (std::find(mesh_ids.begin(), mesh_ids.end(), mesh_id) != mesh_ids.end())
122!
1299
      continue;
×
1300

1301
    mesh_ids.push_back(mesh_id);
122✔
1302
    ww->mesh()->to_hdf5(mesh_group);
122✔
1303
  }
1304

1305
  write_attribute(mesh_group, "n_meshes", mesh_ids.size());
122✔
1306
  write_attribute(mesh_group, "ids", mesh_ids);
122✔
1307
  close_group(mesh_group);
122✔
1308

1309
  write_attribute(weight_windows_group, "n_weight_windows", ww_ids.size());
122✔
1310
  write_attribute(weight_windows_group, "ids", ww_ids);
122✔
1311
  close_group(weight_windows_group);
122✔
1312

1313
  file_close(ww_file);
122✔
1314

1315
  return 0;
122✔
1316
}
122✔
1317

1318
extern "C" int openmc_weight_windows_import(const char* filename)
10✔
1319
{
1320
  std::string name = filename ? filename : "weight_windows.h5";
10!
1321

1322
  if (mpi::master)
10!
1323
    write_message(fmt::format("Importing weight windows from {}...", name), 5);
10✔
1324

1325
  if (!file_exists(name)) {
10!
1326
    set_errmsg(fmt::format("File '{}' does not exist", name));
×
1327
  }
1328

1329
  hid_t ww_file = file_open(name, 'r');
10✔
1330

1331
  // Check that filetype is correct
1332
  std::string filetype;
10✔
1333
  read_attribute(ww_file, "filetype", filetype);
10✔
1334
  if (filetype != "weight_windows") {
10!
1335
    file_close(ww_file);
×
1336
    set_errmsg(fmt::format("File '{}' is not a weight windows file.", name));
×
1337
    return OPENMC_E_INVALID_ARGUMENT;
×
1338
  }
1339

1340
  // Check that the file version is compatible
1341
  std::array<int, 2> file_version;
1342
  read_attribute(ww_file, "version", file_version);
10✔
1343
  if (file_version[0] != VERSION_WEIGHT_WINDOWS[0]) {
10!
1344
    std::string err_msg =
1345
      fmt::format("File '{}' has version {} which is incompatible with the "
1346
                  "expected version ({}).",
1347
        name, file_version, VERSION_WEIGHT_WINDOWS);
×
1348
    set_errmsg(err_msg);
×
1349
    return OPENMC_E_INVALID_ARGUMENT;
×
1350
  }
×
1351

1352
  hid_t weight_windows_group = open_group(ww_file, "weight_windows");
10✔
1353

1354
  hid_t mesh_group = open_group(ww_file, "meshes");
10✔
1355

1356
  read_meshes(mesh_group);
10✔
1357

1358
  std::vector<std::string> names = group_names(weight_windows_group);
10✔
1359

1360
  for (const auto& name : names) {
20✔
1361
    WeightWindows::from_hdf5(weight_windows_group, name);
10✔
1362
  }
1363

1364
  close_group(weight_windows_group);
10✔
1365

1366
  file_close(ww_file);
10✔
1367

1368
  return 0;
10✔
1369
}
10✔
1370

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

© 2026 Coveralls, Inc