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

openmc-dev / openmc / 29958310732

22 Jul 2026 09:12PM UTC coverage: 81.403% (+0.05%) from 81.356%
29958310732

Pull #3956

github

web-flow
Merge 6ca1ce7ba into 54b661d39
Pull Request #3956: Add description attribute to Model

18380 of 26608 branches covered (69.08%)

Branch coverage included in aggregate %.

11 of 11 new or added lines in 1 file covered. (100.0%)

1077 existing lines in 35 files now uncovered.

59961 of 69630 relevant lines covered (86.11%)

49150910.1 hits per line

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

79.3
/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)
318✔
52
{
53
  index_ = variance_reduction::weight_windows.size();
318✔
54
  set_id(id);
318✔
55
  set_defaults();
318✔
56
}
318✔
57

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

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

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

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

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

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

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

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

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

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

121
  set_defaults();
305✔
122
}
305✔
123

124
WeightWindows::~WeightWindows()
623✔
125
{
126
  variance_reduction::ww_map.erase(id());
623✔
127
}
1,869✔
128

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

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

144
  auto wws = WeightWindows::create();
37✔
145

146
  std::string particle_type;
37✔
147
  read_dataset(ww_group, "particle_type", particle_type);
37✔
148
  wws->particle_type_ = ParticleType {particle_type};
37✔
149

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

152
  int32_t mesh_id;
37✔
153
  read_dataset(ww_group, "mesh", mesh_id);
37✔
154

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

161
  wws->lower_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
  wws->upper_ww_ =
37✔
165
    tensor::Tensor<double>({static_cast<size_t>(wws->bounds_size()[0]),
37✔
166
      static_cast<size_t>(wws->bounds_size()[1])});
37✔
167

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

175
  close_group(ww_group);
37✔
176

177
  return wws;
37✔
178
}
37✔
179

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

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

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

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

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

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

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

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

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

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

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

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

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

276
std::pair<bool, WeightWindow> WeightWindows::get_weight_window(
524,907,151✔
277
  const Particle& p) const
278
{
279
  // check for particle type
280
  if (particle_type_ != p.type()) {
524,907,151✔
281
    return {false, {}};
133,279,916✔
282
  }
283

284
  // particle energy
285
  double E = p.E();
391,627,235✔
286

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

291
  // Get mesh index for particle's position
292
  const auto& mesh = this->mesh();
391,535,253✔
293
  int mesh_bin = mesh->get_bin(p.r());
391,535,253✔
294

295
  // particle is outside the weight window mesh
296
  if (mesh_bin < 0)
391,535,253✔
297
    return {false, {}};
103,865✔
298

299
  // get the mesh bin in energy group
300
  int energy_bin =
391,431,388✔
301
    lower_bound_index(energy_bounds_.begin(), energy_bounds_.end(), E);
391,431,388✔
302

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

753
  close_group(ww_group);
144✔
754
}
144✔
755

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

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

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

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

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

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

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

843
  // create a matching weight windows object
844
  auto wws = WeightWindows::create();
127✔
845
  ww_idx_ = wws->index();
127✔
846
  wws->set_mesh(mesh_idx);
127✔
847
  if (e_bounds.size() > 0)
127!
848
    wws->set_energy_bounds(e_bounds);
127✔
849
  wws->set_particle_type(particle_type);
127✔
850
  wws->set_defaults();
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!
UNCOV
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)
460,161,998✔
934
{
935
  // TODO: this is a linear search - should do something more clever
936
  for (const auto& ww : variance_reduction::weight_windows) {
593,637,761✔
937
    auto [ww_found, weight_window] = ww->get_weight_window(p);
524,907,151✔
938
    if (ww_found)
524,907,151✔
939
      return {true, weight_window};
391,431,388✔
940
  }
941
  return {false, {}};
68,730,610✔
942
}
943

944
void apply_weight_windows(Particle& p)
210,030,080✔
945
{
946
  if (!settings::weight_windows_on)
210,030,080✔
947
    return;
197,539,775✔
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)
33,221,823✔
952
    return;
953

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

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

962
  auto [ww_found, ww] = search_weight_window(p);
12,490,305✔
963
  if (ww_found && ww.is_valid()) {
12,490,305✔
964
    apply_weight_window(p, ww);
12,318,140✔
965
  } else {
966
    if (p.wgt_ww_born() == -1.0)
172,165✔
967
      p.wgt_ww_born() = 1.0;
62,458✔
968
  }
969
}
970

971
void apply_weight_window(Particle& p, WeightWindow weight_window)
459,912,779✔
972
{
973
  if (!weight_window.is_valid())
459,912,779✔
974
    return;
975

976
  // skip dead or no energy
977
  if (p.E() <= 0 || !p.alive())
352,663,076✔
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)
344,514,492✔
983
    p.wgt_ww_born() =
765,422✔
984
      (weight_window.lower_weight + weight_window.upper_weight) / 2;
765,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());
344,514,492✔
989

990
  // get the paramters
991
  double weight = p.wgt();
344,514,492✔
992

993
  // first check to see if particle should be killed for weight cutoff
994
  if (p.wgt() < weight_window.weight_cutoff) {
344,514,492✔
995
    p.wgt() = 0.0;
550✔
996
    return;
550✔
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 &&
344,513,942✔
1003
      p.wgt() > weight_window.lower_weight * weight_window.max_lb_ratio) {
160,611✔
1004
    p.ww_factor() =
124,344✔
1005
      p.wgt() / (weight_window.lower_weight * weight_window.max_lb_ratio);
124,344✔
1006
  }
1007

1008
  // move weight window closer to the particle weight if needed
1009
  if (p.ww_factor() > 1.0)
344,513,942✔
1010
    weight_window.scale(p.ww_factor());
17,869,038✔
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)) {
344,513,942✔
1017
    // do not further split the particle if above the limit
1018
    if (p.n_split() >= settings::max_history_splits)
20,308,602✔
1019
      return;
11,698,269✔
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,
17,220,666✔
1031
      std::ceil(
8,610,333✔
1032
        weight / ((1.0 + WEIGHT_WINDOW_REL_TOL) * weight_window.upper_weight)));
8,610,333✔
1033
    double max_split = weight_window.max_split;
8,610,333✔
1034
    n_split = std::min(n_split, max_split);
8,610,333✔
1035

1036
    p.n_split() += n_split;
8,610,333✔
1037

1038
    // Create secondaries and divide weight among all particles
1039
    int i_split = std::round(n_split);
8,610,333✔
1040
    for (int l = 0; l < i_split - 1; l++) {
26,652,854✔
1041
      p.split(weight / n_split);
18,042,521✔
1042
    }
1043
    // remaining weight is applied to current particle
1044
    p.wgt() = weight / n_split;
8,610,333✔
1045

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

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

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

1068
//==============================================================================
1069
// C API
1070
//==============================================================================
1071

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

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

1089
  *idx = it->second;
165✔
1090
  return 0;
165✔
1091
}
1092

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

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

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

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

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

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

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

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

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

1132
  return 0;
121✔
1133
}
1134

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1318
extern "C" int openmc_weight_windows_export(const char* filename)
160✔
1319
{
1320

1321
  if (!mpi::master)
160✔
1322
    return 0;
1323

1324
  std::string name = filename ? filename : "weight_windows.h5";
255✔
1325

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

1328
  hid_t ww_file = file_open(name, 'w');
144✔
1329

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

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

1336
  hid_t weight_windows_group = create_group(ww_file, "weight_windows");
144✔
1337

1338
  hid_t mesh_group = create_group(ww_file, "meshes");
144✔
1339

1340
  std::vector<int32_t> mesh_ids;
144✔
1341
  std::vector<int32_t> ww_ids;
144✔
1342
  for (const auto& ww : variance_reduction::weight_windows) {
288✔
1343

1344
    ww->to_hdf5(weight_windows_group);
144✔
1345
    ww_ids.push_back(ww->id());
144✔
1346

1347
    // if the mesh has already been written, move on
1348
    int32_t mesh_id = ww->mesh()->id();
144!
1349
    if (std::find(mesh_ids.begin(), mesh_ids.end(), mesh_id) != mesh_ids.end())
144!
UNCOV
1350
      continue;
×
1351

1352
    mesh_ids.push_back(mesh_id);
144✔
1353
    ww->mesh()->to_hdf5(mesh_group);
144✔
1354
  }
1355

1356
  write_attribute(mesh_group, "n_meshes", mesh_ids.size());
144✔
1357
  write_attribute(mesh_group, "ids", mesh_ids);
144✔
1358
  close_group(mesh_group);
144✔
1359

1360
  write_attribute(weight_windows_group, "n_weight_windows", ww_ids.size());
144✔
1361
  write_attribute(weight_windows_group, "ids", ww_ids);
144✔
1362
  close_group(weight_windows_group);
144✔
1363

1364
  file_close(ww_file);
144✔
1365

1366
  return 0;
144✔
1367
}
304✔
1368

1369
extern "C" int openmc_weight_windows_import(const char* filename)
37✔
1370
{
1371
  std::string name = filename ? filename : "weight_windows.h5";
37!
1372

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

1376
  if (!file_exists(name)) {
37!
UNCOV
1377
    set_errmsg(fmt::format("File '{}' does not exist", name));
×
1378
  }
1379

1380
  hid_t ww_file = file_open(name, 'r');
37✔
1381

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

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

1403
  hid_t weight_windows_group = open_group(ww_file, "weight_windows");
37✔
1404

1405
  hid_t mesh_group = open_group(ww_file, "meshes");
37✔
1406

1407
  read_meshes(mesh_group);
37✔
1408

1409
  std::vector<std::string> names = group_names(weight_windows_group);
37✔
1410

1411
  for (const auto& name : names) {
74✔
1412
    WeightWindows::from_hdf5(weight_windows_group, name);
37✔
1413
  }
1414

1415
  close_group(weight_windows_group);
37✔
1416

1417
  file_close(ww_file);
37✔
1418

1419
  return 0;
37✔
1420
}
74✔
1421

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