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

openmc-dev / openmc / 21991279157

13 Feb 2026 02:53PM UTC coverage: 81.82% (-0.06%) from 81.875%
21991279157

Pull #3805

github

web-flow
Merge 0a7a80411 into bcb939520
Pull Request #3805: Remove xtensor and xtl Dependencies

17242 of 24268 branches covered (71.05%)

Branch coverage included in aggregate %.

977 of 1013 new or added lines in 39 files covered. (96.45%)

404 existing lines in 8 files now uncovered.

57420 of 66983 relevant lines covered (85.72%)

45458907.73 hits per line

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

79.04
/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
// Non-member functions
49
//==============================================================================
50

51
void apply_weight_windows(Particle& p)
2,147,483,647✔
52
{
53
  if (!settings::weight_windows_on)
2,147,483,647✔
54
    return;
2,147,483,647✔
55

56
  // WW on photon and neutron only
57
  if (!p.type().is_neutron() && !p.type().is_photon())
75,036,483✔
58
    return;
10,347,299✔
59

60
  // skip dead or no energy
61
  if (p.E() <= 0 || !p.alive())
64,689,184✔
62
    return;
3,745,363✔
63

64
  bool in_domain = false;
60,943,821✔
65
  // TODO: this is a linear search - should do something more clever
66
  WeightWindow weight_window;
60,943,821✔
67
  for (const auto& ww : variance_reduction::weight_windows) {
72,882,815✔
68
    weight_window = ww->get_weight_window(p);
62,960,461✔
69
    if (weight_window.is_valid())
62,960,461✔
70
      break;
51,021,467✔
71
  }
72

73
  // If particle has not yet had its birth weight window value set, set it to
74
  // the current weight window (or 1.0 if not born in a weight window).
75
  if (p.wgt_ww_born() == -1.0) {
60,943,821✔
76
    if (weight_window.is_valid()) {
676,780✔
77
      p.wgt_ww_born() =
630,960✔
78
        (weight_window.lower_weight + weight_window.upper_weight) / 2;
630,960✔
79
    } else {
80
      p.wgt_ww_born() = 1.0;
45,820✔
81
    }
82
  }
83

84
  // particle is not in any of the ww domains, do nothing
85
  if (!weight_window.is_valid())
60,943,821✔
86
    return;
9,922,354✔
87

88
  // Normalize weight windows based on particle's starting weight
89
  // and the value of the weight window the particle was born in.
90
  weight_window.scale(p.wgt_born() / p.wgt_ww_born());
51,021,467✔
91

92
  // get the paramters
93
  double weight = p.wgt();
51,021,467✔
94

95
  // first check to see if particle should be killed for weight cutoff
96
  if (p.wgt() < weight_window.weight_cutoff) {
51,021,467!
97
    p.wgt() = 0.0;
×
98
    return;
×
99
  }
100

101
  // check if particle is far above current weight window
102
  // only do this if the factor is not already set on the particle and a
103
  // maximum lower bound ratio is specified
104
  if (p.ww_factor() == 0.0 && weight_window.max_lb_ratio > 1.0 &&
51,031,467✔
105
      p.wgt() > weight_window.lower_weight * weight_window.max_lb_ratio) {
10,000!
106
    p.ww_factor() =
10,000✔
107
      p.wgt() / (weight_window.lower_weight * weight_window.max_lb_ratio);
10,000✔
108
  }
109

110
  // move weight window closer to the particle weight if needed
111
  if (p.ww_factor() > 1.0)
51,021,467✔
112
    weight_window.scale(p.ww_factor());
2,903,310✔
113

114
  // if particle's weight is above the weight window split until they are within
115
  // the window
116
  if (weight > weight_window.upper_weight) {
51,021,467✔
117
    // do not further split the particle if above the limit
118
    if (p.n_split() >= settings::max_history_splits)
11,147,603✔
119
      return;
9,908,954✔
120

121
    double n_split = std::ceil(weight / weight_window.upper_weight);
1,238,649✔
122
    double max_split = weight_window.max_split;
1,238,649✔
123
    n_split = std::min(n_split, max_split);
1,238,649✔
124

125
    p.n_split() += n_split;
1,238,649✔
126

127
    // Create secondaries and divide weight among all particles
128
    int i_split = std::round(n_split);
1,238,649✔
129
    for (int l = 0; l < i_split - 1; l++) {
5,010,144✔
130
      p.split(weight / n_split);
3,771,495✔
131
    }
132
    // remaining weight is applied to current particle
133
    p.wgt() = weight / n_split;
1,238,649✔
134

135
  } else if (weight <= weight_window.lower_weight) {
39,873,864✔
136
    // if the particle weight is below the window, play Russian roulette
137
    double weight_survive =
138
      std::min(weight * weight_window.max_split, weight_window.survival_weight);
1,210,072✔
139
    russian_roulette(p, weight_survive);
1,210,072✔
140
  } // else particle is in the window, continue as normal
141
}
142

143
void free_memory_weight_windows()
7,507✔
144
{
145
  variance_reduction::ww_map.clear();
7,507✔
146
  variance_reduction::weight_windows.clear();
7,507✔
147
}
7,507✔
148

149
//==============================================================================
150
// WeightWindowSettings implementation
151
//==============================================================================
152

153
WeightWindows::WeightWindows(int32_t id)
223✔
154
{
155
  index_ = variance_reduction::weight_windows.size();
223✔
156
  set_id(id);
223✔
157
  set_defaults();
223✔
158
}
223✔
159

160
WeightWindows::WeightWindows(pugi::xml_node node)
83✔
161
{
162
  // Make sure required elements are present
163
  const vector<std::string> required_elems {
164
    "id", "particle_type", "lower_ww_bounds", "upper_ww_bounds"};
581✔
165
  for (const auto& elem : required_elems) {
415✔
166
    if (!check_for_node(node, elem.c_str())) {
332!
167
      fatal_error(fmt::format("Must specify <{}> for weight windows.", elem));
×
168
    }
169
  }
170

171
  // Get weight windows ID
172
  int32_t id = std::stoi(get_node_value(node, "id"));
83✔
173
  this->set_id(id);
83✔
174

175
  // get the particle type
176
  auto particle_type_str = std::string(get_node_value(node, "particle_type"));
83✔
177
  particle_type_ = ParticleType {particle_type_str};
83✔
178

179
  // Determine associated mesh
180
  int32_t mesh_id = std::stoi(get_node_value(node, "mesh"));
83✔
181
  set_mesh(model::mesh_map.at(mesh_id));
83✔
182

183
  // energy bounds
184
  if (check_for_node(node, "energy_bounds"))
83✔
185
    energy_bounds_ = get_node_array<double>(node, "energy_bounds");
69✔
186

187
  // get the survival value - optional
188
  if (check_for_node(node, "survival_ratio")) {
83!
189
    survival_ratio_ = std::stod(get_node_value(node, "survival_ratio"));
83✔
190
    if (survival_ratio_ <= 1)
83!
191
      fatal_error("Survival to lower weight window ratio must bigger than 1 "
×
192
                  "and less than the upper to lower weight window ratio.");
193
  }
194

195
  // get the max lower bound ratio - optional
196
  if (check_for_node(node, "max_lower_bound_ratio")) {
83✔
197
    max_lb_ratio_ = std::stod(get_node_value(node, "max_lower_bound_ratio"));
30✔
198
    if (max_lb_ratio_ < 1.0) {
30!
199
      fatal_error("Maximum lower bound ratio must be larger than 1");
×
200
    }
201
  }
202

203
  // get the max split - optional
204
  if (check_for_node(node, "max_split")) {
83!
205
    max_split_ = std::stod(get_node_value(node, "max_split"));
83✔
206
    if (max_split_ <= 1)
83!
207
      fatal_error("max split must be larger than 1");
×
208
  }
209

210
  // weight cutoff - optional
211
  if (check_for_node(node, "weight_cutoff")) {
83!
212
    weight_cutoff_ = std::stod(get_node_value(node, "weight_cutoff"));
83✔
213
    if (weight_cutoff_ <= 0)
83!
214
      fatal_error("weight_cutoff must be larger than 0");
×
215
    if (weight_cutoff_ > 1)
83!
216
      fatal_error("weight_cutoff must be less than 1");
×
217
  }
218

219
  // read the lower/upper weight bounds
220
  this->set_bounds(get_node_array<double>(node, "lower_ww_bounds"),
83✔
221
    get_node_array<double>(node, "upper_ww_bounds"));
166✔
222

223
  set_defaults();
83✔
224
}
83✔
225

226
WeightWindows::~WeightWindows()
306✔
227
{
228
  variance_reduction::ww_map.erase(id());
306✔
229
}
306✔
230

231
WeightWindows* WeightWindows::create(int32_t id)
83✔
232
{
233
  variance_reduction::weight_windows.push_back(make_unique<WeightWindows>());
83✔
234
  auto wws = variance_reduction::weight_windows.back().get();
83✔
235
  variance_reduction::ww_map[wws->id()] =
83✔
236
    variance_reduction::weight_windows.size() - 1;
83✔
237
  return wws;
83✔
238
}
239

240
WeightWindows* WeightWindows::from_hdf5(
10✔
241
  hid_t wws_group, const std::string& group_name)
242
{
243
  // collect ID from the name of this group
244
  hid_t ww_group = open_group(wws_group, group_name);
10✔
245

246
  auto wws = WeightWindows::create();
10✔
247

248
  std::string particle_type;
10✔
249
  read_dataset(ww_group, "particle_type", particle_type);
10✔
250
  wws->particle_type_ = ParticleType {particle_type};
10✔
251

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

254
  int32_t mesh_id;
255
  read_dataset(ww_group, "mesh", mesh_id);
10✔
256

257
  if (model::mesh_map.count(mesh_id) == 0) {
10!
258
    fatal_error(
×
259
      fmt::format("Mesh {} used in weight windows does not exist.", mesh_id));
×
260
  }
261
  wws->set_mesh(model::mesh_map[mesh_id]);
10✔
262

263
  wws->lower_ww_ =
264
    tensor::Tensor<double>({static_cast<size_t>(wws->bounds_size()[0]),
20✔
265
      static_cast<size_t>(wws->bounds_size()[1])});
10✔
266
  wws->upper_ww_ =
267
    tensor::Tensor<double>({static_cast<size_t>(wws->bounds_size()[0]),
20✔
268
      static_cast<size_t>(wws->bounds_size()[1])});
10✔
269

270
  read_dataset<double>(ww_group, "lower_ww_bounds", wws->lower_ww_);
10✔
271
  read_dataset<double>(ww_group, "upper_ww_bounds", wws->upper_ww_);
10✔
272
  read_dataset(ww_group, "survival_ratio", wws->survival_ratio_);
10✔
273
  read_dataset(ww_group, "max_lower_bound_ratio", wws->max_lb_ratio_);
10✔
274
  read_dataset(ww_group, "max_split", wws->max_split_);
10✔
275
  read_dataset(ww_group, "weight_cutoff", wws->weight_cutoff_);
10✔
276

277
  close_group(ww_group);
10✔
278

279
  return wws;
10✔
280
}
10✔
281

282
void WeightWindows::set_defaults()
379✔
283
{
284
  // set energy bounds to the min/max energy supported by the data
285
  if (energy_bounds_.size() == 0) {
379✔
286
    int p_type = particle_type_.transport_index();
237✔
287
    if (p_type == C_NONE) {
237!
288
      fatal_error("Weight windows particle is not supported for transport.");
×
289
    }
290
    energy_bounds_.push_back(data::energy_min[p_type]);
237✔
291
    energy_bounds_.push_back(data::energy_max[p_type]);
237✔
292
  }
293
}
379✔
294

295
void WeightWindows::allocate_ww_bounds()
499✔
296
{
297
  auto shape = bounds_size();
499✔
298
  if (shape[0] * shape[1] == 0) {
499!
299
    auto msg = fmt::format(
300
      "Size of weight window bounds is zero for WeightWindows {}", id());
×
301
    warning(msg);
×
302
  }
×
303
  lower_ww_ = tensor::Tensor<double>(
499✔
304
    {static_cast<size_t>(shape[0]), static_cast<size_t>(shape[1])});
499✔
305
  lower_ww_.fill(-1);
499✔
306
  upper_ww_ = tensor::Tensor<double>(
499✔
307
    {static_cast<size_t>(shape[0]), static_cast<size_t>(shape[1])});
499✔
308
  upper_ww_.fill(-1);
499✔
309
}
499✔
310

311
void WeightWindows::set_id(int32_t id)
446✔
312
{
313
  assert(id >= 0 || id == C_NONE);
355!
314

315
  // Clear entry in mesh map in case one was already assigned
316
  if (id_ != C_NONE) {
446!
317
    variance_reduction::ww_map.erase(id_);
446✔
318
    id_ = C_NONE;
446✔
319
  }
320

321
  // Ensure no other mesh has the same ID
322
  if (variance_reduction::ww_map.find(id) != variance_reduction::ww_map.end()) {
446!
323
    throw std::runtime_error {
×
324
      fmt::format("Two weight windows have the same ID: {}", id)};
×
325
  }
326

327
  // If no ID is specified, auto-assign the next ID in the sequence
328
  if (id == C_NONE) {
446✔
329
    id = 0;
223✔
330
    for (const auto& m : variance_reduction::weight_windows) {
243✔
331
      id = std::max(id, m->id_);
20✔
332
    }
333
    ++id;
223✔
334
  }
335

336
  // Update ID and entry in the mesh map
337
  id_ = id;
446✔
338
  variance_reduction::ww_map[id] = index_;
446✔
339
}
446✔
340

341
void WeightWindows::set_energy_bounds(span<const double> bounds)
193✔
342
{
343
  energy_bounds_.clear();
193✔
344
  energy_bounds_.insert(energy_bounds_.begin(), bounds.begin(), bounds.end());
193✔
345
  // if the mesh is set, allocate space for weight window bounds
346
  if (mesh_idx_ != C_NONE)
193!
347
    allocate_ww_bounds();
193✔
348
}
193✔
349

350
void WeightWindows::set_particle_type(ParticleType p_type)
233✔
351
{
352
  if (!p_type.is_neutron() && !p_type.is_photon())
233!
353
    fatal_error(fmt::format(
×
354
      "Particle type '{}' cannot be applied to weight windows.", p_type.str()));
×
355
  particle_type_ = p_type;
233✔
356
}
233✔
357

358
void WeightWindows::set_mesh(int32_t mesh_idx)
306✔
359
{
360
  if (mesh_idx < 0 || mesh_idx >= model::meshes.size())
306!
361
    fatal_error(fmt::format("Could not find a mesh for index {}", mesh_idx));
×
362

363
  mesh_idx_ = mesh_idx;
306✔
364
  model::meshes[mesh_idx_]->prepare_for_point_location();
306✔
365
  allocate_ww_bounds();
306✔
366
}
306✔
367

368
void WeightWindows::set_mesh(const std::unique_ptr<Mesh>& mesh)
×
369
{
370
  set_mesh(mesh.get());
×
371
}
×
372

373
void WeightWindows::set_mesh(const Mesh* mesh)
×
374
{
375
  set_mesh(model::mesh_map[mesh->id_]);
×
376
}
×
377

378
WeightWindow WeightWindows::get_weight_window(const Particle& p) const
62,960,461✔
379
{
380
  // check for particle type
381
  if (particle_type_ != p.type()) {
62,960,461✔
382
    return {};
3,034,850✔
383
  }
384

385
  // Get mesh index for particle's position
386
  const auto& mesh = this->mesh();
59,925,611✔
387
  int mesh_bin = mesh->get_bin(p.r());
59,925,611✔
388

389
  // particle is outside the weight window mesh
390
  if (mesh_bin < 0)
59,925,611✔
391
    return {};
14,588✔
392

393
  // particle energy
394
  double E = p.E();
59,911,023✔
395

396
  // check to make sure energy is in range, expects sorted energy values
397
  if (E < energy_bounds_.front() || E > energy_bounds_.back())
59,911,023!
398
    return {};
84,180✔
399

400
  // get the mesh bin in energy group
401
  int energy_bin =
402
    lower_bound_index(energy_bounds_.begin(), energy_bounds_.end(), E);
59,826,843✔
403

404
  // mesh_bin += energy_bin * mesh->n_bins();
405
  // Create individual weight window
406
  WeightWindow ww;
59,826,843✔
407
  ww.lower_weight = lower_ww_(energy_bin, mesh_bin);
59,826,843✔
408
  ww.upper_weight = upper_ww_(energy_bin, mesh_bin);
59,826,843✔
409
  ww.survival_weight = ww.lower_weight * survival_ratio_;
59,826,843✔
410
  ww.max_lb_ratio = max_lb_ratio_;
59,826,843✔
411
  ww.max_split = max_split_;
59,826,843✔
412
  ww.weight_cutoff = weight_cutoff_;
59,826,843✔
413
  return ww;
59,826,843✔
414
}
415

416
std::array<int, 2> WeightWindows::bounds_size() const
725✔
417
{
418
  int num_spatial_bins = this->mesh()->n_bins();
725✔
419
  int num_energy_bins =
420
    energy_bounds_.size() > 0 ? energy_bounds_.size() - 1 : 1;
725✔
421
  return {num_energy_bins, num_spatial_bins};
725✔
422
}
423

424
template<class T>
425
void WeightWindows::check_bounds(const T& lower, const T& upper) const
93✔
426
{
427
  // make sure that the upper and lower bounds have the same size
428
  if (lower.size() != upper.size()) {
93!
429
    auto msg = fmt::format("The upper and lower weight window lengths do not "
×
430
                           "match.\n Lower size: {}\n Upper size: {}",
431
      lower.size(), upper.size());
×
432
    fatal_error(msg);
×
433
  }
×
434
  this->check_bounds(lower);
93✔
435
}
93✔
436

437
template<class T>
438
void WeightWindows::check_bounds(const T& bounds) const
93✔
439
{
440
  // check that the number of weight window entries is correct
441
  auto dims = this->bounds_size();
93✔
442
  if (bounds.size() != dims[0] * dims[1]) {
93!
443
    auto err_msg =
×
444
      fmt::format("In weight window domain {} the number of spatial "
445
                  "energy/spatial bins ({}) does not match the number "
446
                  "of weight bins ({})",
447
        id_, dims, bounds.size());
×
448
    fatal_error(err_msg);
×
449
  }
×
450
}
93✔
451

NEW
452
void WeightWindows::set_bounds(const tensor::Tensor<double>& lower_bounds,
×
453
  const tensor::Tensor<double>& upper_bounds)
454
{
455

456
  this->check_bounds(lower_bounds, upper_bounds);
×
457

458
  // set new weight window values
459
  lower_ww_ = lower_bounds;
×
460
  upper_ww_ = upper_bounds;
×
461
}
×
462

463
void WeightWindows::set_bounds(
×
464
  const tensor::Tensor<double>& lower_bounds, double ratio)
465
{
466
  this->check_bounds(lower_bounds);
×
467

468
  // set new weight window values
469
  lower_ww_ = lower_bounds;
×
470
  upper_ww_ = lower_bounds;
×
471
  upper_ww_ *= ratio;
×
472
}
×
473

474
void WeightWindows::set_bounds(
93✔
475
  span<const double> lower_bounds, span<const double> upper_bounds)
476
{
477
  check_bounds(lower_bounds, upper_bounds);
93✔
478
  auto shape = this->bounds_size();
93✔
479
  lower_ww_ = tensor::Tensor<double>(
93✔
480
    {static_cast<size_t>(shape[0]), static_cast<size_t>(shape[1])});
93✔
481
  upper_ww_ = tensor::Tensor<double>(
93✔
482
    {static_cast<size_t>(shape[0]), static_cast<size_t>(shape[1])});
93✔
483

484
  // Copy weight window values from input spans into the tensors
485
  std::copy(lower_bounds.data(), lower_bounds.data() + lower_ww_.size(),
93✔
486
    lower_ww_.data());
487
  std::copy(upper_bounds.data(), upper_bounds.data() + upper_ww_.size(),
93✔
488
    upper_ww_.data());
489
}
93✔
490

491
void WeightWindows::set_bounds(span<const double> lower_bounds, double ratio)
×
492
{
493
  this->check_bounds(lower_bounds);
×
494

495
  auto shape = this->bounds_size();
×
NEW
496
  lower_ww_ = tensor::Tensor<double>(
×
NEW
497
    {static_cast<size_t>(shape[0]), static_cast<size_t>(shape[1])});
×
NEW
498
  upper_ww_ = tensor::Tensor<double>(
×
NEW
499
    {static_cast<size_t>(shape[0]), static_cast<size_t>(shape[1])});
×
500

501
  // Copy lower bounds into both arrays, then scale upper by ratio
NEW
502
  std::copy(lower_bounds.data(), lower_bounds.data() + lower_ww_.size(),
×
503
    lower_ww_.data());
NEW
504
  std::copy(lower_bounds.data(), lower_bounds.data() + upper_ww_.size(),
×
505
    upper_ww_.data());
506
  upper_ww_ *= ratio;
×
507
}
×
508

509
void WeightWindows::update_weights(const Tally* tally, const std::string& value,
226✔
510
  double threshold, double ratio, WeightWindowUpdateMethod method)
511
{
512
  ///////////////////////////
513
  // Setup and checks
514
  ///////////////////////////
515
  this->check_tally_update_compatibility(tally);
226✔
516

517
  // Dimensions of weight window arrays
518
  int e_bins = lower_ww_.shape(0);
226✔
519
  int64_t mesh_bins = lower_ww_.shape(1);
226✔
520

521
  // Initialize weight window arrays to -1.0 by default
522
#pragma omp parallel for collapse(2) schedule(static)
140✔
523
  for (int e = 0; e < e_bins; e++) {
740✔
524
    for (int64_t m = 0; m < mesh_bins; m++) {
943,694✔
525
      lower_ww_(e, m) = -1.0;
943,040✔
526
      upper_ww_(e, m) = -1.0;
943,040✔
527
    }
528
  }
529

530
  // determine which value to use
531
  const std::set<std::string> allowed_values = {"mean", "rel_err"};
1,130✔
532
  if (allowed_values.count(value) == 0) {
226!
533
    fatal_error(fmt::format("Invalid value '{}' specified for weight window "
×
534
                            "generation. Must be one of: 'mean' or 'rel_err'",
535
      value));
536
  }
537

538
  // determine the index of the specified score
539
  int score_index = tally->score_index("flux");
226✔
540
  if (score_index == C_NONE) {
226!
541
    fatal_error(
×
542
      fmt::format("A 'flux' score required for weight window generation "
×
543
                  "is not present on tally {}.",
544
        tally->id()));
×
545
  }
546

547
  ///////////////////////////
548
  // Extract tally data
549
  //
550
  // At the end of this section, mean and rel_err are
551
  // 2D tensors of tally data (n_e_groups, n_mesh_bins)
552
  //
553
  ///////////////////////////
554

555
  // build a shape for the tally results, this will always be
556
  // dimension 5 (3 filter dimensions, 1 score dimension, 1 results dimension)
557
  // Look for the size of the last dimension of the results tensor
558
  const auto& results = tally->results();
226✔
559
  const int results_dim = static_cast<int>(results.shape(2));
226✔
560
  std::array<int, 5> shape = {1, 1, 1, tally->n_scores(), results_dim};
226✔
561

562
  // set the shape for the filters applied on the tally
563
  for (int i = 0; i < tally->filters().size(); i++) {
864✔
564
    const auto& filter = model::tally_filters[tally->filters(i)];
638✔
565
    shape[i] = filter->n_bins();
638✔
566
  }
567

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

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

574
  // assign other filter types to dummy positions if needed
575
  if (!tally->has_filter(FilterType::PARTICLE))
226✔
576
    filter_types.push_back(FilterType::PARTICLE);
20✔
577

578
  if (!tally->has_filter(FilterType::ENERGY))
226✔
579
    filter_types.push_back(FilterType::ENERGY);
20✔
580

581
  // particle axis mapping
582
  transpose[0] =
226✔
583
    std::find(filter_types.begin(), filter_types.end(), FilterType::PARTICLE) -
226✔
584
    filter_types.begin();
226✔
585

586
  // energy axis mapping
587
  transpose[1] =
226✔
588
    std::find(filter_types.begin(), filter_types.end(), FilterType::ENERGY) -
226✔
589
    filter_types.begin();
226✔
590

591
  // mesh axis mapping
592
  transpose[2] =
226✔
593
    std::find(filter_types.begin(), filter_types.end(), FilterType::MESH) -
226✔
594
    filter_types.begin();
226✔
595

596
  // determine the index of the particle within its filter
597
  int particle_idx = 0;
226✔
598
  if (tally->has_filter(FilterType::PARTICLE)) {
226✔
599
    auto pf = tally->get_filter<ParticleFilter>();
206✔
600
    const auto& particles = pf->particles();
206✔
601

602
    auto p_it =
603
      std::find(particles.begin(), particles.end(), this->particle_type_);
206✔
604
    if (p_it == particles.end()) {
206!
605
      auto msg = fmt::format("Particle type '{}' not present on Filter {} for "
606
                             "Tally {} used to update WeightWindows {}",
607
        this->particle_type_.str(), pf->id(), tally->id(), this->id());
×
608
      fatal_error(msg);
×
609
    }
×
610

611
    particle_idx = p_it - particles.begin();
206✔
612
  }
613

614
  // The tally results array is 3D: (n_filter_combos, n_scores, n_result_types).
615
  // The first dimension is a row-major flattening of up to 3 filter dimensions
616
  // (particle, energy, mesh) whose storage order depends on which filters the
617
  // tally has. We need to map our desired indices (particle, energy, mesh)
618
  // into the correct flat filter combination index.
619
  //
620
  // transpose[i] tells us which storage position holds dimension i:
621
  //   i=0 -> particle, i=1 -> energy, i=2 -> mesh
622
  // shape[j] gives the number of bins for filter storage position j.
623

624
  // Row-major strides for the 3 filter dimensions
625
  const int stride0 = shape[1] * shape[2];
226✔
626
  const int stride1 = shape[2];
226✔
627

628
  tensor::Tensor<double> sum(
629
    {static_cast<size_t>(e_bins), static_cast<size_t>(mesh_bins)});
226✔
630
  tensor::Tensor<double> sum_sq(
631
    {static_cast<size_t>(e_bins), static_cast<size_t>(mesh_bins)});
226✔
632

633
  const int i_sum = static_cast<int>(TallyResult::SUM);
226✔
634
  const int i_sum_sq = static_cast<int>(TallyResult::SUM_SQ);
226✔
635

636
  for (int e = 0; e < e_bins; e++) {
1,872✔
637
    for (int64_t m = 0; m < mesh_bins; m++) {
2,380,272✔
638
      // Place particle, energy, and mesh indices into their storage positions
639
      std::array<int, 3> idx = {0, 0, 0};
2,378,626✔
640
      idx[transpose[0]] = particle_idx;
2,378,626✔
641
      idx[transpose[1]] = e;
2,378,626✔
642
      idx[transpose[2]] = static_cast<int>(m);
2,378,626✔
643

644
      // Compute flat filter combination index (row-major over filter dims)
645
      int flat = idx[0] * stride0 + idx[1] * stride1 + idx[2];
2,378,626✔
646

647
      sum(e, m) = results(flat, score_index, i_sum);
2,378,626✔
648
      sum_sq(e, m) = results(flat, score_index, i_sum_sq);
2,378,626✔
649
    }
650
  }
651
  int n = tally->n_realizations_;
226✔
652

653
  //////////////////////////////////////////////
654
  //
655
  // Assign new weight windows
656
  //
657
  // Use references to the existing weight window data
658
  // to store and update the values
659
  //
660
  //////////////////////////////////////////////
661

662
  // up to this point the data arrays are views into the tally results (no
663
  // computation has been performed) now we'll switch references to the tally's
664
  // bounds to avoid allocating additional memory
665
  auto& new_bounds = this->lower_ww_;
226✔
666
  auto& rel_err = this->upper_ww_;
226✔
667

668
  // get mesh volumes
669
  auto mesh_vols = this->mesh()->volumes();
226✔
670

671
  // Calculate mean (new_bounds) and relative error
672
#pragma omp parallel for collapse(2) schedule(static)
140✔
673
  for (int e = 0; e < e_bins; e++) {
740✔
674
    for (int64_t m = 0; m < mesh_bins; m++) {
943,694✔
675
      // Calculate mean
676
      new_bounds(e, m) = sum(e, m) / n;
943,040✔
677
      // Calculate relative error
678
      if (sum(e, m) > 0.0) {
943,040✔
679
        double mean_val = new_bounds(e, m);
81,184✔
680
        double variance = (sum_sq(e, m) / n - mean_val * mean_val) / (n - 1);
81,184✔
681
        rel_err(e, m) = std::sqrt(variance) / mean_val;
81,184✔
682
      } else {
683
        rel_err(e, m) = INFTY;
861,856✔
684
      }
685
      if (value == "rel_err") {
943,040✔
686
        new_bounds(e, m) = 1.0 / rel_err(e, m);
276,000✔
687
      }
688
    }
689
  }
690

691
  // Divide by volume of mesh elements
692
#pragma omp parallel for collapse(2) schedule(static)
140✔
693
  for (int e = 0; e < e_bins; e++) {
740✔
694
    for (int64_t m = 0; m < mesh_bins; m++) {
943,694✔
695
      new_bounds(e, m) /= mesh_vols[m];
943,040✔
696
    }
697
  }
698

699
  if (method == WeightWindowUpdateMethod::MAGIC) {
226✔
700
    // For MAGIC, weight windows are proportional to the forward fluxes.
701
    // We normalize weight windows independently for each energy group.
702

703
    // Find group maximum and normalize (per energy group)
704
    for (int e = 0; e < e_bins; e++) {
1,700✔
705
      double group_max = 0.0;
1,560✔
706

707
      // Find maximum value across all elements in this energy group
708
#pragma omp parallel for schedule(static) reduction(max : group_max)
936✔
709
      for (int64_t m = 0; m < mesh_bins; m++) {
874,004✔
710
        if (new_bounds(e, m) > group_max) {
873,380✔
711
          group_max = new_bounds(e, m);
2,016✔
712
        }
713
      }
714

715
      // Normalize values in this energy group by the maximum value
716
      if (group_max > 0.0) {
1,560✔
717
        double norm_factor = 1.0 / (2.0 * group_max);
1,530✔
718
#pragma omp parallel for schedule(static)
918✔
719
        for (int64_t m = 0; m < mesh_bins; m++) {
873,272✔
720
          new_bounds(e, m) *= norm_factor;
872,660✔
721
        }
722
      }
723
    }
724
  } else {
725
    // For FW-CADIS, weight windows are inversely proportional to the adjoint
726
    // fluxes. We normalize the weight windows across all energy groups.
727
#pragma omp parallel for collapse(2) schedule(static)
56✔
728
    for (int e = 0; e < e_bins; e++) {
60✔
729
      for (int64_t m = 0; m < mesh_bins; m++) {
69,690✔
730
        // Take the inverse, but are careful not to divide by zero
731
        if (new_bounds(e, m) != 0.0) {
69,660✔
732
          new_bounds(e, m) = 1.0 / new_bounds(e, m);
55,728✔
733
        } else {
734
          new_bounds(e, m) = 0.0;
13,932✔
735
        }
736
      }
737
    }
738

739
    // Find the maximum value across all elements
740
    double max_val = 0.0;
86✔
741
#pragma omp parallel for collapse(2) schedule(static) reduction(max : max_val)
56✔
742
    for (int e = 0; e < e_bins; e++) {
60✔
743
      for (int64_t m = 0; m < mesh_bins; m++) {
69,690✔
744
        if (new_bounds(e, m) > max_val) {
69,660✔
745
          max_val = new_bounds(e, m);
324✔
746
        }
747
      }
748
    }
749

750
    // Parallel normalization
751
    if (max_val > 0.0) {
86✔
752
      double norm_factor = 1.0 / (2.0 * max_val);
62✔
753
#pragma omp parallel for collapse(2) schedule(static)
38✔
754
      for (int e = 0; e < e_bins; e++) {
48✔
755
        for (int64_t m = 0; m < mesh_bins; m++) {
55,752✔
756
          new_bounds(e, m) *= norm_factor;
55,728✔
757
        }
758
      }
759
    }
760
  }
761

762
  // Final processing
763
#pragma omp parallel for collapse(2) schedule(static)
140✔
764
  for (int e = 0; e < e_bins; e++) {
740✔
765
    for (int64_t m = 0; m < mesh_bins; m++) {
943,694✔
766
      // Values where the mean is zero should be ignored
767
      if (sum(e, m) <= 0.0) {
943,040✔
768
        new_bounds(e, m) = -1.0;
861,856✔
769
      }
770
      // Values where the relative error is higher than the threshold should be
771
      // ignored
772
      else if (rel_err(e, m) > threshold) {
81,184✔
773
        new_bounds(e, m) = -1.0;
1,136✔
774
      }
775
      // Set the upper bounds
776
      upper_ww_(e, m) = ratio * lower_ww_(e, m);
943,040✔
777
    }
778
  }
779
}
226✔
780

781
void WeightWindows::check_tally_update_compatibility(const Tally* tally)
226✔
782
{
783
  // define the set of allowed filters for the tally
784
  const std::set<FilterType> allowed_filters = {
785
    FilterType::MESH, FilterType::ENERGY, FilterType::PARTICLE};
226✔
786

787
  // retrieve a mapping of filter type to filter index for the tally
788
  auto filter_indices = tally->filter_indices();
226✔
789

790
  // a mesh filter is required for a tally used to update weight windows
791
  if (!filter_indices.count(FilterType::MESH)) {
226!
792
    fatal_error(
×
793
      "A mesh filter is required for a tally to update weight window bounds");
794
  }
795

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

799
  // make sure that all of the filters present on the tally are allowed
800
  for (auto filter_pair : filter_indices) {
864✔
801
    if (allowed_filters.find(filter_pair.first) == allowed_filters.end()) {
638!
802
      fatal_error(fmt::format("Invalid filter type '{}' found on tally "
×
803
                              "used for weight window generation.",
804
        model::tally_filters[tally->filters(filter_pair.second)]->type_str()));
×
805
    }
806
  }
807

808
  if (mesh_filter->mesh() != mesh_idx_) {
226!
809
    int32_t mesh_filter_id = model::meshes[mesh_filter->mesh()]->id();
×
810
    int32_t ww_mesh_id = model::meshes[this->mesh_idx_]->id();
×
811
    fatal_error(fmt::format("Mesh filter {} uses a different mesh ({}) than "
×
812
                            "weight window {} mesh ({})",
813
      mesh_filter->id(), mesh_filter_id, id_, ww_mesh_id));
×
814
  }
815

816
  // if an energy filter exists, make sure the energy grid matches that of this
817
  // weight window object
818
  if (auto energy_filter = tally->get_filter<EnergyFilter>()) {
226✔
819
    std::vector<double> filter_bins = energy_filter->bins();
206✔
820
    std::set<double> filter_e_bounds(
821
      energy_filter->bins().begin(), energy_filter->bins().end());
206✔
822
    if (filter_e_bounds.size() != energy_bounds().size()) {
206!
823
      fatal_error(
×
824
        fmt::format("Energy filter {} does not have the same number of energy "
×
825
                    "bounds ({}) as weight window object {} ({})",
826
          energy_filter->id(), filter_e_bounds.size(), id_,
×
827
          energy_bounds().size()));
×
828
    }
829

830
    for (auto e : energy_bounds()) {
2,038✔
831
      if (filter_e_bounds.count(e) == 0) {
1,832!
832
        fatal_error(fmt::format(
×
833
          "Energy bounds of filter {} and weight windows {} do not match",
834
          energy_filter->id(), id_));
×
835
      }
836
    }
837
  }
206✔
838
}
226✔
839

840
void WeightWindows::to_hdf5(hid_t group) const
122✔
841
{
842
  hid_t ww_group = create_group(group, fmt::format("weight_windows_{}", id()));
244✔
843

844
  write_dataset(ww_group, "mesh", this->mesh()->id());
122✔
845
  write_dataset(ww_group, "particle_type", particle_type_.str());
122✔
846
  write_dataset(ww_group, "energy_bounds", energy_bounds_);
122✔
847
  write_dataset(ww_group, "lower_ww_bounds", lower_ww_);
122✔
848
  write_dataset(ww_group, "upper_ww_bounds", upper_ww_);
122✔
849
  write_dataset(ww_group, "survival_ratio", survival_ratio_);
122✔
850
  write_dataset(ww_group, "max_lower_bound_ratio", max_lb_ratio_);
122✔
851
  write_dataset(ww_group, "max_split", max_split_);
122✔
852
  write_dataset(ww_group, "weight_cutoff", weight_cutoff_);
122✔
853

854
  close_group(ww_group);
122✔
855
}
122✔
856

857
WeightWindowsGenerator::WeightWindowsGenerator(pugi::xml_node node)
73✔
858
{
859
  // read information from the XML node
860
  int32_t mesh_id = std::stoi(get_node_value(node, "mesh"));
73✔
861
  int32_t mesh_idx = model::mesh_map[mesh_id];
73✔
862
  max_realizations_ = std::stoi(get_node_value(node, "max_realizations"));
73✔
863

864
  int32_t active_batches = settings::n_batches - settings::n_inactive;
73✔
865
  if (max_realizations_ > active_batches) {
73✔
866
    auto msg =
867
      fmt::format("The maximum number of specified tally realizations ({}) is "
868
                  "greater than the number of active batches ({}).",
869
        max_realizations_, active_batches);
27✔
870
    warning(msg);
15✔
871
  }
15✔
872
  auto tmp_str = get_node_value(node, "particle_type", false, true);
73✔
873
  auto particle_type = ParticleType {tmp_str};
73✔
874

875
  update_interval_ = std::stoi(get_node_value(node, "update_interval"));
73✔
876
  on_the_fly_ = get_node_value_bool(node, "on_the_fly");
73✔
877

878
  std::vector<double> e_bounds;
73✔
879
  if (check_for_node(node, "energy_bounds")) {
73✔
880
    e_bounds = get_node_array<double>(node, "energy_bounds");
21✔
881
  } else {
882
    int p_type = particle_type.transport_index();
52✔
883
    if (p_type == C_NONE) {
52!
884
      fatal_error("Weight windows particle is not supported for transport.");
×
885
    }
886
    e_bounds.push_back(data::energy_min[p_type]);
52✔
887
    e_bounds.push_back(data::energy_max[p_type]);
52✔
888
  }
889

890
  // set method
891
  std::string method_string = get_node_value(node, "method");
73✔
892
  if (method_string == "magic") {
73✔
893
    method_ = WeightWindowUpdateMethod::MAGIC;
30✔
894
    if (settings::solver_type == SolverType::RANDOM_RAY &&
30!
895
        FlatSourceDomain::adjoint_) {
896
      fatal_error("Random ray weight window generation with MAGIC cannot be "
×
897
                  "done in adjoint mode.");
898
    }
899
  } else if (method_string == "fw_cadis") {
43!
900
    method_ = WeightWindowUpdateMethod::FW_CADIS;
43✔
901
    if (settings::solver_type != SolverType::RANDOM_RAY) {
43!
902
      fatal_error("FW-CADIS can only be run in random ray solver mode.");
×
903
    }
904
    FlatSourceDomain::adjoint_ = true;
43✔
905
  } else {
906
    fatal_error(fmt::format(
×
907
      "Unknown weight window update method '{}' specified", method_string));
908
  }
909

910
  // parse non-default update parameters if specified
911
  if (check_for_node(node, "update_parameters")) {
73✔
912
    pugi::xml_node params_node = node.child("update_parameters");
20✔
913
    if (check_for_node(params_node, "value"))
20!
914
      tally_value_ = get_node_value(params_node, "value");
20✔
915
    if (check_for_node(params_node, "threshold"))
20!
916
      threshold_ = std::stod(get_node_value(params_node, "threshold"));
20✔
917
    if (check_for_node(params_node, "ratio")) {
20!
918
      ratio_ = std::stod(get_node_value(params_node, "ratio"));
20✔
919
    }
920
  }
921

922
  // check update parameter values
923
  if (tally_value_ != "mean" && tally_value_ != "rel_err") {
73!
924
    fatal_error(fmt::format("Unsupported tally value '{}' specified for "
×
925
                            "weight window generation.",
926
      tally_value_));
×
927
  }
928
  if (threshold_ <= 0.0)
73!
929
    fatal_error(fmt::format("Invalid relative error threshold '{}' (<= 0.0) "
×
930
                            "specified for weight window generation",
931
      ratio_));
×
932
  if (ratio_ <= 1.0)
73!
933
    fatal_error(fmt::format("Invalid weight window ratio '{}' (<= 1.0) "
×
934
                            "specified for weight window generation"));
935

936
  // create a matching weight windows object
937
  auto wws = WeightWindows::create();
73✔
938
  ww_idx_ = wws->index();
73✔
939
  wws->set_mesh(mesh_idx);
73✔
940
  if (e_bounds.size() > 0)
73!
941
    wws->set_energy_bounds(e_bounds);
73✔
942
  wws->set_particle_type(particle_type);
73✔
943
  wws->set_defaults();
73✔
944
}
73✔
945

946
void WeightWindowsGenerator::create_tally()
73✔
947
{
948
  const auto& wws = variance_reduction::weight_windows[ww_idx_];
73✔
949

950
  // create a tally based on the WWG information
951
  Tally* ww_tally = Tally::create();
73✔
952
  tally_idx_ = model::tally_map[ww_tally->id()];
73✔
953
  ww_tally->set_scores({"flux"});
146!
954

955
  int32_t mesh_id = wws->mesh()->id();
73✔
956
  int32_t mesh_idx = model::mesh_map.at(mesh_id);
73✔
957
  // see if there's already a mesh filter using this mesh
958
  bool found_mesh_filter = false;
73✔
959
  for (const auto& f : model::tally_filters) {
229✔
960
    if (f->type() == FilterType::MESH) {
166✔
961
      const auto* mesh_filter = dynamic_cast<MeshFilter*>(f.get());
10!
962
      if (mesh_filter->mesh() == mesh_idx && !mesh_filter->translated() &&
20!
963
          !mesh_filter->rotated()) {
10!
964
        ww_tally->add_filter(f.get());
10✔
965
        found_mesh_filter = true;
10✔
966
        break;
10✔
967
      }
968
    }
969
  }
970

971
  if (!found_mesh_filter) {
73✔
972
    auto mesh_filter = Filter::create("mesh");
63✔
973
    openmc_mesh_filter_set_mesh(mesh_filter->index(), model::mesh_map[mesh_id]);
63✔
974
    ww_tally->add_filter(mesh_filter);
63✔
975
  }
976

977
  const auto& e_bounds = wws->energy_bounds();
73✔
978
  if (e_bounds.size() > 0) {
73!
979
    auto energy_filter = Filter::create("energy");
73✔
980
    openmc_energy_filter_set_bins(
146✔
981
      energy_filter->index(), e_bounds.size(), e_bounds.data());
73✔
982
    ww_tally->add_filter(energy_filter);
73✔
983
  }
984

985
  // add a particle filter
986
  auto particle_type = wws->particle_type();
73✔
987
  auto particle_filter = Filter::create("particle");
73✔
988
  auto pf = dynamic_cast<ParticleFilter*>(particle_filter);
73!
989
  pf->set_particles({&particle_type, 1});
73✔
990
  ww_tally->add_filter(particle_filter);
73✔
991
}
73✔
992

993
void WeightWindowsGenerator::update() const
2,122✔
994
{
995
  const auto& wws = variance_reduction::weight_windows[ww_idx_];
2,122✔
996

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

999
  // If in random ray mode, only update on the last batch
1000
  if (settings::solver_type == SolverType::RANDOM_RAY) {
2,122✔
1001
    if (simulation::current_batch != settings::n_batches) {
1,972✔
1002
      return;
1,886✔
1003
    }
1004
    // If in Monte Carlo mode and beyond the number of max realizations or
1005
    // not at the correct update interval, skip the update
1006
  } else if (max_realizations_ < tally->n_realizations_ ||
150✔
1007
             tally->n_realizations_ % update_interval_ != 0) {
30!
1008
    return;
120✔
1009
  }
1010

1011
  wws->update_weights(tally, tally_value_, threshold_, ratio_, method_);
116✔
1012

1013
  // if we're not doing on the fly generation, reset the tally results once
1014
  // we're done with the update
1015
  if (!on_the_fly_)
116!
1016
    tally->reset();
×
1017

1018
  // TODO: deactivate or remove tally once weight window generation is
1019
  // complete
1020
}
1021

1022
//==============================================================================
1023
// Non-member functions
1024
//==============================================================================
1025

1026
void finalize_variance_reduction()
7,364✔
1027
{
1028
  for (const auto& wwg : variance_reduction::weight_windows_generators) {
7,437✔
1029
    wwg->create_tally();
73✔
1030
  }
1031
}
7,364✔
1032

1033
//==============================================================================
1034
// C API
1035
//==============================================================================
1036

1037
int verify_ww_index(int32_t index)
1,810✔
1038
{
1039
  if (index < 0 || index >= variance_reduction::weight_windows.size()) {
1,810!
1040
    set_errmsg(fmt::format("Index '{}' for weight windows is invalid", index));
×
1041
    return OPENMC_E_OUT_OF_BOUNDS;
×
1042
  }
1043
  return 0;
1,810✔
1044
}
1045

1046
extern "C" int openmc_get_weight_windows_index(int32_t id, int32_t* idx)
150✔
1047
{
1048
  auto it = variance_reduction::ww_map.find(id);
150✔
1049
  if (it == variance_reduction::ww_map.end()) {
150!
1050
    set_errmsg(fmt::format("No weight windows exist with ID={}", id));
×
1051
    return OPENMC_E_INVALID_ID;
×
1052
  }
1053

1054
  *idx = it->second;
150✔
1055
  return 0;
150✔
1056
}
1057

1058
extern "C" int openmc_weight_windows_get_id(int32_t index, int32_t* id)
470✔
1059
{
1060
  if (int err = verify_ww_index(index))
470!
1061
    return err;
×
1062

1063
  const auto& wws = variance_reduction::weight_windows.at(index);
470✔
1064
  *id = wws->id();
470✔
1065
  return 0;
470✔
1066
}
1067

1068
extern "C" int openmc_weight_windows_set_id(int32_t index, int32_t id)
140✔
1069
{
1070
  if (int err = verify_ww_index(index))
140!
1071
    return err;
×
1072

1073
  const auto& wws = variance_reduction::weight_windows.at(index);
140✔
1074
  wws->set_id(id);
140✔
1075
  return 0;
140✔
1076
}
1077

1078
extern "C" int openmc_weight_windows_update_magic(int32_t ww_idx,
110✔
1079
  int32_t tally_idx, const char* value, double threshold, double ratio)
1080
{
1081
  if (int err = verify_ww_index(ww_idx))
110!
1082
    return err;
×
1083

1084
  if (tally_idx < 0 || tally_idx >= model::tallies.size()) {
110!
1085
    set_errmsg(fmt::format("Index '{}' for tally is invalid", tally_idx));
×
1086
    return OPENMC_E_OUT_OF_BOUNDS;
×
1087
  }
1088

1089
  // get the requested tally
1090
  const Tally* tally = model::tallies.at(tally_idx).get();
110✔
1091

1092
  // get the WeightWindows object
1093
  const auto& wws = variance_reduction::weight_windows.at(ww_idx);
110✔
1094

1095
  wws->update_weights(tally, value, threshold, ratio);
110✔
1096

1097
  return 0;
110✔
1098
}
1099

1100
extern "C" int openmc_weight_windows_set_mesh(int32_t ww_idx, int32_t mesh_idx)
140✔
1101
{
1102
  if (int err = verify_ww_index(ww_idx))
140!
1103
    return err;
×
1104
  const auto& wws = variance_reduction::weight_windows.at(ww_idx);
140✔
1105
  wws->set_mesh(mesh_idx);
140✔
1106
  return 0;
140✔
1107
}
1108

1109
extern "C" int openmc_weight_windows_get_mesh(int32_t ww_idx, int32_t* mesh_idx)
10✔
1110
{
1111
  if (int err = verify_ww_index(ww_idx))
10!
1112
    return err;
×
1113
  const auto& wws = variance_reduction::weight_windows.at(ww_idx);
10✔
1114
  *mesh_idx = model::mesh_map.at(wws->mesh()->id());
10✔
1115
  return 0;
10✔
1116
}
1117

1118
extern "C" int openmc_weight_windows_set_energy_bounds(
120✔
1119
  int32_t ww_idx, double* e_bounds, size_t e_bounds_size)
1120
{
1121
  if (int err = verify_ww_index(ww_idx))
120!
1122
    return err;
×
1123
  const auto& wws = variance_reduction::weight_windows.at(ww_idx);
120✔
1124
  wws->set_energy_bounds({e_bounds, e_bounds_size});
120✔
1125
  return 0;
120✔
1126
}
1127

1128
extern "C" int openmc_weight_windows_get_energy_bounds(
10✔
1129
  int32_t ww_idx, const double** e_bounds, size_t* e_bounds_size)
1130
{
1131
  if (int err = verify_ww_index(ww_idx))
10!
1132
    return err;
×
1133
  const auto& wws = variance_reduction::weight_windows[ww_idx].get();
10✔
1134
  *e_bounds = wws->energy_bounds().data();
10✔
1135
  *e_bounds_size = wws->energy_bounds().size();
10✔
1136
  return 0;
10✔
1137
}
1138

1139
extern "C" int openmc_weight_windows_set_particle(
160✔
1140
  int32_t index, int32_t particle)
1141
{
1142
  if (int err = verify_ww_index(index))
160!
1143
    return err;
×
1144

1145
  const auto& wws = variance_reduction::weight_windows.at(index);
160✔
1146
  wws->set_particle_type(ParticleType {particle});
160✔
1147
  return 0;
160✔
1148
}
1149

1150
extern "C" int openmc_weight_windows_get_particle(
40✔
1151
  int32_t index, int32_t* particle)
1152
{
1153
  if (int err = verify_ww_index(index))
40!
1154
    return err;
×
1155

1156
  const auto& wws = variance_reduction::weight_windows.at(index);
40✔
1157
  *particle = wws->particle_type().pdg_number();
40✔
1158
  return 0;
40✔
1159
}
1160

1161
extern "C" int openmc_weight_windows_get_bounds(int32_t index,
440✔
1162
  const double** lower_bounds, const double** upper_bounds, size_t* size)
1163
{
1164
  if (int err = verify_ww_index(index))
440!
1165
    return err;
×
1166

1167
  const auto& wws = variance_reduction::weight_windows[index];
440✔
1168
  *size = wws->lower_ww_bounds().size();
440✔
1169
  *lower_bounds = wws->lower_ww_bounds().data();
440✔
1170
  *upper_bounds = wws->upper_ww_bounds().data();
440✔
1171
  return 0;
440✔
1172
}
1173

1174
extern "C" int openmc_weight_windows_set_bounds(int32_t index,
10✔
1175
  const double* lower_bounds, const double* upper_bounds, size_t size)
1176
{
1177
  if (int err = verify_ww_index(index))
10!
1178
    return err;
×
1179

1180
  const auto& wws = variance_reduction::weight_windows[index];
10✔
1181
  wws->set_bounds(span<const double>(lower_bounds, size),
10✔
1182
    span<const double>(upper_bounds, size));
1183
  return 0;
10✔
1184
}
1185

1186
extern "C" int openmc_weight_windows_get_survival_ratio(
30✔
1187
  int32_t index, double* ratio)
1188
{
1189
  if (int err = verify_ww_index(index))
30!
1190
    return err;
×
1191
  const auto& wws = variance_reduction::weight_windows[index];
30✔
1192
  *ratio = wws->survival_ratio();
30✔
1193
  return 0;
30✔
1194
}
1195

1196
extern "C" int openmc_weight_windows_set_survival_ratio(
10✔
1197
  int32_t index, double ratio)
1198
{
1199
  if (int err = verify_ww_index(index))
10!
1200
    return err;
×
1201
  const auto& wws = variance_reduction::weight_windows[index];
10✔
1202
  wws->survival_ratio() = ratio;
10✔
1203
  std::cout << "Survival ratio: " << wws->survival_ratio() << std::endl;
10✔
1204
  return 0;
10✔
1205
}
1206

1207
extern "C" int openmc_weight_windows_get_max_lower_bound_ratio(
30✔
1208
  int32_t index, double* lb_ratio)
1209
{
1210
  if (int err = verify_ww_index(index))
30!
1211
    return err;
×
1212
  const auto& wws = variance_reduction::weight_windows[index];
30✔
1213
  *lb_ratio = wws->max_lower_bound_ratio();
30✔
1214
  return 0;
30✔
1215
}
1216

1217
extern "C" int openmc_weight_windows_set_max_lower_bound_ratio(
10✔
1218
  int32_t index, double lb_ratio)
1219
{
1220
  if (int err = verify_ww_index(index))
10!
1221
    return err;
×
1222
  const auto& wws = variance_reduction::weight_windows[index];
10✔
1223
  wws->max_lower_bound_ratio() = lb_ratio;
10✔
1224
  return 0;
10✔
1225
}
1226

1227
extern "C" int openmc_weight_windows_get_weight_cutoff(
30✔
1228
  int32_t index, double* cutoff)
1229
{
1230
  if (int err = verify_ww_index(index))
30!
1231
    return err;
×
1232
  const auto& wws = variance_reduction::weight_windows[index];
30✔
1233
  *cutoff = wws->weight_cutoff();
30✔
1234
  return 0;
30✔
1235
}
1236

1237
extern "C" int openmc_weight_windows_set_weight_cutoff(
10✔
1238
  int32_t index, double cutoff)
1239
{
1240
  if (int err = verify_ww_index(index))
10!
1241
    return err;
×
1242
  const auto& wws = variance_reduction::weight_windows[index];
10✔
1243
  wws->weight_cutoff() = cutoff;
10✔
1244
  return 0;
10✔
1245
}
1246

1247
extern "C" int openmc_weight_windows_get_max_split(
30✔
1248
  int32_t index, int* max_split)
1249
{
1250
  if (int err = verify_ww_index(index))
30!
1251
    return err;
×
1252
  const auto& wws = variance_reduction::weight_windows[index];
30✔
1253
  *max_split = wws->max_split();
30✔
1254
  return 0;
30✔
1255
}
1256

1257
extern "C" int openmc_weight_windows_set_max_split(int32_t index, int max_split)
10✔
1258
{
1259
  if (int err = verify_ww_index(index))
10!
1260
    return err;
×
1261
  const auto& wws = variance_reduction::weight_windows[index];
10✔
1262
  wws->max_split() = max_split;
10✔
1263
  return 0;
10✔
1264
}
1265

1266
extern "C" int openmc_extend_weight_windows(
140✔
1267
  int32_t n, int32_t* index_start, int32_t* index_end)
1268
{
1269
  if (index_start)
140!
1270
    *index_start = variance_reduction::weight_windows.size();
140✔
1271
  if (index_end)
140!
1272
    *index_end = variance_reduction::weight_windows.size() + n - 1;
×
1273
  for (int i = 0; i < n; ++i)
280✔
1274
    variance_reduction::weight_windows.push_back(make_unique<WeightWindows>());
140✔
1275
  return 0;
140✔
1276
}
1277

1278
extern "C" size_t openmc_weight_windows_size()
140✔
1279
{
1280
  return variance_reduction::weight_windows.size();
140✔
1281
}
1282

1283
extern "C" int openmc_weight_windows_export(const char* filename)
146✔
1284
{
1285

1286
  if (!mpi::master)
146✔
1287
    return 0;
24✔
1288

1289
  std::string name = filename ? filename : "weight_windows.h5";
244✔
1290

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

1293
  hid_t ww_file = file_open(name, 'w');
122✔
1294

1295
  // Write file type
1296
  write_attribute(ww_file, "filetype", "weight_windows");
122✔
1297

1298
  // Write revisiion number for state point file
1299
  write_attribute(ww_file, "version", VERSION_WEIGHT_WINDOWS);
122✔
1300

1301
  hid_t weight_windows_group = create_group(ww_file, "weight_windows");
122✔
1302

1303
  hid_t mesh_group = create_group(ww_file, "meshes");
122✔
1304

1305
  std::vector<int32_t> mesh_ids;
122✔
1306
  std::vector<int32_t> ww_ids;
122✔
1307
  for (const auto& ww : variance_reduction::weight_windows) {
244✔
1308

1309
    ww->to_hdf5(weight_windows_group);
122✔
1310
    ww_ids.push_back(ww->id());
122✔
1311

1312
    // if the mesh has already been written, move on
1313
    int32_t mesh_id = ww->mesh()->id();
122✔
1314
    if (std::find(mesh_ids.begin(), mesh_ids.end(), mesh_id) != mesh_ids.end())
122!
1315
      continue;
×
1316

1317
    mesh_ids.push_back(mesh_id);
122✔
1318
    ww->mesh()->to_hdf5(mesh_group);
122✔
1319
  }
1320

1321
  write_attribute(mesh_group, "n_meshes", mesh_ids.size());
122✔
1322
  write_attribute(mesh_group, "ids", mesh_ids);
122✔
1323
  close_group(mesh_group);
122✔
1324

1325
  write_attribute(weight_windows_group, "n_weight_windows", ww_ids.size());
122✔
1326
  write_attribute(weight_windows_group, "ids", ww_ids);
122✔
1327
  close_group(weight_windows_group);
122✔
1328

1329
  file_close(ww_file);
122✔
1330

1331
  return 0;
122✔
1332
}
122✔
1333

1334
extern "C" int openmc_weight_windows_import(const char* filename)
10✔
1335
{
1336
  std::string name = filename ? filename : "weight_windows.h5";
10!
1337

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

1341
  if (!file_exists(name)) {
10!
1342
    set_errmsg(fmt::format("File '{}' does not exist", name));
×
1343
  }
1344

1345
  hid_t ww_file = file_open(name, 'r');
10✔
1346

1347
  // Check that filetype is correct
1348
  std::string filetype;
10✔
1349
  read_attribute(ww_file, "filetype", filetype);
10✔
1350
  if (filetype != "weight_windows") {
10!
1351
    file_close(ww_file);
×
1352
    set_errmsg(fmt::format("File '{}' is not a weight windows file.", name));
×
1353
    return OPENMC_E_INVALID_ARGUMENT;
×
1354
  }
1355

1356
  // Check that the file version is compatible
1357
  std::array<int, 2> file_version;
1358
  read_attribute(ww_file, "version", file_version);
10✔
1359
  if (file_version[0] != VERSION_WEIGHT_WINDOWS[0]) {
10!
1360
    std::string err_msg =
1361
      fmt::format("File '{}' has version {} which is incompatible with the "
1362
                  "expected version ({}).",
1363
        name, file_version, VERSION_WEIGHT_WINDOWS);
×
1364
    set_errmsg(err_msg);
×
1365
    return OPENMC_E_INVALID_ARGUMENT;
×
1366
  }
×
1367

1368
  hid_t weight_windows_group = open_group(ww_file, "weight_windows");
10✔
1369

1370
  hid_t mesh_group = open_group(ww_file, "meshes");
10✔
1371

1372
  read_meshes(mesh_group);
10✔
1373

1374
  std::vector<std::string> names = group_names(weight_windows_group);
10✔
1375

1376
  for (const auto& name : names) {
20✔
1377
    WeightWindows::from_hdf5(weight_windows_group, name);
10✔
1378
  }
1379

1380
  close_group(weight_windows_group);
10✔
1381

1382
  file_close(ww_file);
10✔
1383

1384
  return 0;
10✔
1385
}
10✔
1386

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