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

openmc-dev / openmc / 30024940656

23 Jul 2026 04:24PM UTC coverage: 81.335% (-0.08%) from 81.413%
30024940656

Pull #4026

github

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

19250 of 28006 branches covered (68.74%)

Branch coverage included in aggregate %.

1183 of 1206 new or added lines in 12 files covered. (98.09%)

4 existing lines in 1 file now uncovered.

61107 of 70791 relevant lines covered (86.32%)

49020891.15 hits per line

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

93.25
/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,231✔
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,494✔
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,091✔
406
          bcast_size++;
846,077✔
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,478✔
422
          if (pair.second.scalars_.volume_ > 0.0) {
883,091✔
423
            SourceRegionKey sr_key = pair.first;
846,077✔
424
            local_base_ids[i] = sr_key.base_source_region_id;
846,077✔
425
            local_mesh_bins[i] = sr_key.mesh_bin;
846,077✔
426
            i++;
846,077✔
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,450✔
439

440
        SourceRegionKey sr_key(local_base_ids[j], local_mesh_bins[j]);
1,692,154✔
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,154✔
445
          subdomain_map_[sr_key] = rank;
1,691,056✔
446
        } else {
447
          int resident_rank = subdomain_map_[sr_key]; // current owner
1,098✔
448
          int challenger_rank = rank; // current broadcasting rank
1,098✔
449

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

457
          // If load of challenger rank is lower, assign source region to that
458
          // rank, otherwise resident keeps it
459
          int sender;
1,098✔
460
          int receiver;
1,098✔
461
          if (challenger_rank_load < resident_rank_load) {
1,098✔
462
            subdomain_map_[sr_key] = challenger_rank;
976✔
463
            sender = resident_rank;
976✔
464
            receiver = challenger_rank;
976✔
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;
1,098✔
474
          if (mpi::rank == sender) {
1,098✔
475
            SourceRegion& contested_sr = discovered_source_regions[sr_key];
549✔
476
            double volume_sr = contested_sr.scalars_.volume_;
549✔
477
            bcast_load = C1_ * 
549✔
478
              (contested_sr.scalars_.n_hits_ / simulation::current_batch) * 
549✔
479
              negroups_ + volume_sr * 
549✔
480
              ray_tracing_cost_[sr_key.base_source_region_id];
549✔
481
          }
482
          MPI_Bcast(&bcast_load, 1, MPI_DOUBLE, sender, mpi::intracomm);
1,098✔
483

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

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

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

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

513
void DecompositionMap::send_sr_data(int receiver, SourceRegion& sr_send)
45,712✔
514
{
515

516
  int num_scalar_messages = 1;
45,712✔
517
  //! NOTE: update if new vector fields are added to SourceExchangeVectors
518
  //! struct
519
  int num_vector_messages = 4;
45,712✔
520
  if (is_linear_) {
45,712✔
521
    num_vector_messages += 4;
14,266✔
522
  }
523
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
45,712✔
524
    num_vector_messages += 1;
43,496✔
525
  }
526
  int num_requests = num_scalar_messages + num_vector_messages;
45,712✔
527

528
  vector<MPI_Request> requests(num_requests);
45,712✔
529
  int req_idx = 0;
45,712✔
530

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

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

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

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

550
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
45,712✔
551
    MPI_Isend(sr_send.external_source_.data(), negroups_, MPI_FLOAT, receiver,
43,496✔
552
      5, mpi::intracomm, &requests[req_idx]);
43,496✔
553
    req_idx++;
554
  }
555

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

560
  if (is_linear_) {
45,712✔
561
    MPI_Isend(sr_send.source_gradients_.data(), 3 * negroups_, MPI_DOUBLE,
14,266✔
562
      receiver, 7, mpi::intracomm, &requests[req_idx]);
14,266✔
563
    req_idx++;
14,266✔
564

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

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

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

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

585
  // Wait for all communication to complete
586
  MPI_Waitall(num_requests, requests.data(), MPI_STATUSES_IGNORE);
45,712✔
587
}
45,712✔
588

589
void DecompositionMap::receive_sr_data(int sender, SourceRegion& sr_recv)
45,712✔
590
{
591

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

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

604
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
45,712✔
605
    MPI_Recv(sr_recv.external_source_.data(), negroups_, MPI_FLOAT, sender, 5,
43,496✔
606
      mpi::intracomm, MPI_STATUS_IGNORE);
607
  }
608

609
  MPI_Recv(sr_recv.scalar_flux_final_.data(), negroups_, MPI_DOUBLE, sender, 6,
45,712✔
610
    mpi::intracomm, MPI_STATUS_IGNORE);
611

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

624
int DecompositionMap::find_owner(SourceRegionKey sr_key, Position r,
523,973,533✔
625
  ParallelMap<SourceRegionKey, SourceRegion, SourceRegionKey::HashFunctor>&
626
  discovered_source_regions)
627
{
628

629
  // Check if source region key is in subdomain map
630
  auto it = subdomain_map_.find(sr_key);
523,973,533✔
631
  if (it != subdomain_map_.end()) {
523,973,533✔
632
    return it->second;
505,372,918✔
633
  }
634

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

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

649
int DecompositionMap::find_closest_rank(Position r, bool test_all_ranks)
25,041,433✔
650
{
651

652
  int closest_rank = C_NONE;
25,041,433✔
653
  double min_distance = INFTY;
25,041,433✔
654
  vector<int> test_ranks;
25,041,433✔
655

656
  if (test_all_ranks) {
25,041,433✔
657
    test_ranks.resize(mpi::n_procs);
12,016,107✔
658
    std::iota(test_ranks.begin(), test_ranks.end(),
12,016,107✔
659
      0); // fill with 0, 1, ..., n_procs-1
660
  } else {
661
    // convert unordered set of neighboring ranks to vector and add self rank
662
    test_ranks = vector<int>(mpi::decomp_map.my_neighbors_.begin(),
13,025,326✔
663
      mpi::decomp_map.my_neighbors_.end());
13,025,326✔
664
    test_ranks.push_back(mpi::rank);
13,025,326✔
665
  }
666

667
  // Find closest rank center
668
  for (int rank : test_ranks) {
75,124,299✔
669
    double dist = (r - rank_centers_[rank]).norm();
50,082,866✔
670
    // Distance function corresponding to weighted power Voronoi diagram
671
    dist = dist * dist - rank_weights_[rank];
50,082,866✔
672
    if (dist < min_distance) {
50,082,866✔
673
      min_distance = dist;
43,764,109✔
674
      closest_rank = rank;
43,764,109✔
675
    }
676
  }
677

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

685
  return closest_rank;
25,041,433✔
686
}
25,041,433✔
687

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

829
    it_outer++;
42,908✔
830

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

845
    if (simulation::current_batch > 1) {
42,908✔
846
      // Check distances to all ranks only in first batch, otherwise only
847
      // recorded neighbors
848
      check_all_ranks = false;
30,730✔
849
    }
850

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

857
    // Store imbalance history
858
    imbalance_history.push_back(max_imbalance);
42,908✔
859
    weight_history.push_back(rank_weights_);
42,908✔
860

861
    // Adaptive factor
862
    if (max_imbalance > prev_imbalance)
42,908✔
863
      adaptation_factor =
18,372✔
864
        std::max(adaptation_factor * 0.5, min_adaptation_factor);
13,782✔
865
    else
866
      adaptation_factor =
67,444✔
867
        std::min(adaptation_factor * 1.05, max_adaptation_factor);
35,030✔
868

869
    prev_imbalance = max_imbalance;
870
  }
871

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

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

883
    for (int i = 1; i < imbalance_history.size(); i++) {
31,356✔
884
      // Calculate change
885
      double change = imbalance_history[i] - imbalance_history[i - 1];
31,200✔
886

887
      // Count direction changes
888
      if (i > 1) {
31,200✔
889
        double prev_change =
31,044✔
890
          imbalance_history[i - 1] - imbalance_history[i - 2];
31,044✔
891
        if (change * prev_change < 0) {
31,044✔
892
          direction_changes++;
5,926✔
893
        }
894
      }
895
    }
896

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

901
    if (oscillation_ratio > 0.4) {
156✔
902
      // decrease weight for next batch if oscillating
903
      optimization_history_factor_ =
4✔
904
        std::max(optimization_history_factor_ * 0.5, min_adaptation_factor);
2!
905
    } else {
906
      // increase weight for faster convergence if too slow
907
      optimization_history_factor_ =
308✔
908
        std::min(optimization_history_factor_ * 1.2, max_adaptation_factor);
166✔
909
    }
910

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

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

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

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

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

948
  vector<double> load(mpi::n_procs, 0);
42,908✔
949

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

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

969
// Combine results from different threads
970
#pragma omp critical(combining_loads)
971
    {
972
      for (int i = 0; i < mpi::n_procs; i++) {
27,450✔
973
        load[i] += thread_load[i];
18,300✔
974
      }
975
    }
976
  }
977

978
  // Communicate new load estimates across ranks
979
  MPI_Allreduce(MPI_IN_PLACE, load.data(), mpi::n_procs, MPI_DOUBLE, MPI_SUM,
42,908✔
980
    mpi::intracomm);
981
  double load_sum = std::accumulate(load.begin(), load.end(), 0.0);
42,908✔
982

983
  // Update new combined rank load fractions
984
  for (int rank = 0; rank < mpi::n_procs; rank++) {
128,724✔
985
    combined_rank_load[rank] = load[rank] / load_sum;
85,816✔
986
  }
987
}
42,908✔
988

989
void DecompositionMap::redistribute_source_regions(FlatSourceDomain* domain)
2,254✔
990
{
991

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

997
  // Local source region container that contains updated list
998
  SourceRegionContainer source_regions_new(negroups_, is_linear_);
2,254✔
999

1000
  // Each rank identifies source regions that need to be transferred to new
1001
  // owner and updates subdomain map accordingly
1002
  for (int64_t sr = 0; sr < domain->n_source_regions(); sr++) {
4,756,136✔
1003
    Position centroid = domain->source_regions_.centroid(sr);
4,753,882✔
1004
    int owner = find_closest_rank(centroid, true);
4,753,882✔
1005

1006
    // If owner changed, write source region to list of outbound source regions
1007
    if (owner != mpi::rank) {
4,753,882✔
1008
      sr_send_list[owner].push_back(sr);
45,163✔
1009
    }
1010
    // If owner did not change, add source region to new local container
1011
    else {
1012
      source_regions_new.push_back(
4,708,719✔
1013
        domain->source_regions_.get_source_region_handle(sr));
9,417,438✔
1014
    }
1015
  }
1016

1017
  // Each rank informs other ranks about ownership changes to update subdomain
1018
  // map
1019
  for (int rank = 0; rank < mpi::n_procs; rank++) {
6,762✔
1020

1021
    // Send size
1022
    int bcast_size = 0;
4,508✔
1023
    if (rank == mpi::rank) {
4,508✔
1024
      for (const auto& pair : sr_send_list) {
3,281✔
1025
        bcast_size += pair.second.size();
1,027✔
1026
      }
1027
    }
1028
    MPI_Bcast(&bcast_size, 1, MPI_INT, rank, mpi::intracomm);
4,508✔
1029

1030
    if (bcast_size > 0) {
4,508✔
1031
      vector<int> rank_ids(bcast_size);
2,054✔
1032
      vector<int64_t> base_ids(bcast_size);
2,054✔
1033
      vector<int64_t> mesh_bins(bcast_size);
2,054✔
1034

1035
      // Current owner prepares communication and fills vectors to be sent
1036
      if (rank == mpi::rank) {
2,054✔
1037
        int i = 0;
1,027✔
1038
        for (auto& pair : sr_send_list) {
2,054✔
1039

1040
          int receiver = pair.first; // new owner
1,027✔
1041
          vector<int>& sr_indices =
1,027✔
1042
            pair.second; // vector of source region indices
1043

1044
          // Iterate through all source regions for this key
1045
          for (int sr_idx : sr_indices) {
46,190✔
1046
            SourceRegionKey sr_key = domain->source_regions_.key(sr_idx);
45,163✔
1047
            rank_ids[i] = receiver;
45,163✔
1048
            base_ids[i] = sr_key.base_source_region_id;
45,163✔
1049
            mesh_bins[i] = sr_key.mesh_bin;
45,163✔
1050
            i++;
45,163✔
1051
          }
1052
        }
1053
      }
1054

1055
      // Broadcast source region data
1056
      MPI_Bcast(rank_ids.data(), bcast_size, MPI_INT, rank, mpi::intracomm);
2,054✔
1057
      MPI_Bcast(base_ids.data(), bcast_size, MPI_INT64_T, rank, mpi::intracomm);
2,054✔
1058
      MPI_Bcast(
2,054✔
1059
        mesh_bins.data(), bcast_size, MPI_INT64_T, rank, mpi::intracomm);
2,054✔
1060

1061
      // Every rank updates subdomain map
1062
      for (int j = 0; j < bcast_size; j++) {
92,380✔
1063

1064
        // Re-assemble source region key and extract new owner
1065
        SourceRegionKey sr_key(base_ids[j], mesh_bins[j]);
90,326✔
1066
        int rank_new = rank_ids[j];
90,326✔
1067

1068
        // Update subdomain_map with new owner
1069
        subdomain_map_[sr_key] = rank_new;
90,326✔
1070

1071
        // If calling rank is new owner, increase count of source regions coming
1072
        // from sending rank (current broadcaster)
1073
        if (mpi::rank == rank_new) {
90,326✔
1074
          num_sr_receiving[rank] += 1;
45,163✔
1075
        }
1076
      }
1077
    }
2,054✔
1078
  }
1079

1080
  // Clear source_region_map_
1081
  domain->source_region_map_.clear();
2,254✔
1082

1083
  // Send source region data to new owner
1084
  for (auto& pair : sr_send_list) {
3,281✔
1085
    int receiver = pair.first;             // destination rank
1,027✔
1086
    vector<int>& sr_indices = pair.second; // vector of source region indices
1,027✔
1087

1088
    // Iterate through all source regions for this key
1089
    for (int sr_idx : sr_indices) {
46,190✔
1090
      SourceRegionKey sr_key = domain->source_regions_.key(sr_idx);
45,163✔
1091
      SourceRegionHandle srh =
45,163✔
1092
        domain->source_regions_.get_source_region_handle(sr_idx);
45,163✔
1093
      SourceRegion sr(srh);
45,163✔
1094
      send_sr_data(receiver, sr);
45,163✔
1095
    }
45,163✔
1096
  }
1097

1098
  // Record starting source region ID for tally reinitialization later
1099
  int64_t start_sr_id = source_regions_new.n_source_regions();
2,254✔
1100

1101
  // Receive source regions
1102
  for (int sender = 0; sender < mpi::n_procs; sender++) {
6,762✔
1103

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

1112
    int num_sr = num_sr_receiving[sender];
2,254✔
1113
    for (int i = 0; i < num_sr; ++i) {
47,417✔
1114
      SourceRegion sr_recv(negroups_, is_linear_);
45,163✔
1115
      receive_sr_data(sender, sr_recv);
45,163✔
1116
      source_regions_new.push_back(sr_recv);
45,163✔
1117
    }
45,163✔
1118
  }
1119

1120
  // Update source regions in domain to new container
1121
  domain->source_regions_ = source_regions_new;
2,254✔
1122

1123
  // Update source region map
1124
  for (int64_t sr = 0; sr < domain->n_source_regions(); sr++) {
4,756,136✔
1125
    SourceRegionKey key = domain->source_regions_.key(sr);
4,753,882✔
1126
    domain->source_region_map_[key] = sr;
4,753,882✔
1127
  }
1128

1129
  // Reinitialise tallies
1130
  domain->convert_source_regions_to_tallies(start_sr_id);
2,254✔
1131
}
4,508✔
1132

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

1143
} // namespace openmc
1144
#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