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

openmc-dev / openmc / 30025709467

23 Jul 2026 04:34PM UTC coverage: 81.336% (-0.08%) from 81.413%
30025709467

Pull #4026

github

web-flow
Merge 805f7cecb into a533fcd7f
Pull Request #4026: Add domain decomposition for random ray solver

19250 of 28006 branches covered (68.74%)

Branch coverage included in aggregate %.

1186 of 1209 new or added lines in 12 files covered. (98.1%)

61110 of 70794 relevant lines covered (86.32%)

49022453.97 hits per line

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

93.27
/src/random_ray/decomposition_map.cpp
1
#include "openmc/random_ray/decomposition_map.h"
2

3
#include "openmc/cell.h"
4
#include "openmc/constants.h"
5
#include "openmc/message_passing.h"
6
#include "openmc/mgxs_interface.h"
7
#include "openmc/random_lcg.h"
8
#include "openmc/random_ray/flat_source_domain.h"
9
#include "openmc/random_ray/random_ray.h"
10
#include "openmc/simulation.h"
11
#include "openmc/timer.h"
12
#include "openmc/vector.h"
13
#include <numeric>
14

15
#ifdef OPENMC_MPI
16

17
namespace openmc {
18

19
namespace mpi {
20
DecompositionMap decomp_map;
21
}
22

23
// Constructor
24
DecompositionMap::DecompositionMap() {}
3,272✔
25

26
void DecompositionMap::initialize()
464✔
27
{
28
  negroups_ = data::mg.num_energy_groups_;
464✔
29
  estimated_rank_load_fractions_.resize(mpi::n_procs, 0.0);
464✔
30
  measured_rank_load_fractions_.resize(mpi::n_procs, 0.0);
464✔
31
  estimated_rank_load_totals_.resize(mpi::n_procs, 0.0);
464✔
32
  target_load_ = 1.0 / mpi::n_procs;
464✔
33
  rank_weights_.resize(mpi::n_procs, 1.0);
464✔
34

35
  spatial_box_ = dynamic_cast<SpatialBox*>(
928!
36
    dynamic_cast<IndependentSource*>(RandomRay::ray_source_.get())->space());
464!
37

38
  double x_length =
464✔
39
    spatial_box_->upper_right().x - spatial_box_->lower_left().x;
464✔
40
  double y_length =
464✔
41
    spatial_box_->upper_right().y - spatial_box_->lower_left().y;
464✔
42
  double z_length =
464✔
43
    spatial_box_->upper_right().z - spatial_box_->lower_left().z;
464✔
44

45
  max_domain_length_ =
928✔
46
    sqrt(x_length * x_length + y_length * y_length + z_length * z_length);
464✔
47

48
  is_linear_ = RandomRay::source_shape_ != RandomRaySourceShape::FLAT;
464✔
49

50
  // Count the number of source regions in the model
51
  n_base_sr_ = 0;
464✔
52
  for (const auto& c : model::cells) {
3,664✔
53
    if (c->type_ == Fill::MATERIAL) {
3,200✔
54
      n_base_sr_ += c->n_instances();
1,800✔
55
    }
56
  }
57

58
  num_base_source_region_RT_.resize(n_base_sr_, 0);
464✔
59
  num_mesh_bin_RT_.resize(n_base_sr_, 0);
464✔
60
  ray_tracing_cost_.resize(n_base_sr_);
464✔
61
  volume_base_sr_.resize(n_base_sr_, 0.0);
464✔
62
}
464✔
63

64
void DecompositionMap::generate_rank_centers()
416✔
65
{
66

67
  // Calculate grid points that are used for Voronoi cells
68
  int grid_points_total = grid_points_per_rank_ * mpi::n_procs;
416✔
69
  if (mpi::master)
416✔
70
    printf("Calculating %d grid points for Voronoi tessellation...\n",
208✔
71
      grid_points_total);
72
  calculate_grid_points(grid_points_total);
416✔
73

74
  // Initialize points with random positions
75
  initialize_voronoi_centers();
416✔
76

77
  double err = 1.0;
416✔
78
  double precision = 1e-3; // corresponding to 0.001 cm position change
416✔
79
  int it = 0;
416✔
80
  int max_iterations = 100;
416✔
81

82
  // Lloyd's algorithm to move Voronoi centers to centroids of their cells
83
  // https://en.wikipedia.org/wiki/Lloyd%27s_algorithm
84
  while (err > precision && it < max_iterations) {
4,912!
85
    // Reset error to determine maximum movement below
86
    err = 0.0;
4,080✔
87

88
    vector<Position> position_sum_per_rank(mpi::n_procs, Position(0, 0, 0));
4,080✔
89
    vector<int> num_points_per_rank(mpi::n_procs, 0);
4,080✔
90

91
    // Compute Voronoi cells by summing up all position values of mesh grid
92
    // points that are closest to a Voronoi center
93
    calculate_voronoi(position_sum_per_rank, num_points_per_rank);
4,080✔
94

95
    for (int rank = 0; rank < mpi::n_procs; rank++) {
12,240✔
96

97
      // Calculate centroid of the cell
98
      Position centroid = calculate_centroids(
8,160✔
99
        position_sum_per_rank[rank], num_points_per_rank[rank], rank);
8,160✔
100

101
      // Calculate movement for convergence check
102
      double movement = (centroid - rank_centers_[rank]).norm();
8,160✔
103

104
      // Move rank center to centroid
105
      rank_centers_[rank] = centroid;
8,160✔
106

107
      // Record maximum movement
108
      if (movement > err) {
8,160✔
109
        err = movement;
5,208✔
110
      }
111
    }
112

113
    it++;
4,080✔
114
  }
4,080✔
115

116
  if (mpi::master) {
416✔
117
    if (it == max_iterations) {
208!
NEW
118
      warning("Lloyd's algorithm did not converge within the maximum number of "
×
119
              "iterations.");
120
    } else {
121
      printf("Lloyd's algorithm converged in %d iterations.\n", it);
208✔
122
    }
123
  }
124
}
416✔
125

126
// Calculate grid points needed for calculating Voronoi cells
127
void DecompositionMap::calculate_grid_points(int grid_points_total)
416✔
128
{
129

130
  // Calculate length along each dimension
131
  vector<double> domain_length(3);
416✔
132
  domain_length[0] =
416✔
133
    spatial_box_->upper_right().x - spatial_box_->lower_left().x;
416✔
134
  domain_length[1] =
416✔
135
    spatial_box_->upper_right().y - spatial_box_->lower_left().y;
416✔
136
  domain_length[2] =
416✔
137
    spatial_box_->upper_right().z - spatial_box_->lower_left().z;
416✔
138

139
  double volume = domain_length[0] * domain_length[1] * domain_length[2];
416✔
140

141
  // For each dimension, determine grid points along that direction based on
142
  // aspect ratio: domain_length / volume^(1/3) = grid_points_dimension /
143
  // grid_points_total^(1/3). Check if any dimension is so distorted that it
144
  // would only receive minimum of 1 grid point and flag that direction to
145
  // correct total number of grid points.
146
  int excluded_dimension = -1;
416✔
147
  vector<int> grid_points_per_dimension(3);
416✔
148
  for (int i = 0; i < 3; i++) {
1,664✔
149
    double grid_points_estimate =
1,248✔
150
      ((domain_length[i] / cbrt(volume)) * cbrt(grid_points_total));
1,248✔
151

152
    if (grid_points_estimate > 1) {
1,248!
153
      grid_points_per_dimension[i] = round(grid_points_estimate);
1,248✔
154
    } else {
NEW
155
      excluded_dimension = i;
×
NEW
156
      grid_points_per_dimension[i] = 1;
×
157
    }
158
  }
159

160
  // If problem is 2D, exclude z direction
161
  if (RandomRay::geom_dim_ == RandomRayGeomDim::TWO_DIM) {
416✔
162
    excluded_dimension = 2;
200✔
163
    grid_points_per_dimension[2] = 1;
200✔
164
  }
165

166
  // If one dimension is excluded, recalculate grid points in other two
167
  // dimensions based on area.
168
  if (excluded_dimension != -1) {
416!
169
    double area = 1.0;
170

171
    for (int i = 0; i < 3; i++) {
800✔
172
      if (i == excluded_dimension)
600✔
173
        continue;
200✔
174
      area *= domain_length[i];
400✔
175
    }
176

177
    for (int i = 0; i < 3; i++) {
800✔
178
      if (i == excluded_dimension)
600✔
179
        continue;
200✔
180
      grid_points_per_dimension[i] =
800✔
181
        round((domain_length[i] / sqrt(area)) * sqrt(grid_points_total));
400✔
182
    }
183
  }
184

185
  // Adjust grid points in each dimension to match actual total number of grid
186
  // points to the number of grid points requested
187
  double new_total = grid_points_per_dimension[0] *
416✔
188
                     grid_points_per_dimension[1] *
416✔
189
                     grid_points_per_dimension[2];
416✔
190
  double adjustment;
416✔
191
  if (excluded_dimension != -1) {
416✔
192
    // When one dimension is excluded, use square root for the other two
193
    // dimensions
194
    adjustment = sqrt(grid_points_total / new_total);
200✔
195
  } else {
196
    // When all dimensions are used, use cubic root
197
    adjustment = cbrt(grid_points_total / new_total);
216✔
198
  }
199

200
  // Multiply adjustment factor
201
  for (int i = 0; i < 3; i++) {
1,664✔
202
    if (i == excluded_dimension)
1,248✔
203
      continue;
200✔
204
    grid_points_per_dimension[i] =
1,048✔
205
      round(grid_points_per_dimension[i] * adjustment);
1,048✔
206
  }
207

208
  // Calculate spacing between grid points in each dimension
209
  vector<double> delta_value(3, 0.0);
416✔
210

211
  for (int i = 0; i < 3; i++) {
1,664✔
212
    if (grid_points_per_dimension[i] > 1) {
1,248✔
213
      delta_value[i] =
1,048✔
214
        (domain_length[i] - 2 * TINY_BIT) / (grid_points_per_dimension[i] - 1);
1,048✔
215
    }
216
  }
217

218
  // Initialize point at center of domain (in case of only 1 grid point in a
219
  // dimension)
220
  double x = spatial_box_->lower_left().x + domain_length[0] * 0.5;
416✔
221
  double y = spatial_box_->lower_left().y + domain_length[1] * 0.5;
416✔
222
  double z = spatial_box_->lower_left().z + domain_length[2] * 0.5;
416✔
223

224
  // Generate all grid points
225
  for (int i = 0; i < grid_points_per_dimension[0]; i++) {
5,040✔
226
    if (grid_points_per_dimension[0] > 1) {
4,624!
227
      x = spatial_box_->lower_left().x + TINY_BIT + i * delta_value[0];
4,624✔
228
    }
229
    for (int j = 0; j < grid_points_per_dimension[1]; j++) {
63,600✔
230
      if (grid_points_per_dimension[1] > 1) {
58,976!
231
        y = spatial_box_->lower_left().y + TINY_BIT + j * delta_value[1];
58,976✔
232
      }
233
      for (int k = 0; k < grid_points_per_dimension[2]; k++) {
156,832✔
234
        if (grid_points_per_dimension[2] > 1) {
97,856✔
235
          z = spatial_box_->lower_left().z + TINY_BIT + k * delta_value[2];
46,656✔
236
        }
237
        // Add grid point
238
        grid_points_.push_back({x, y, z});
97,856✔
239
      }
240
    }
241
  }
242

243
  // Check if mesh grid points are inside spatial domain, which can be different
244
  // from a box. If not, erase them.
245
  for (int i = grid_points_.size() - 1; i >= 0; i--) {
98,272✔
246
    Position xi = grid_points_[i];
97,856✔
247

248
    bool is_inside_domain =
97,856✔
249
      RandomRay::ray_source_->satisfies_spatial_constraints(xi);
97,856✔
250

251
    if (!is_inside_domain) {
97,856!
NEW
252
      grid_points_.erase(grid_points_.begin() + i);
×
253
    }
254
  }
255

256
  if (mpi::master && grid_points_.size() < grid_points_total) {
416✔
257
    warning(fmt::format("Spatial constraints reduced grid points for Voronoi "
135✔
258
                        "tesselation from {} to {}.",
259
      grid_points_total, grid_points_.size()));
189✔
260
  }
261
}
416✔
262

263
// Places random points in the spatial domain.
264
// Each point corresponds to the initial center of a rank.
265
void DecompositionMap::initialize_voronoi_centers()
416✔
266
{
267
  rank_centers_.resize(mpi::n_procs);
416✔
268

269
  uint64_t seed = openmc_get_seed();
416✔
270
  int rank_cnt = 0;
416✔
271

272
  // Sample random positions to start with
273
  while (rank_cnt < mpi::n_procs) {
1,664✔
274

275
    double x = prn(&seed);
832✔
276
    double y = prn(&seed);
832✔
277
    double z = 0.0;
832✔
278
    if (RandomRay::geom_dim_ == RandomRayGeomDim::THREE_DIM) {
832✔
279
      z = prn(&seed);
432✔
280
    } else {
281
      z = 0.5; // Mid-plane in z direction
282
    }
283

284
    Position xi {x, y, z};
832✔
285

286
    // Make a small shift in position to avoid geometry floating point issues at
287
    // boundaries
288
    Position shift {FP_COINCIDENT, FP_COINCIDENT, FP_COINCIDENT};
832✔
289
    xi = (spatial_box_->lower_left() + shift) +
1,664✔
290
         xi * ((spatial_box_->upper_right() - shift) -
832✔
291
                (spatial_box_->lower_left() + shift));
832✔
292

293
    bool is_inside_domain =
832✔
294
      RandomRay::ray_source_->satisfies_spatial_constraints(xi);
832✔
295

296
    if (is_inside_domain) {
832!
297
      rank_centers_[rank_cnt] = xi;
832✔
298
      rank_cnt++;
832✔
299
    }
300
  }
301
}
416✔
302

303
// Determine the distance of each mesh grid point to all rank centers.
304
// Sum up the positions of all mesh grid points that are closest to a given rank
305
// center for computation of centroid later. Record number of grid points per
306
// rank center.
307
void DecompositionMap::calculate_voronoi(
4,080✔
308
  vector<Position>& position_sum_per_rank, vector<int>& num_points_per_rank)
309
{
310

311
// Assign each point to the closest rank center
312
#pragma omp parallel for schedule(static)
3,060✔
313
  for (int p = 0; p < grid_points_.size(); p++) {
242,700✔
314
    Position point = grid_points_[p];
241,680✔
315
    int closest_rank = C_NONE;
241,680✔
316
    double min_distance = INFTY;
241,680✔
317

318
    // Find closest rank center
319
    for (int rank = 0; rank < mpi::n_procs; rank++) {
725,040✔
320
      double dist = (point - rank_centers_[rank]).norm();
483,360✔
321
      // Power Voronoi diagram uses squared distances
322
      dist = dist * dist - rank_weights_[rank];
483,360✔
323

324
      if (dist < min_distance) {
483,360✔
325
        min_distance = dist;
362,388✔
326
        closest_rank = rank;
362,388✔
327
      }
328
    }
329

330
    if (mpi::master && closest_rank == C_NONE) {
241,680!
331
      fatal_error("Could not find closest rank for Voronoi cell point " +
×
332
                  std::to_string(p) + ".");
×
333
    }
334

335
// Accumulate point coordinates for the closest rank
336
#pragma omp atomic
337
    position_sum_per_rank[closest_rank].x += point.x;
241,680✔
338
#pragma omp atomic
339
    position_sum_per_rank[closest_rank].y += point.y;
241,680✔
340
#pragma omp atomic
341
    position_sum_per_rank[closest_rank].z += point.z;
241,680✔
342

343
// Record number of mesh grid points for closest rank
344
#pragma omp atomic
345
    num_points_per_rank[closest_rank]++;
241,680✔
346
  }
347
}
4,080✔
348

349
Position DecompositionMap::calculate_centroids(
8,160✔
350
  const Position position_sum, const int num_points, int rank)
351
{
352

353
  // check if any points have been recorded in rank
354
  if (num_points == 0) {
8,160!
NEW
355
    fatal_error("Rank " + std::to_string(rank) +
×
356
                " has no Voronoi cell points. This indicates that the number "
357
                "of grid points for the Voronoi tesselation is too coarse. "
358
                "Requires source code modificaiton to fix.");
359
  }
360

361
  Position centroid = position_sum;
8,160✔
362

363
  // Divide by number of points
364
  double n = static_cast<double>(num_points);
8,160✔
365
  centroid.x /= n;
8,160✔
366
  centroid.y /= n;
8,160✔
367
  centroid.z /= n;
8,160✔
368

369
  return centroid;
8,160✔
370
}
371

372
bool DecompositionMap::any_discovered_source_regions(
10,000✔
373
  ParallelMap<SourceRegionKey, SourceRegion, SourceRegionKey::HashFunctor>&
374
    discovered_source_regions)
375
{
376

377
  simulation::time_decomposition_handling.start();
10,000✔
378

379
  int flag = 0;
10,000✔
380
  if (discovered_source_regions.begin() != discovered_source_regions.end()) {
10,000✔
381
    flag = 1;
648✔
382
  }
383

384
  MPI_Allreduce(MPI_IN_PLACE, &flag, 1, MPI_INT, MPI_MAX, mpi::intracomm);
10,000✔
385

386
  return flag > 0;
10,000✔
387

388
  simulation::time_decomposition_handling.start();
389
}
390

391
void DecompositionMap::exchange_sr_info(
656✔
392
  ParallelMap<SourceRegionKey, SourceRegion, SourceRegionKey::HashFunctor>&
393
    discovered_source_regions)
394
{
395

396
  // Communicate maps
397
  for (int rank = 0; rank < mpi::n_procs; rank++) {
1,968✔
398

399
    // Send size
400
    uint64_t bcast_size = 0;
1,312✔
401
    if (rank == mpi::rank) {
1,312✔
402
      for (const auto& pair : discovered_source_regions) {
1,767,356✔
403
        // Only broadcast source regions that have non-zero volume, i.e. regions
404
        // discovered during active phase of ray
405
        if (pair.second.scalars_.volume_ > 0.0) {
883,022✔
406
          bcast_size++;
846,018✔
407
        }
408
      }
409
    }
410

411
    MPI_Bcast(&bcast_size, 1, MPI_UINT64_T, rank, mpi::intracomm);
1,312✔
412

413
    if (bcast_size > 0) {
1,312✔
414

415
      vector<int64_t> local_base_ids(bcast_size);
1,296✔
416
      vector<int64_t> local_mesh_bins(bcast_size);
1,296✔
417

418
      if (rank == mpi::rank) {
1,296✔
419
        // fill in vectors to be sent
420
        int i = 0;
648✔
421
        for (const auto& pair : discovered_source_regions) {
1,767,340✔
422
          if (pair.second.scalars_.volume_ > 0.0) {
883,022✔
423
            SourceRegionKey sr_key = pair.first;
846,018✔
424
            local_base_ids[i] = sr_key.base_source_region_id;
846,018✔
425
            local_mesh_bins[i] = sr_key.mesh_bin;
846,018✔
426
            i++;
846,018✔
427
          }
428
        }
429
      }
430

431
      // Broadcast all data
432
      MPI_Bcast(
1,296✔
433
        local_base_ids.data(), bcast_size, MPI_INT64_T, rank, mpi::intracomm);
1,296✔
434
      MPI_Bcast(
1,296✔
435
        local_mesh_bins.data(), bcast_size, MPI_INT64_T, rank, mpi::intracomm);
1,296✔
436

437
      // Update subdomain map
438
      for (int j = 0; j < bcast_size; j++) {
1,693,332✔
439

440
        SourceRegionKey sr_key(local_base_ids[j], local_mesh_bins[j]);
1,692,036✔
441

442
        // check if already in map or not, i.e. has someone else discovered that
443
        // region already?
444
        if (subdomain_map_.find(sr_key) == subdomain_map_.end()) {
1,692,036✔
445
          subdomain_map_[sr_key] = rank;
1,691,056✔
446
        } else {
447
          int resident_rank = subdomain_map_[sr_key]; // current owner
980✔
448
          int challenger_rank = rank; // current broadcasting rank
980✔
449

450
          double ratio_resident = calculate_load_ratio(resident_rank);
980✔
451
          double ratio_challenger = calculate_load_ratio(challenger_rank);
980✔
452
          double resident_rank_load =
980✔
453
            ratio_resident * estimated_rank_load_totals_[resident_rank];
980✔
454
          double challenger_rank_load =
980✔
455
            ratio_challenger * estimated_rank_load_totals_[challenger_rank];
980✔
456

457
          // If load of challenger rank is lower, assign source region to that
458
          // rank, otherwise resident keeps it
459
          int sender;
980✔
460
          int receiver;
980✔
461
          if (challenger_rank_load < resident_rank_load) {
980✔
462
            subdomain_map_[sr_key] = challenger_rank;
538✔
463
            sender = resident_rank;
538✔
464
            receiver = challenger_rank;
538✔
465
          } else {
466
            // resident keeps it
467
            sender = challenger_rank;
468
            receiver = resident_rank;
469
          }
470

471
          // Broadcast load of exchanged source region such that each rank can
472
          // update load balance
473
          double bcast_load = 0;
980✔
474
          if (mpi::rank == sender) {
980✔
475
            SourceRegion& contested_sr = discovered_source_regions[sr_key];
490✔
476
            double volume_sr = contested_sr.scalars_.volume_;
490✔
477
            bcast_load =
490✔
478
              C1_ *
490✔
479
                (contested_sr.scalars_.n_hits_ / simulation::current_batch) *
490✔
480
                negroups_ +
490✔
481
              volume_sr * ray_tracing_cost_[sr_key.base_source_region_id];
490✔
482
          }
483
          MPI_Bcast(&bcast_load, 1, MPI_DOUBLE, sender, mpi::intracomm);
980✔
484

485
          double load_change_fraction =
980✔
486
            bcast_load / estimated_load_sum_; // load fraction update
980✔
487
          estimated_rank_load_fractions_[sender] -= load_change_fraction;
980✔
488
          estimated_rank_load_fractions_[receiver] += load_change_fraction;
980✔
489
          double load_change_total = bcast_load; // load total update
980✔
490
          estimated_rank_load_totals_[sender] -= load_change_total;
980✔
491
          estimated_rank_load_totals_[receiver] += load_change_total;
980✔
492

493
          // Communicate source region data and merge on receiver side
494
          if (mpi::rank == sender) {
980✔
495
            SourceRegion& sr = discovered_source_regions[sr_key];
490✔
496
            send_sr_data(receiver, sr);
490✔
497

498
            // clear old source region data from discovered regions map
499
            discovered_source_regions.erase(sr_key);
490✔
500
          }
501
          if (mpi::rank == receiver) {
980✔
502
            SourceRegion& sr = discovered_source_regions[sr_key];
490✔
503
            SourceRegion sr_recv(negroups_, is_linear_);
490✔
504
            receive_sr_data(sender, sr_recv);
490✔
505

506
            sr.merge(sr_recv, is_linear_);
490✔
507
          }
490✔
508
        }
509
      }
510
    }
1,296✔
511
  }
512
}
656✔
513

514
void DecompositionMap::send_sr_data(int receiver, SourceRegion& sr_send)
102,044✔
515
{
516

517
  int num_scalar_messages = 1;
102,044✔
518
  //! NOTE: update if new vector fields are added to SourceExchangeVectors
519
  //! struct
520
  int num_vector_messages = 4;
102,044✔
521
  if (is_linear_) {
102,044✔
522
    num_vector_messages += 4;
36,761✔
523
  }
524
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
102,044✔
525
    num_vector_messages += 1;
98,647✔
526
  }
527
  int num_requests = num_scalar_messages + num_vector_messages;
102,044✔
528

529
  vector<MPI_Request> requests(num_requests);
102,044✔
530
  int req_idx = 0;
102,044✔
531

532
  // Send scalar data to receiver
533
  MPI_Isend(&sr_send.scalars_, sizeof(ScalarSourceRegionFields), MPI_BYTE,
102,044✔
534
    receiver, 1, mpi::intracomm, &requests[req_idx]);
102,044✔
535
  req_idx++;
102,044✔
536

537
  // Send vector data to receiver
538
  // Tags hardcoded to avoid confusion if new fields are not added sequentially
539
  MPI_Isend(sr_send.scalar_flux_old_.data(), negroups_, MPI_DOUBLE, receiver, 2,
102,044✔
540
    mpi::intracomm, &requests[req_idx]);
102,044✔
541
  req_idx++;
102,044✔
542

543
  MPI_Isend(sr_send.scalar_flux_new_.data(), negroups_, MPI_DOUBLE, receiver, 3,
102,044✔
544
    mpi::intracomm, &requests[req_idx]);
102,044✔
545
  req_idx++;
102,044✔
546

547
  MPI_Isend(sr_send.source_.data(), negroups_, MPI_FLOAT, receiver, 4,
102,044✔
548
    mpi::intracomm, &requests[req_idx]);
102,044✔
549
  req_idx++;
102,044✔
550

551
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
102,044✔
552
    MPI_Isend(sr_send.external_source_.data(), negroups_, MPI_FLOAT, receiver,
98,647✔
553
      5, mpi::intracomm, &requests[req_idx]);
98,647✔
554
    req_idx++;
555
  }
556

557
  MPI_Isend(sr_send.scalar_flux_final_.data(), negroups_, MPI_DOUBLE, receiver,
102,044✔
558
    6, mpi::intracomm, &requests[req_idx]);
102,044✔
559
  req_idx++;
102,044✔
560

561
  if (is_linear_) {
102,044✔
562
    MPI_Isend(sr_send.source_gradients_.data(), 3 * negroups_, MPI_DOUBLE,
36,761✔
563
      receiver, 7, mpi::intracomm, &requests[req_idx]);
36,761✔
564
    req_idx++;
36,761✔
565

566
    MPI_Isend(sr_send.flux_moments_old_.data(), 3 * negroups_, MPI_DOUBLE,
36,761✔
567
      receiver, 8, mpi::intracomm, &requests[req_idx]);
36,761✔
568
    req_idx++;
36,761✔
569

570
    MPI_Isend(sr_send.flux_moments_new_.data(), 3 * negroups_, MPI_DOUBLE,
36,761✔
571
      receiver, 9, mpi::intracomm, &requests[req_idx]);
36,761✔
572
    req_idx++;
36,761✔
573

574
    MPI_Isend(sr_send.flux_moments_t_.data(), 3 * negroups_, MPI_DOUBLE,
36,761✔
575
      receiver, 10, mpi::intracomm, &requests[req_idx]);
36,761✔
576
    req_idx++;
36,761✔
577
  }
578

579
  if (req_idx != num_requests) {
102,044!
NEW
580
    fatal_error(fmt::format(
×
581
      "Number of MPI requests does not match number of messages sent."
582
      "Check if num_vector_messages corresponds to number of transferred "
583
      "vectors."));
584
  }
585

586
  // Wait for all communication to complete
587
  MPI_Waitall(num_requests, requests.data(), MPI_STATUSES_IGNORE);
102,044✔
588
}
102,044✔
589

590
void DecompositionMap::receive_sr_data(int sender, SourceRegion& sr_recv)
102,044✔
591
{
592

593
  // Receive scalar data from sender
594
  MPI_Recv(&sr_recv.scalars_, sizeof(ScalarSourceRegionFields), MPI_BYTE,
102,044✔
595
    sender, 1, mpi::intracomm, MPI_STATUS_IGNORE);
596

597
  // Receive vector data from sender
598
  MPI_Recv(sr_recv.scalar_flux_old_.data(), negroups_, MPI_DOUBLE, sender, 2,
102,044✔
599
    mpi::intracomm, MPI_STATUS_IGNORE);
600
  MPI_Recv(sr_recv.scalar_flux_new_.data(), negroups_, MPI_DOUBLE, sender, 3,
102,044✔
601
    mpi::intracomm, MPI_STATUS_IGNORE);
602
  MPI_Recv(sr_recv.source_.data(), negroups_, MPI_FLOAT, sender, 4,
102,044✔
603
    mpi::intracomm, MPI_STATUS_IGNORE);
604

605
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
102,044✔
606
    MPI_Recv(sr_recv.external_source_.data(), negroups_, MPI_FLOAT, sender, 5,
98,647✔
607
      mpi::intracomm, MPI_STATUS_IGNORE);
608
  }
609

610
  MPI_Recv(sr_recv.scalar_flux_final_.data(), negroups_, MPI_DOUBLE, sender, 6,
102,044✔
611
    mpi::intracomm, MPI_STATUS_IGNORE);
612

613
  if (is_linear_) {
102,044✔
614
    MPI_Recv(sr_recv.source_gradients_.data(), 3 * negroups_, MPI_DOUBLE,
36,761✔
615
      sender, 7, mpi::intracomm, MPI_STATUS_IGNORE);
616
    MPI_Recv(sr_recv.flux_moments_old_.data(), 3 * negroups_, MPI_DOUBLE,
36,761✔
617
      sender, 8, mpi::intracomm, MPI_STATUS_IGNORE);
618
    MPI_Recv(sr_recv.flux_moments_new_.data(), 3 * negroups_, MPI_DOUBLE,
36,761✔
619
      sender, 9, mpi::intracomm, MPI_STATUS_IGNORE);
620
    MPI_Recv(sr_recv.flux_moments_t_.data(), 3 * negroups_, MPI_DOUBLE, sender,
36,761✔
621
      10, mpi::intracomm, MPI_STATUS_IGNORE);
622
  }
623
}
102,044✔
624

625
int DecompositionMap::find_owner(SourceRegionKey sr_key, Position r,
524,017,989✔
626
  ParallelMap<SourceRegionKey, SourceRegion, SourceRegionKey::HashFunctor>&
627
    discovered_source_regions)
628
{
629

630
  // Check if source region key is in subdomain map
631
  auto it = subdomain_map_.find(sr_key);
524,017,989✔
632
  if (it != subdomain_map_.end()) {
524,017,989✔
633
    return it->second;
505,417,195✔
634
  }
635

636
  // Check if already recorded in newly discovered source regions
637
  discovered_source_regions.lock(sr_key);
18,600,794✔
638
  bool sr_key_discovered = discovered_source_regions.contains(sr_key);
18,600,794✔
639
  discovered_source_regions.unlock(sr_key);
18,600,794✔
640
  if (sr_key_discovered) {
18,600,794✔
641
    return mpi::rank;
17,047,997✔
642
  }
643

644
  // If not found in either map, check which rank owns source
645
  // region beased on location
646
  int closest_rank = find_closest_rank(r, true);
1,552,797✔
647
  return closest_rank;
648
}
649

650
int DecompositionMap::find_closest_rank(Position r, bool test_all_ranks)
30,309,468✔
651
{
652

653
  int closest_rank = C_NONE;
30,309,468✔
654
  double min_distance = INFTY;
30,309,468✔
655
  vector<int> test_ranks;
30,309,468✔
656

657
  if (test_all_ranks) {
30,309,468✔
658
    test_ranks.resize(mpi::n_procs);
13,748,140✔
659
    std::iota(test_ranks.begin(), test_ranks.end(),
13,748,140✔
660
      0); // fill with 0, 1, ..., n_procs-1
661
  } else {
662
    // convert unordered set of neighboring ranks to vector and add self rank
663
    test_ranks = vector<int>(mpi::decomp_map.my_neighbors_.begin(),
16,561,328✔
664
      mpi::decomp_map.my_neighbors_.end());
16,561,328✔
665
    test_ranks.push_back(mpi::rank);
16,561,328✔
666
  }
667

668
  // Find closest rank center
669
  for (int rank : test_ranks) {
90,928,404✔
670
    double dist = (r - rank_centers_[rank]).norm();
60,618,936✔
671
    // Distance function corresponding to weighted power Voronoi diagram
672
    dist = dist * dist - rank_weights_[rank];
60,618,936✔
673
    if (dist < min_distance) {
60,618,936✔
674
      min_distance = dist;
52,995,399✔
675
      closest_rank = rank;
52,995,399✔
676
    }
677
  }
678

679
  if (mpi::master && closest_rank == C_NONE) {
30,309,468!
NEW
680
    fatal_error(
×
NEW
681
      "Could not find closest rank for new source region at position (" +
×
NEW
682
      std::to_string(r.x) + ", " + std::to_string(r.y) + ", " +
×
NEW
683
      std::to_string(r.z) + ").");
×
684
  }
685

686
  return closest_rank;
30,309,468✔
687
}
30,309,468✔
688

689
void DecompositionMap::calculate_rank_load(
2,320✔
690
  FlatSourceDomain* domain, double batch_transport_time)
691
{
692

693
  // Reset local volumes of base source regions, which might change when source
694
  // regions change rank ownership
695
  std::fill(volume_base_sr_.begin(), volume_base_sr_.end(), 0.0);
2,320✔
696

697
  // Add volumes of newly discovered source regions
698
  vector<uint64_t> mesh_bins_per_base_sr_local(n_base_sr_, 0);
2,320✔
699
  for (const auto& [sr_key, sr] : domain->discovered_source_regions_) {
886,386✔
700
    volume_base_sr_[sr_key.base_source_region_id] += sr.scalars_.volume_;
881,746✔
701
  }
702

703
  // Add volumes of known source regions
704
  for (int64_t sr = 0; sr < domain->n_source_regions(); sr++) {
3,912,256✔
705
    SourceRegionKey sr_key = domain->source_regions_.key(sr);
3,909,936✔
706
    uint64_t base_sr = sr_key.base_source_region_id;
3,909,936✔
707
    volume_base_sr_[base_sr] += domain->source_regions_.volume_t(sr);
3,909,936✔
708
    volume_base_sr_[base_sr] += domain->source_regions_.volume(sr);
3,909,936✔
709
  }
710

711
// Calculate ray tracing cost per base source region
712
#pragma omp parallel for
1,740✔
713
  for (uint64_t bsr = 0; bsr < n_base_sr_; bsr++) {
570,820✔
714
    if (volume_base_sr_[bsr] > 0.0) {
570,240✔
715
      ray_tracing_cost_[bsr] =
287,468✔
716
        (C2_ * static_cast<double>(num_base_source_region_RT_[bsr]) +
287,468✔
717
          C3_ * static_cast<double>(num_mesh_bin_RT_[bsr])) /
287,468✔
718
        volume_base_sr_[bsr];
287,468✔
719
    } else {
720
      ray_tracing_cost_[bsr] = 0.0;
282,772✔
721
    }
722
  }
723

724
  // Accumulate load of known source regions
725
  double local_estimated_load = 0.0;
2,320✔
726
#pragma omp parallel for reduction(+ : local_estimated_load)
1,740✔
727
  for (int64_t sr = 0; sr < domain->n_source_regions(); sr++) {
978,064✔
728
    SourceRegionKey sr_key = domain->source_regions_.key(sr);
977,484✔
729
    uint64_t base_sr = sr_key.base_source_region_id;
977,484✔
730

731
    // Calculate volume of unique source region to weight volume-dependent ray
732
    // tracing cost
733
    double volume_sr =
977,484✔
734
      domain->source_regions_.volume_t(sr) + domain->source_regions_.volume(sr);
977,484✔
735

736
    // Calculate load of source region
737
    double load_sr =
977,484✔
738
      C1_ * (domain->source_regions_.n_hits(sr) / simulation::current_batch) *
977,484✔
739
        negroups_ +
977,484✔
740
      volume_sr * ray_tracing_cost_[base_sr];
977,484✔
741

742
    // Accumulate to local estimated load
743
    local_estimated_load += load_sr;
977,484✔
744
  }
745

746
  // Accumulate load of newly discovered source regions
747
  for (const auto& [sr_key, sr] : domain->discovered_source_regions_) {
886,386✔
748
    uint64_t base_sr = sr_key.base_source_region_id;
881,746✔
749
    double volume_sr = sr.scalars_.volume_;
881,746✔
750
    double load_sr =
881,746✔
751
      C1_ * (sr.scalars_.n_hits_ / simulation::current_batch) * negroups_ +
881,746✔
752
      volume_sr * ray_tracing_cost_[base_sr];
881,746✔
753
    local_estimated_load += load_sr;
881,746✔
754
  }
755

756
  // Communicate estimated load across ranks
757
  MPI_Allgather(&local_estimated_load, 1, MPI_DOUBLE,
2,320✔
758
    estimated_rank_load_totals_.data(), 1, MPI_DOUBLE, mpi::intracomm);
2,320✔
759
  estimated_load_sum_ = std::accumulate(estimated_rank_load_totals_.begin(),
2,320✔
760
    estimated_rank_load_totals_.end(), 0.0);
761

762
  // Communicate measured load across ranks
763
  MPI_Allgather(&batch_transport_time, 1, MPI_DOUBLE,
2,320✔
764
    measured_rank_load_fractions_.data(), 1, MPI_DOUBLE, mpi::intracomm);
2,320✔
765
  double measured_load_sum =
2,320✔
766
    std::accumulate(measured_rank_load_fractions_.begin(),
2,320✔
767
      measured_rank_load_fractions_.end(), 0.0);
768

769
  // Calculate fractions
770
  for (int rank = 0; rank < mpi::n_procs; rank++) {
6,960✔
771
    estimated_rank_load_fractions_[rank] =
4,640✔
772
      estimated_rank_load_totals_[rank] / estimated_load_sum_;
4,640✔
773
    measured_rank_load_fractions_[rank] =
4,640✔
774
      measured_rank_load_fractions_[rank] / measured_load_sum;
4,640✔
775
  }
776

777
  // Reset ray trace counters
778
  fill(num_base_source_region_RT_.begin(), num_base_source_region_RT_.end(), 0);
2,320✔
779
  fill(num_mesh_bin_RT_.begin(), num_mesh_bin_RT_.end(), 0);
4,640✔
780
}
2,320✔
781

782
void DecompositionMap::balance_load(FlatSourceDomain* domain)
2,320✔
783
{
784

785
  // Optimization parameters
786
  int max_iterations = 200;
2,320✔
787
  int it_outer = 0;
2,320✔
788
  double adaptation_factor = 1;
2,320✔
789
  double min_adaptation_factor = 0.01;
2,320✔
790
  double max_adaptation_factor = 2;
2,320✔
791
  double avg_rank_distance =
2,320✔
792
    max_domain_length_ /
2,320✔
793
    cbrt(mpi::n_procs); // rough estimate of average distance between ranks
2,320✔
794
  double weight_scale =
2,320✔
795
    avg_rank_distance * avg_rank_distance * optimization_history_factor_;
2,320✔
796
  double beta = 0.6; // momentum damping
2,320✔
797
  bool check_all_ranks = true;
2,320✔
798

799
  vector<double> weight_change(mpi::n_procs, 0.0);
2,320✔
800
  vector<double> combined_rank_load(mpi::n_procs, 0.0);
2,320✔
801
  vector<double> load_ratio(mpi::n_procs, 0.0);
2,320✔
802

803
  // Combine estimated load with measured load ratios
804
  for (int rank = 0; rank < mpi::n_procs; rank++) {
6,960✔
805
    load_ratio[rank] = calculate_load_ratio(rank);
4,640✔
806
    combined_rank_load[rank] =
4,640✔
807
      load_ratio[rank] * estimated_rank_load_totals_[rank];
4,640✔
808
  }
809

810
  double combined_load_sum =
2,320✔
811
    std::accumulate(combined_rank_load.begin(), combined_rank_load.end(), 0.0);
2,320✔
812

813
  for (int rank = 0; rank < mpi::n_procs; rank++) {
6,960✔
814
    combined_rank_load[rank] = (combined_rank_load[rank] / combined_load_sum);
4,640✔
815
  }
816
  double max_load =
2,320!
817
    *std::max_element(combined_rank_load.begin(), combined_rank_load.end());
2,320!
818
  double max_imbalance = (max_load - target_load_) / target_load_;
2,320✔
819
  double prev_imbalance = max_imbalance;
2,320✔
820

821
  // History tracking
822
  vector<double> imbalance_history;
2,320✔
823
  vector<vector<double>> weight_history;
2,320✔
824
  imbalance_history.push_back(max_imbalance);
2,320✔
825
  weight_history.push_back(rank_weights_);
2,320✔
826

827
  // Change weights to equalize load based on combined load estimates
828
  while (max_imbalance > imbalance_tolerance_ && it_outer < max_iterations) {
80,136✔
829

830
    it_outer++;
77,816✔
831

832
    for (int rank = 0; rank < mpi::n_procs; rank++) {
233,448✔
833
      double corr = ((combined_rank_load[rank] - target_load_) / target_load_) *
155,632!
834
                    weight_scale;
155,632✔
835
      weight_change[rank] =
155,632✔
836
        beta * weight_change[rank] +
155,632✔
837
        (1.0 - beta) * corr; // keep some inertia from previous changes to
155,632✔
838
                             // prevent oscillations
839
      double damping = std::clamp(max_imbalance / prev_imbalance, 0.1,
311,264✔
840
        1.0); // dampening factor based on whether we are getting closer to
155,632!
841
              // convergence or not, prevents big jumps, if too big a change,
842
              // more dampening is applied
843
      rank_weights_[rank] -= adaptation_factor * damping * weight_change[rank];
155,632✔
844
    }
845

846
    if (simulation::current_batch > 1) {
77,816✔
847
      // Check distances to all ranks only in first batch, otherwise only
848
      // recorded neighbors
849
      check_all_ranks = false;
54,920✔
850
    }
851

852
    // Calculate new load after weight update
853
    update_load(domain, check_all_ranks, combined_rank_load, load_ratio);
77,816✔
854
    double max_load =
77,816!
855
      *std::max_element(combined_rank_load.begin(), combined_rank_load.end());
77,816!
856
    max_imbalance = (max_load - target_load_) / target_load_;
77,816✔
857

858
    // Store imbalance history
859
    imbalance_history.push_back(max_imbalance);
77,816✔
860
    weight_history.push_back(rank_weights_);
77,816✔
861

862
    // Adaptive factor
863
    if (max_imbalance > prev_imbalance)
77,816✔
864
      adaptation_factor =
38,604✔
865
        std::max(adaptation_factor * 0.5, min_adaptation_factor);
31,916✔
866
    else
867
      adaptation_factor =
117,028✔
868
        std::min(adaptation_factor * 1.05, max_adaptation_factor);
59,540✔
869

870
    prev_imbalance = max_imbalance;
871
  }
872

873
  // Check convergence and adjust history optimization factor depending on
874
  // failure mode to enable better convergence in the following batch
875
  if (it_outer == max_iterations) {
2,320✔
876
    if (mpi::master) {
314✔
877
      warning("MPI load balancing has not converged after " +
471✔
878
              std::to_string(max_iterations) + " iterations.");
628✔
879
    }
880

881
    // Check if oscillating or simply slow convergence
882
    int direction_changes = 0;
883

884
    for (int i = 1; i < imbalance_history.size(); i++) {
63,114✔
885
      // Calculate change
886
      double change = imbalance_history[i] - imbalance_history[i - 1];
62,800✔
887

888
      // Count direction changes
889
      if (i > 1) {
62,800✔
890
        double prev_change =
62,486✔
891
          imbalance_history[i - 1] - imbalance_history[i - 2];
62,486✔
892
        if (change * prev_change < 0) {
62,486✔
893
          direction_changes++;
14,834✔
894
        }
895
      }
896
    }
897

898
    // Check for oscillations
899
    double oscillation_ratio =
314✔
900
      (double)direction_changes / (imbalance_history.size() - 2);
314✔
901

902
    if (oscillation_ratio > 0.4) {
314✔
903
      // decrease weight for next batch if oscillating
904
      optimization_history_factor_ =
32✔
905
        std::max(optimization_history_factor_ * 0.5, min_adaptation_factor);
16!
906
    } else {
907
      // increase weight for faster convergence if too slow
908
      optimization_history_factor_ =
596✔
909
        std::min(optimization_history_factor_ * 1.2, max_adaptation_factor);
328✔
910
    }
911

912
    // Check which iteration had the best imbalance and revert to those weights
913
    auto min_it =
314!
914
      std::min_element(imbalance_history.begin(), imbalance_history.end());
314!
915
    int best_index = std::distance(imbalance_history.begin(), min_it);
314✔
916
    double best_imbalance = *min_it;
314✔
917
    rank_weights_ = weight_history[best_index];
314✔
918

919
    if (mpi::master) {
314✔
920
      printf(
157✔
921
        "Best imbalance during optimization was %.2f%% at iteration %d. \n",
922
        best_imbalance * 100.0, best_index);
923
    }
924

925
    if (best_index == 0) {
314✔
926
      // if no improvement at all, just keep current decomposition and return
927
      // without redistributing
928
      return;
46✔
929
    }
930
  } else {
931
    optimization_history_factor_ = 1.0; // reset history factor if converged
2,006✔
932
    if (mpi::master) {
2,006✔
933
      printf("MPI load balancing converged after %d iterations. Max. "
1,003✔
934
             "imbalance: %.2f%% \n",
935
        it_outer, max_imbalance * 100.0);
936
    }
937
  }
938

939
  // Redistribute source regions according to new weights determined in
940
  // optimization
941
  redistribute_source_regions(domain);
2,274✔
942
}
2,320✔
943

944
void DecompositionMap::update_load(FlatSourceDomain* domain,
77,816✔
945
  bool check_all_ranks, vector<double>& combined_rank_load,
946
  vector<double>& load_ratio)
947
{
948

949
  vector<double> load(mpi::n_procs, 0);
77,816✔
950

951
// Add up source region hits to respective rank depending of position of
952
// centroid of each source region
953
#pragma omp parallel
55,218✔
954
  {
22,598✔
955
    // number of hits per thread
956
    vector<double> thread_load(mpi::n_procs, 0);
22,598✔
957

958
#pragma omp for
959
    for (int64_t sr = 0; sr < domain->n_source_regions(); sr++) {
9,248,529✔
960
      Position centroid = domain->source_regions_.centroid(sr);
9,225,931✔
961
      int owner = find_closest_rank(centroid, check_all_ranks);
9,225,931✔
962
      double volume_sr = domain->source_regions_.volume_t(sr);
9,225,931✔
963
      thread_load[owner] +=
9,225,931✔
964
        load_ratio[owner] *
9,225,931✔
965
        (C1_ *
9,225,931✔
966
            (domain->source_regions_.n_hits(sr) / simulation::current_batch) *
9,225,931✔
967
            negroups_ +
9,225,931✔
968
          volume_sr * ray_tracing_cost_[domain->source_regions_.key(sr)
9,225,931✔
969
                          .base_source_region_id]);
9,225,931✔
970
    }
971

972
// Combine results from different threads
973
#pragma omp critical(combining_loads)
974
    {
975
      for (int i = 0; i < mpi::n_procs; i++) {
67,794✔
976
        load[i] += thread_load[i];
45,196✔
977
      }
978
    }
979
  }
980

981
  // Communicate new load estimates across ranks
982
  MPI_Allreduce(MPI_IN_PLACE, load.data(), mpi::n_procs, MPI_DOUBLE, MPI_SUM,
77,816✔
983
    mpi::intracomm);
984
  double load_sum = std::accumulate(load.begin(), load.end(), 0.0);
77,816✔
985

986
  // Update new combined rank load fractions
987
  for (int rank = 0; rank < mpi::n_procs; rank++) {
233,448✔
988
    combined_rank_load[rank] = load[rank] / load_sum;
155,632✔
989
  }
990
}
77,816✔
991

992
void DecompositionMap::redistribute_source_regions(FlatSourceDomain* domain)
2,274✔
993
{
994

995
  // Map of source regions to be sent to other ranks
996
  std::unordered_map<int, vector<int>> sr_send_list;
2,274✔
997
  // Number of source regions to be received from other ranks
998
  vector<int> num_sr_receiving(mpi::n_procs, 0);
2,274✔
999

1000
  // Local source region container that contains updated list
1001
  SourceRegionContainer source_regions_new(negroups_, is_linear_);
2,274✔
1002

1003
  // Each rank identifies source regions that need to be transferred to new
1004
  // owner and updates subdomain map accordingly
1005
  for (int64_t sr = 0; sr < domain->n_source_regions(); sr++) {
4,756,274✔
1006
    Position centroid = domain->source_regions_.centroid(sr);
4,754,000✔
1007
    int owner = find_closest_rank(centroid, true);
4,754,000✔
1008

1009
    // If owner changed, write source region to list of outbound source regions
1010
    if (owner != mpi::rank) {
4,754,000✔
1011
      sr_send_list[owner].push_back(sr);
101,554✔
1012
    }
1013
    // If owner did not change, add source region to new local container
1014
    else {
1015
      source_regions_new.push_back(
4,652,446✔
1016
        domain->source_regions_.get_source_region_handle(sr));
9,304,892✔
1017
    }
1018
  }
1019

1020
  // Each rank informs other ranks about ownership changes to update subdomain
1021
  // map
1022
  for (int rank = 0; rank < mpi::n_procs; rank++) {
6,822✔
1023

1024
    // Send size
1025
    int bcast_size = 0;
4,548✔
1026
    if (rank == mpi::rank) {
4,548✔
1027
      for (const auto& pair : sr_send_list) {
3,399✔
1028
        bcast_size += pair.second.size();
1,125✔
1029
      }
1030
    }
1031
    MPI_Bcast(&bcast_size, 1, MPI_INT, rank, mpi::intracomm);
4,548✔
1032

1033
    if (bcast_size > 0) {
4,548✔
1034
      vector<int> rank_ids(bcast_size);
2,250✔
1035
      vector<int64_t> base_ids(bcast_size);
2,250✔
1036
      vector<int64_t> mesh_bins(bcast_size);
2,250✔
1037

1038
      // Current owner prepares communication and fills vectors to be sent
1039
      if (rank == mpi::rank) {
2,250✔
1040
        int i = 0;
1,125✔
1041
        for (auto& pair : sr_send_list) {
2,250✔
1042

1043
          int receiver = pair.first; // new owner
1,125✔
1044
          vector<int>& sr_indices =
1,125✔
1045
            pair.second; // vector of source region indices
1046

1047
          // Iterate through all source regions for this key
1048
          for (int sr_idx : sr_indices) {
102,679✔
1049
            SourceRegionKey sr_key = domain->source_regions_.key(sr_idx);
101,554✔
1050
            rank_ids[i] = receiver;
101,554✔
1051
            base_ids[i] = sr_key.base_source_region_id;
101,554✔
1052
            mesh_bins[i] = sr_key.mesh_bin;
101,554✔
1053
            i++;
101,554✔
1054
          }
1055
        }
1056
      }
1057

1058
      // Broadcast source region data
1059
      MPI_Bcast(rank_ids.data(), bcast_size, MPI_INT, rank, mpi::intracomm);
2,250✔
1060
      MPI_Bcast(base_ids.data(), bcast_size, MPI_INT64_T, rank, mpi::intracomm);
2,250✔
1061
      MPI_Bcast(
2,250✔
1062
        mesh_bins.data(), bcast_size, MPI_INT64_T, rank, mpi::intracomm);
2,250✔
1063

1064
      // Every rank updates subdomain map
1065
      for (int j = 0; j < bcast_size; j++) {
205,358✔
1066

1067
        // Re-assemble source region key and extract new owner
1068
        SourceRegionKey sr_key(base_ids[j], mesh_bins[j]);
203,108✔
1069
        int rank_new = rank_ids[j];
203,108✔
1070

1071
        // Update subdomain_map with new owner
1072
        subdomain_map_[sr_key] = rank_new;
203,108✔
1073

1074
        // If calling rank is new owner, increase count of source regions coming
1075
        // from sending rank (current broadcaster)
1076
        if (mpi::rank == rank_new) {
203,108✔
1077
          num_sr_receiving[rank] += 1;
101,554✔
1078
        }
1079
      }
1080
    }
2,250✔
1081
  }
1082

1083
  // Clear source_region_map_
1084
  domain->source_region_map_.clear();
2,274✔
1085

1086
  // Send source region data to new owner
1087
  for (auto& pair : sr_send_list) {
3,399✔
1088
    int receiver = pair.first;             // destination rank
1,125✔
1089
    vector<int>& sr_indices = pair.second; // vector of source region indices
1,125✔
1090

1091
    // Iterate through all source regions for this key
1092
    for (int sr_idx : sr_indices) {
102,679✔
1093
      SourceRegionKey sr_key = domain->source_regions_.key(sr_idx);
101,554✔
1094
      SourceRegionHandle srh =
101,554✔
1095
        domain->source_regions_.get_source_region_handle(sr_idx);
101,554✔
1096
      SourceRegion sr(srh);
101,554✔
1097
      send_sr_data(receiver, sr);
101,554✔
1098
    }
101,554✔
1099
  }
1100

1101
  // Record starting source region ID for tally reinitialization later
1102
  int64_t start_sr_id = source_regions_new.n_source_regions();
2,274✔
1103

1104
  // Receive source regions
1105
  for (int sender = 0; sender < mpi::n_procs; sender++) {
6,822✔
1106

1107
    if (sender == mpi::rank) {
4,548✔
1108
      if (num_sr_receiving[sender] > 0) {
2,274!
NEW
1109
        fatal_error("Rank sends source regions to itself. Rank should not "
×
1110
                    "receive source regions from itself.");
1111
      }
1112
      continue; // skip self
2,274✔
1113
    }
1114

1115
    int num_sr = num_sr_receiving[sender];
2,274✔
1116
    for (int i = 0; i < num_sr; ++i) {
103,828✔
1117
      SourceRegion sr_recv(negroups_, is_linear_);
101,554✔
1118
      receive_sr_data(sender, sr_recv);
101,554✔
1119
      source_regions_new.push_back(sr_recv);
101,554✔
1120
    }
101,554✔
1121
  }
1122

1123
  // Update source regions in domain to new container
1124
  domain->source_regions_ = source_regions_new;
2,274✔
1125

1126
  // Update source region map
1127
  for (int64_t sr = 0; sr < domain->n_source_regions(); sr++) {
4,756,274✔
1128
    SourceRegionKey key = domain->source_regions_.key(sr);
4,754,000✔
1129
    domain->source_region_map_[key] = sr;
4,754,000✔
1130
  }
1131

1132
  // Reinitialise tallies
1133
  domain->convert_source_regions_to_tallies(start_sr_id);
2,274✔
1134
}
4,548✔
1135

1136
double DecompositionMap::calculate_load_ratio(int rank)
6,600✔
1137
{
1138
  if (estimated_rank_load_fractions_[rank] > 0.0) {
6,600!
1139
    return measured_rank_load_fractions_[rank] /
6,600✔
1140
           estimated_rank_load_fractions_[rank];
6,600✔
1141
  } else {
1142
    return 1.0;
1143
  }
1144
}
1145

1146
} // namespace openmc
1147
#endif // OPENMC_MPI
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