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

openmc-dev / openmc / 30082591782

24 Jul 2026 09:26AM UTC coverage: 81.336% (-0.08%) from 81.413%
30082591782

Pull #4026

github

web-flow
Merge d3b3c7684 into 01790598d
Pull Request #4026: Add domain decomposition for random ray solver

19231 of 27986 branches covered (68.72%)

Branch coverage included in aggregate %.

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

415 existing lines in 10 files now uncovered.

61115 of 70797 relevant lines covered (86.32%)

49017206.19 hits per line

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

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

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

15
#include "openmc/distribution_spatial.h"
16
#include "openmc/random_dist.h"
17
#include "openmc/random_ray/decomposition_map.h"
18
#include "openmc/source.h"
19

20
namespace openmc {
21

22
//==============================================================================
23
// Non-method functions
24
//==============================================================================
25

26
// returns 1 - exp(-tau)
27
// Equivalent to -(_expm1f(-tau)), but faster
28
// Written by Colin Josey.
29
float cjosey_exponential(float tau)
2,147,483,647✔
30
{
31
  constexpr float c1n = -1.0000013559236386308f;
2,147,483,647✔
32
  constexpr float c2n = 0.23151368626911062025f;
2,147,483,647✔
33
  constexpr float c3n = -0.061481916409314966140f;
2,147,483,647✔
34
  constexpr float c4n = 0.0098619906458127653020f;
2,147,483,647✔
35
  constexpr float c5n = -0.0012629460503540849940f;
2,147,483,647✔
36
  constexpr float c6n = 0.00010360973791574984608f;
2,147,483,647✔
37
  constexpr float c7n = -0.000013276571933735820960f;
2,147,483,647✔
38

39
  constexpr float c0d = 1.0f;
2,147,483,647✔
40
  constexpr float c1d = -0.73151337729389001396f;
2,147,483,647✔
41
  constexpr float c2d = 0.26058381273536471371f;
2,147,483,647✔
42
  constexpr float c3d = -0.059892419041316836940f;
2,147,483,647✔
43
  constexpr float c4d = 0.0099070188241094279067f;
2,147,483,647✔
44
  constexpr float c5d = -0.0012623388962473160860f;
2,147,483,647✔
45
  constexpr float c6d = 0.00010361277635498731388f;
2,147,483,647✔
46
  constexpr float c7d = -0.000013276569500666698498f;
2,147,483,647✔
47

48
  float x = -tau;
2,147,483,647✔
49

50
  float den = c7d;
2,147,483,647✔
51
  den = den * x + c6d;
2,147,483,647✔
52
  den = den * x + c5d;
2,147,483,647✔
53
  den = den * x + c4d;
2,147,483,647✔
54
  den = den * x + c3d;
2,147,483,647✔
55
  den = den * x + c2d;
2,147,483,647✔
56
  den = den * x + c1d;
2,147,483,647✔
57
  den = den * x + c0d;
2,147,483,647✔
58

59
  float num = c7n;
2,147,483,647✔
60
  num = num * x + c6n;
2,147,483,647✔
61
  num = num * x + c5n;
2,147,483,647✔
62
  num = num * x + c4n;
2,147,483,647✔
63
  num = num * x + c3n;
2,147,483,647✔
64
  num = num * x + c2n;
2,147,483,647✔
65
  num = num * x + c1n;
2,147,483,647✔
66
  num = num * x;
2,147,483,647✔
67

68
  return num / den;
2,147,483,647✔
69
}
70

71
// The below two functions (exponentialG and exponentialG2) were developed
72
// by Colin Josey. The implementation of these functions is closely based
73
// on the OpenMOC versions of these functions. The OpenMOC license is given
74
// below:
75

76
// Copyright (C) 2012-2023 Massachusetts Institute of Technology and OpenMOC
77
// contributors
78
//
79
// Permission is hereby granted, free of charge, to any person obtaining a copy
80
// of this software and associated documentation files (the "Software"), to deal
81
// in the Software without restriction, including without limitation the rights
82
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
83
// copies of the Software, and to permit persons to whom the Software is
84
// furnished to do so, subject to the following conditions:
85
//
86
// The above copyright notice and this permission notice shall be included in
87
// all copies or substantial portions of the Software.
88
//
89
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
90
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
91
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
92
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
93
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
94
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
95
// SOFTWARE.
96

97
// Computes y = 1/x-(1-exp(-x))/x**2 using a 5/6th order rational
98
// approximation. It is accurate to 2e-7 over [0, 1e5]. Developed by Colin
99
// Josey using Remez's algorithm, with original implementation in OpenMOC at:
100
// https://github.com/mit-crpg/OpenMOC/blob/develop/src/exponentials.h
101
float exponentialG(float tau)
2,147,483,647✔
102
{
103
  // Numerator coefficients in rational approximation for 1/x - (1 - exp(-x)) /
104
  // x^2
105
  constexpr float d0n = 0.5f;
2,147,483,647✔
106
  constexpr float d1n = 0.176558112351595f;
2,147,483,647✔
107
  constexpr float d2n = 0.04041584305811143f;
2,147,483,647✔
108
  constexpr float d3n = 0.006178333902037397f;
2,147,483,647✔
109
  constexpr float d4n = 0.0006429894635552992f;
2,147,483,647✔
110
  constexpr float d5n = 0.00006064409107557148f;
2,147,483,647✔
111

112
  // Denominator coefficients in rational approximation for 1/x - (1 - exp(-x))
113
  // / x^2
114
  constexpr float d0d = 1.0f;
2,147,483,647✔
115
  constexpr float d1d = 0.6864462055546078f;
2,147,483,647✔
116
  constexpr float d2d = 0.2263358514260129f;
2,147,483,647✔
117
  constexpr float d3d = 0.04721469893686252f;
2,147,483,647✔
118
  constexpr float d4d = 0.006883236664917246f;
2,147,483,647✔
119
  constexpr float d5d = 0.0007036272419147752f;
2,147,483,647✔
120
  constexpr float d6d = 0.00006064409107557148f;
2,147,483,647✔
121

122
  float x = tau;
2,147,483,647✔
123

124
  float num = d5n;
2,147,483,647✔
125
  num = num * x + d4n;
2,147,483,647✔
126
  num = num * x + d3n;
2,147,483,647✔
127
  num = num * x + d2n;
2,147,483,647✔
128
  num = num * x + d1n;
2,147,483,647✔
129
  num = num * x + d0n;
2,147,483,647✔
130

131
  float den = d6d;
2,147,483,647✔
132
  den = den * x + d5d;
2,147,483,647✔
133
  den = den * x + d4d;
2,147,483,647✔
134
  den = den * x + d3d;
2,147,483,647✔
135
  den = den * x + d2d;
2,147,483,647✔
136
  den = den * x + d1d;
2,147,483,647✔
137
  den = den * x + d0d;
2,147,483,647✔
138

139
  return num / den;
2,147,483,647✔
140
}
141

142
// Computes G2 : y = 2/3 - (1 + 2/x) * (1/x + 0.5 - (1 + 1/x) * (1-exp(-x)) /
143
// x) using a 5/5th order rational approximation. It is accurate to 1e-6 over
144
// [0, 1e6]. Developed by Colin Josey using Remez's algorithm, with original
145
// implementation in OpenMOC at:
146
// https://github.com/mit-crpg/OpenMOC/blob/develop/src/exponentials.h
147
float exponentialG2(float tau)
2,147,483,647✔
148
{
149

150
  // Coefficients for numerator in rational approximation
151
  constexpr float g1n = -0.08335775885589858f;
2,147,483,647✔
152
  constexpr float g2n = -0.003603942303847604f;
2,147,483,647✔
153
  constexpr float g3n = 0.0037673183263550827f;
2,147,483,647✔
154
  constexpr float g4n = 0.00001124183494990467f;
2,147,483,647✔
155
  constexpr float g5n = 0.00016837426505799449f;
2,147,483,647✔
156

157
  // Coefficients for denominator in rational approximation
158
  constexpr float g1d = 0.7454048371823628f;
2,147,483,647✔
159
  constexpr float g2d = 0.23794300531408347f;
2,147,483,647✔
160
  constexpr float g3d = 0.05367250964303789f;
2,147,483,647✔
161
  constexpr float g4d = 0.006125197988351906f;
2,147,483,647✔
162
  constexpr float g5d = 0.0010102514456857377f;
2,147,483,647✔
163

164
  float x = tau;
2,147,483,647✔
165

166
  float num = g5n;
2,147,483,647✔
167
  num = num * x + g4n;
2,147,483,647✔
168
  num = num * x + g3n;
2,147,483,647✔
169
  num = num * x + g2n;
2,147,483,647✔
170
  num = num * x + g1n;
2,147,483,647✔
171
  num = num * x;
2,147,483,647✔
172

173
  float den = g5d;
2,147,483,647✔
174
  den = den * x + g4d;
2,147,483,647✔
175
  den = den * x + g3d;
2,147,483,647✔
176
  den = den * x + g2d;
2,147,483,647✔
177
  den = den * x + g1d;
2,147,483,647✔
178
  den = den * x + 1.0f;
2,147,483,647✔
179

180
  return num / den;
2,147,483,647✔
181
}
182

183
// Implementation of the Fisher-Yates shuffle algorithm.
184
// Algorithm adapted from:
185
//    https://en.cppreference.com/w/cpp/algorithm/random_shuffle#Version_3
186
void fisher_yates_shuffle(vector<int64_t>& arr, uint64_t* seed)
128,304,000✔
187
{
188
  // Loop over the array from the last element down to the second
189
  for (int i = arr.size() - 1; i > 0; --i) {
553,311,000✔
190
    // Generate a random index in the range [0, i]
191
    int j = uniform_int_distribution(0, i, seed);
425,007,000✔
192
    std::swap(arr[i], arr[j]);
425,007,000✔
193
  }
194
}
128,304,000✔
195

196
// Function to generate randomized Halton sequence samples
197
//
198
// Algorithm adapted from:
199
//      A. B. Owen. A randomized halton algorithm in r. Arxiv, 6 2017.
200
//      URL https://arxiv.org/abs/1706.02808
201
vector<double> rhalton(int dim, uint64_t* seed, int64_t skip = 0)
891,000✔
202
{
203
  if (dim > 10) {
891,000!
204
    fatal_error("Halton sampling dimension too large");
×
205
  }
206
  int64_t b, res, dig;
891,000✔
207
  double b2r, ans;
891,000✔
208
  const std::array<int64_t, 10> primes = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
891,000✔
209
  vector<double> halton(dim, 0.0);
891,000✔
210

211
  vector<int64_t> perm;
891,000✔
212
  for (int D = 0; D < dim; ++D) {
5,346,000✔
213
    b = primes[D];
4,455,000✔
214
    perm.resize(b);
4,455,000✔
215
    b2r = 1.0 / b;
4,455,000✔
216
    res = skip;
4,455,000✔
217
    ans = 0.0;
4,455,000✔
218

219
    while ((1.0 - b2r) < 1.0) {
132,759,000✔
220
      std::iota(perm.begin(), perm.end(), 0);
128,304,000✔
221
      fisher_yates_shuffle(perm, seed);
128,304,000✔
222
      dig = res % b;
128,304,000✔
223
      ans += perm[dig] * b2r;
128,304,000✔
224
      res = (res - dig) / b;
128,304,000✔
225
      b2r /= b;
128,304,000✔
226
    }
227

228
    halton[D] = ans;
4,455,000✔
229
  }
230

231
  return halton;
891,000✔
232
}
891,000✔
233

234
//==============================================================================
235
// RandomRay implementation
236
//==============================================================================
237

238
// Static Variable Declarations
239
double RandomRay::distance_inactive_;
240
double RandomRay::distance_active_;
241
unique_ptr<Source> RandomRay::ray_source_;
242
RandomRaySourceShape RandomRay::source_shape_ {RandomRaySourceShape::FLAT};
243
RandomRayGeomDim RandomRay::geom_dim_ {RandomRayGeomDim::THREE_DIM};
244
RandomRaySampleMethod RandomRay::sample_method_ {RandomRaySampleMethod::PRNG};
245

246
RandomRay::RandomRay()
18,247,085✔
247
  : angular_flux_(data::mg.num_energy_groups_),
36,494,170✔
248
    delta_psi_(data::mg.num_energy_groups_),
22,128,990✔
249
    negroups_(data::mg.num_energy_groups_)
18,247,085✔
250
{
251
  if (source_shape_ == RandomRaySourceShape::LINEAR ||
18,247,085✔
252
      source_shape_ == RandomRaySourceShape::LINEAR_XY) {
253
    delta_moments_.resize(negroups_);
9,905,024✔
254
  }
255
}
18,247,085✔
256

257
RandomRay::RandomRay(uint64_t ray_id, FlatSourceDomain* domain) : RandomRay()
3,197,320✔
258
{
259
  initialize_ray(ray_id, domain);
3,197,320✔
260
}
3,197,320✔
261

262
// Transports ray until termination criteria are met
263
uint64_t RandomRay::transport_history_based_single_ray()
17,846,762✔
264
{
265
  using namespace openmc;
17,846,762✔
266
  int n_start = n_event();
17,846,762✔
267

268
  while (alive()) {
1,054,695,372!
269
    event_advance_ray();
1,036,848,610✔
270

271
    if (!alive())
1,036,848,610✔
272
      break;
273
    event_cross_surface();
1,019,001,848✔
274

275
    if (n_event() >= settings::max_particle_events) {
1,019,001,848!
276
      warning("Ray " + std::to_string(id()) +
×
277
              " underwent maximum number of events, terminating ray.");
278
      wgt() = 0.0;
×
279
    }
280
  }
281

282
  int delta_n = n_event() - n_start;
17,846,762✔
283
  return delta_n;
17,846,762✔
284
}
285

286
// Transports ray across a single source region
287
void RandomRay::event_advance_ray()
1,036,848,610✔
288
{
289
  // If geometry debug mode is on, check for cell overlaps
290
  if (settings::check_overlaps)
1,036,848,610!
291
    check_cell_overlap(*this);
×
292

293
  // Find the distance to the nearest boundary
294
  boundary() = distance_to_boundary(*this);
1,036,848,610✔
295
  double distance = boundary().distance();
1,036,848,610✔
296

297
#ifdef OPENMC_MPI
298
  if (mpi::n_procs > 1) {
386,374,380✔
299
    // If domain decomposition is being used, update counter for
300
    // ray trace operations in source region for load estimation
301
    int64_t sr = domain_->lookup_base_source_region_idx(*this);
344,905,530✔
302
    for (int i = 0; i < n_coord(); i++) {
1,309,198,284✔
303
      Cell& c {*model::cells[coord(i).cell()]};
964,292,754✔
304
      mpi::decomp_map.num_base_source_region_RT_[sr] += c.n_surfaces();
964,292,754✔
305
    }
306
  }
307
#endif
308

309
  if (distance < 0.0) {
1,036,848,610!
310
    mark_as_lost("Negative transport distance detected for particle " +
×
311
                 std::to_string(id()));
×
312
    return;
×
313
  }
314

315
  if (is_active_) {
1,036,848,610✔
316
    // If the ray is in the active length, need to check if it has
317
    // reached its maximum termination distance. If so, reduce
318
    // the ray traced length so that the ray does not overrun the
319
    // maximum numerical length (so as to avoid numerical bias).
320
    if (distance_travelled_ + distance >= distance_active_) {
847,429,743✔
321
      distance = distance_active_ - distance_travelled_;
3,236,917✔
322
      wgt() = 0.0;
3,236,917✔
323
    }
324

325
    attenuate_flux(distance, true);
847,429,743✔
326
    distance_travelled_ += distance;
847,429,743✔
327
  } else {
328
    // If the ray is still in the dead zone, need to check if it
329
    // has entered the active phase. If so, split into two segments (one
330
    // representing the final part of the dead zone, the other representing the
331
    // first part of the active length) and attenuate each. Otherwise, if the
332
    // full length of the segment is within the dead zone, attenuate as normal.
333
    if (distance_travelled_ + distance >= distance_inactive_) {
189,418,867✔
334
      double distance_dead = distance_inactive_ - distance_travelled_;
3,236,668✔
335
      attenuate_flux(distance_dead, false);
3,236,668✔
336
      is_active_ = true;
3,236,668✔
337
      distance_travelled_ = 0.0;
3,236,668✔
338

339
      if (has_left_subdomain()) {
3,236,668✔
340
        return;
341
      }
342

343
      double distance_alive = distance - distance_dead;
3,197,320✔
344

345
      // Ensure we haven't travelled past the active phase as well
346
      if (distance_alive > distance_active_) {
3,197,320!
347
        distance_alive = distance_active_;
×
348
        wgt() = 0.0;
×
349
      }
350

351
      attenuate_flux(distance_alive, true, distance_dead);
3,197,320✔
352
      distance_travelled_ = distance_alive;
3,197,320✔
353
    } else {
354
      attenuate_flux(distance, false);
186,182,199✔
355
      distance_travelled_ += distance;
186,182,199✔
356
    }
357
  }
358

359
  // Advance particle
360
  for (int j = 0; j < n_coord(); ++j) {
2,147,483,647✔
361
    coord(j).r() += distance * coord(j).u();
2,147,483,647✔
362
  }
363
}
364

365
void RandomRay::attenuate_flux(double distance, bool is_active, double offset)
1,040,045,930✔
366
{
367
  // Lookup base source region index
368
  int64_t sr = domain_->lookup_base_source_region_idx(*this);
1,040,045,930✔
369

370
  // Initialize values needed to buffer ray for domain decomposition
371
  double mesh_partial_length = 0.0;
1,040,045,930✔
372
  double tiny_multiplier = 0.0;
1,040,045,930✔
373

374
  // Perform ray tracing across mesh
375
  // Determine the mesh index for the base source region, if any
376
  int mesh_idx = domain_->lookup_mesh_idx(sr);
1,040,045,930✔
377

378
  if (mesh_idx == C_NONE) {
1,040,045,930✔
379
    // If there's no mesh being applied to this cell, then
380
    // we just attenuate the flux as normal, and set
381
    // the mesh bin to 0
382
    attenuate_flux_inner(distance, is_active, sr, 0, r());
468,434,645✔
383
  } else {
384
    // If there is a mesh being applied to this cell, then
385
    // we loop over all the bin crossings and attenuate
386
    // separately.
387
    Mesh* mesh = model::meshes[mesh_idx].get();
571,611,285✔
388

389
    // We adjust the start and end positions of the ray slightly
390
    // to accomodate for floating point precision issues that tend
391
    // to occur at mesh boundaries that overlap with geometry lattice
392
    // boundaries.
393
    Position start = r() + (offset + TINY_BIT) * u();
571,611,285✔
394
    Position end = start + (distance - 2.0 * TINY_BIT) * u();
571,611,285✔
395
    double reduced_distance = (end - start).norm();
571,611,285✔
396

397
    // Ray trace through the mesh and record bins and lengths
398
    mesh_bins_.resize(0);
571,611,285✔
399
    mesh_fractional_lengths_.resize(0);
571,611,285✔
400
    mesh->bins_crossed(start, end, u(), mesh_bins_, mesh_fractional_lengths_);
571,611,285✔
401

402
    // Loop over all mesh bins and attenuate flux
403
    for (int b = 0; b < mesh_bins_.size(); b++) {
1,682,522,120✔
404
      double physical_length = reduced_distance * mesh_fractional_lengths_[b];
1,121,325,841✔
405

406
#ifdef OPENMC_MPI
407
      if (mpi::n_procs > 1) {
414,429,896✔
408
        mpi::decomp_map.num_mesh_bin_RT_[sr] += 1;
384,558,714✔
409
      }
410
#endif
411

412
      // Very flat angles can result in very small physical lengths,
413
      // despite the TINY_BIT adjustment for Position start. If this happens at
414
      // an MPI boundary, this can cause rays to bounce back and forth
415
      // indefinitely. Very small lengths are therefore skipped.
416
      if (physical_length <= TINY_BIT) {
1,121,325,841✔
417
        start += physical_length * u();
11✔
418
        continue;
11✔
419
      }
420

421
      attenuate_flux_inner(
1,121,325,830✔
422
        physical_length, is_active, sr, mesh_bins_[b], start);
1,121,325,830✔
423

424
      start += physical_length * u();
1,121,325,830✔
425

426
      // If ray has left MPI subdomain, stop transport
427
      // and calculate position
428
      if (has_left_subdomain()) {
1,121,325,830✔
429
        for (int i = 0; i <= b - 1; i++) {
17,543,696✔
430
          mesh_partial_length += mesh_fractional_lengths_[i];
7,128,690✔
431
        }
432

433
        if (b > 0) {
10,415,006✔
434
          // If ray is stopped within mesh of base source region,
435
          // need to add TINY_BIT to account for deleted length
436
          tiny_multiplier = 1.0;
5,058,907✔
437
          // Reset last surface crossed to none if ray is stopped
438
          // within mesh of base source region
439
          surface() = 0;
5,058,907✔
440
        }
441

442
        mesh_partial_length =
10,415,006✔
443
          tiny_multiplier * TINY_BIT + reduced_distance * mesh_partial_length;
10,415,006✔
444
        break;
10,415,006✔
445
      }
446
    }
447
  }
448

449
  // If ray has left my subdomain, buffer ray state
450
  if (has_left_subdomain()) {
1,040,045,930✔
451
    Position position_buffer = r() + (offset + mesh_partial_length) * u();
14,649,442✔
452
    double distance_buffer = distance_travelled_ + mesh_partial_length;
14,649,442✔
453

454
#ifdef OPENMC_DAGMC_ENABLED
455
    history().rollback_last_intersection();
3,490,528✔
456
#endif
457
    pack_ray_for_buffer(distance_buffer, position_buffer);
14,649,442✔
458
    wgt() = 0.0;
14,649,442✔
459
  }
460
}
1,040,045,930✔
461

462
void RandomRay::attenuate_flux_inner(
1,589,760,475✔
463
  double distance, bool is_active, int64_t sr, int mesh_bin, Position r)
464
{
465
  SourceRegionKey sr_key {sr, mesh_bin};
1,589,760,475✔
466

467
#ifdef OPENMC_MPI
468
  if (mpi::n_procs > 1) {
587,464,404✔
469
    // Check which rank owns the source region at the current position
470
    Position midpoint = r + u() * (distance / 2.0);
523,371,542✔
471
    int owner = mpi::decomp_map.find_owner(SourceRegionKey(sr, mesh_bin),
1,046,743,084✔
472
      midpoint, domain_->discovered_source_regions_);
523,371,542✔
473

474
    // If current rank is not the owner return and mark as not local.
475
    if (owner != mpi::rank) {
523,371,542✔
476
      is_local_ = false;
14,649,442✔
477
      owner_rank_ = owner;
14,649,442✔
478
      return;
14,649,442✔
479
    }
480
  }
481
#endif
482

483
  SourceRegionHandle srh;
1,575,111,033✔
484
  srh = domain_->get_subdivided_source_region_handle(sr_key, r, u());
1,575,111,033✔
485
  if (srh.is_numerical_fp_artifact_) {
1,575,111,033✔
486
    return;
77✔
487
  }
488

489
  switch (source_shape_) {
1,575,110,912!
490
  case RandomRaySourceShape::FLAT:
899,558,859✔
491
    if (srh.material() == MATERIAL_VOID) {
899,558,859✔
492
      attenuate_flux_flat_source_void(srh, distance, is_active, r);
48,548,645✔
493
    } else {
494
      attenuate_flux_flat_source(srh, distance, is_active, r);
851,010,214✔
495
    }
496
    break;
497
  case RandomRaySourceShape::LINEAR:
675,552,053✔
498
  case RandomRaySourceShape::LINEAR_XY:
675,552,053✔
499
    if (srh.material() == MATERIAL_VOID) {
675,552,053✔
500
      attenuate_flux_linear_source_void(srh, distance, is_active, r);
8,287,235✔
501
    } else {
502
      attenuate_flux_linear_source(srh, distance, is_active, r);
667,264,818✔
503
    }
504
    break;
505
  default:
×
506
    fatal_error("Unknown source shape for random ray transport.");
×
507
  }
508
}
509

510
// This function forms the inner loop of the random ray transport process.
511
// It is responsible for several tasks. Based on the incoming angular flux
512
// of the ray and the source term in the region, the outgoing angular flux
513
// is computed. The delta psi between the incoming and outgoing fluxes is
514
// contributed to the estimate of the total scalar flux in the source region.
515
// Additionally, the contribution of the ray path to the stochastically
516
// estimated volume is also kept track of. All tasks involving writing
517
// to the data for the source region are done with a lock over the entire
518
// source region.  Locks are used instead of atomics as all energy groups
519
// must be written, such that locking once is typically much more efficient
520
// than use of many atomic operations corresponding to each energy group
521
// individually (at least on CPU). Several other bookkeeping tasks are also
522
// performed when inside the lock.
523
void RandomRay::attenuate_flux_flat_source(
851,010,214✔
524
  SourceRegionHandle& srh, double distance, bool is_active, Position r)
525
{
526
  // The number of geometric intersections is counted for reporting purposes
527
  n_event()++;
851,010,214✔
528

529
  // Get material
530
  int material = srh.material();
851,010,214✔
531
  int temp = srh.temperature_idx();
851,010,214✔
532

533
  // MOC incoming flux attenuation + source contribution/attenuation equation
534
  for (int g = 0; g < negroups_; g++) {
2,147,483,647✔
535
    float sigma_t =
2,147,483,647✔
536
      domain_->sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] *
2,147,483,647✔
537
      srh.density_mult();
2,147,483,647✔
538
    float tau = sigma_t * distance;
2,147,483,647✔
539
    float exponential = cjosey_exponential(tau); // exponential = 1 - exp(-tau)
2,147,483,647✔
540
    float new_delta_psi = (angular_flux_[g] - srh.source(g)) * exponential;
2,147,483,647✔
541
    delta_psi_[g] = new_delta_psi;
2,147,483,647✔
542
    angular_flux_[g] -= new_delta_psi;
2,147,483,647✔
543
  }
544

545
  // If ray is in the active phase (not in dead zone), make contributions to
546
  // source region bookkeeping
547
  Position midpoint = r + u() * (distance / 2.0);
851,010,214✔
548

549
  // Aquire lock for source region
550
  srh.lock();
851,010,214✔
551

552
  if (is_active) {
851,010,214✔
553
    // Accumulate delta psi into new estimate of source region flux for
554
    // this iteration
555
    for (int g = 0; g < negroups_; g++) {
2,147,483,647✔
556
      srh.scalar_flux_new(g) += delta_psi_[g];
1,772,365,563✔
557
    }
558

559
    // Accomulate volume (ray distance) into this iteration's estimate
560
    // of the source region's volume
561
    srh.volume() += distance;
703,141,797✔
562
    srh.centroid_iteration() += midpoint * distance;
703,141,797✔
563

564
    srh.n_hits() += 1;
703,141,797✔
565
  }
566

567
  // Tally valid position inside the source region (e.g., midpoint of
568
  // the ray) if not done already
569
  if (!srh.position_recorded()) {
851,010,214✔
570
    srh.position() = midpoint;
1,631,851✔
571
    srh.position_recorded() = 1;
1,631,851✔
572
  }
573

574
  // Release lock
575
  srh.unlock();
851,010,214✔
576
}
851,010,214✔
577

578
// Alternative flux attenuation function for true void regions.
579
void RandomRay::attenuate_flux_flat_source_void(
48,548,645✔
580
  SourceRegionHandle& srh, double distance, bool is_active, Position r)
581
{
582
  // The number of geometric intersections is counted for reporting purposes
583
  n_event()++;
48,548,645✔
584

585
  int material = srh.material();
48,548,645✔
586

587
  // If ray is in the active phase (not in dead zone), make contributions to
588
  // source region bookkeeping
589
  if (is_active) {
48,548,645✔
590

591
    Position midpoint = r + u() * (distance / 2.0);
38,837,421✔
592

593
    // Aquire lock for source region
594
    srh.lock();
38,837,421✔
595

596
    // Accumulate delta psi into new estimate of source region flux for
597
    // this iteration
598
    for (int g = 0; g < negroups_; g++) {
173,362,936✔
599
      srh.scalar_flux_new(g) += angular_flux_[g] * distance;
134,525,515✔
600
    }
601

602
    // Accomulate volume (ray distance) into this iteration's estimate
603
    // of the source region's volume
604
    srh.volume() += distance;
38,837,421✔
605
    srh.centroid_iteration() += midpoint * distance;
38,837,421✔
606
    srh.volume_sq() += distance * distance;
38,837,421✔
607
    srh.n_hits() += 1;
38,837,421✔
608

609
    // Tally valid position inside the source region (e.g., midpoint of
610
    // the ray) if not done already
611
    if (!srh.position_recorded()) {
38,837,421✔
612
      srh.position() = midpoint;
52,996✔
613
      srh.position_recorded() = 1;
52,996✔
614
    }
615

616
    // Release lock
617
    srh.unlock();
21,208,486✔
618
  }
619

620
  // Add source to incoming angular flux, assuming void region
621
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
48,548,645!
622
    for (int g = 0; g < negroups_; g++) {
217,705,142✔
623
      angular_flux_[g] += srh.external_source(g) * distance;
169,156,497✔
624
    }
625
  }
626
}
48,548,645✔
627

628
void RandomRay::attenuate_flux_linear_source(
667,264,818✔
629
  SourceRegionHandle& srh, double distance, bool is_active, Position r)
630
{
631
  // The number of geometric intersections is counted for reporting purposes
632
  n_event()++;
667,264,818✔
633

634
  int material = srh.material();
667,264,818✔
635
  int temp = srh.temperature_idx();
667,264,818✔
636

637
  Position& centroid = srh.centroid();
667,264,818✔
638
  Position midpoint = r + u() * (distance / 2.0);
667,264,818✔
639

640
  // Determine the local position of the midpoint and the ray origin
641
  // relative to the source region's centroid
642
  Position rm_local;
667,264,818✔
643
  Position r0_local;
667,264,818✔
644

645
  // In the first few iterations of the simulation, the source region
646
  // may not yet have had any ray crossings, in which case there will
647
  // be no estimate of its centroid. We detect this by checking if it has
648
  // any accumulated volume. If its volume is zero, just use the midpoint
649
  // of the ray as the region's centroid.
650
  if (srh.volume_t()) {
667,264,818✔
651
    rm_local = midpoint - centroid;
640,949,089✔
652
    r0_local = r - centroid;
640,949,089✔
653
  } else {
654
    rm_local = {0.0, 0.0, 0.0};
26,315,729✔
655
    r0_local = -u() * 0.5 * distance;
52,631,458✔
656
  }
657
  double distance_2 = distance * distance;
667,264,818✔
658

659
  // Linear Source MOC incoming flux attenuation + source
660
  // contribution/attenuation equation
661
  for (int g = 0; g < negroups_; g++) {
2,147,483,647✔
662

663
    // Compute tau, the optical thickness of the ray segment
664
    float sigma_t =
2,147,483,647✔
665
      domain_->sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] *
2,147,483,647✔
666
      srh.density_mult();
2,147,483,647✔
667
    float tau = sigma_t * distance;
2,147,483,647✔
668

669
    // If tau is very small, set it to zero to avoid numerical issues.
670
    // The following computations will still work with tau = 0.
671
    if (tau < 1.0e-8f) {
2,147,483,647✔
672
      tau = 0.0f;
5,874✔
673
    }
674

675
    // Compute linear source terms, spatial and directional (dir),
676
    // calculated from the source gradients dot product with local centroid
677
    // and direction, respectively.
678
    float spatial_source =
2,147,483,647✔
679
      srh.source(g) + rm_local.dot(srh.source_gradients(g));
2,147,483,647✔
680
    float dir_source = u().dot(srh.source_gradients(g));
2,147,483,647✔
681

682
    float gn = exponentialG(tau);
2,147,483,647✔
683
    float f1 = 1.0f - tau * gn;
2,147,483,647✔
684
    float f2 = (2.0f * gn - f1) * distance_2;
2,147,483,647✔
685
    float new_delta_psi = (angular_flux_[g] - spatial_source) * f1 * distance -
2,147,483,647✔
686
                          0.5 * dir_source * f2;
2,147,483,647✔
687

688
    float h1 = f1 - gn;
2,147,483,647✔
689
    float g1 = 0.5f - h1;
2,147,483,647✔
690
    float g2 = exponentialG2(tau);
2,147,483,647✔
691
    g1 = g1 * spatial_source;
2,147,483,647✔
692
    g2 = g2 * dir_source * distance * 0.5f;
2,147,483,647✔
693
    h1 = h1 * angular_flux_[g];
2,147,483,647✔
694
    h1 = (g1 + g2 + h1) * distance_2;
2,147,483,647✔
695
    spatial_source = spatial_source * distance + new_delta_psi;
2,147,483,647✔
696

697
    // Store contributions for this group into arrays, so that they can
698
    // be accumulated into the source region's estimates inside of the locked
699
    // region.
700
    delta_psi_[g] = new_delta_psi;
2,147,483,647✔
701
    delta_moments_[g] = r0_local * spatial_source + u() * h1;
2,147,483,647✔
702

703
    // Update the angular flux for this group
704
    angular_flux_[g] -= new_delta_psi * sigma_t;
2,147,483,647✔
705

706
    // If 2D mode is enabled, the z-component of the flux moments is forced
707
    // to zero
708
    if (source_shape_ == RandomRaySourceShape::LINEAR_XY) {
2,147,483,647✔
709
      delta_moments_[g].z = 0.0;
462,208,043✔
710
    }
711
  }
712

713
  // Compute an estimate of the spatial moments matrix for the source
714
  // region based on parameters from this ray's crossing
715
  MomentMatrix moment_matrix_estimate;
667,264,818✔
716
  moment_matrix_estimate.compute_spatial_moments_matrix(
667,264,818✔
717
    rm_local, u(), distance);
667,264,818✔
718

719
  // Aquire lock for source region
720
  srh.lock();
667,264,818✔
721

722
  // If ray is in the active phase (not in dead zone), make contributions to
723
  // source region bookkeeping
724

725
  if (is_active_) {
667,264,818✔
726
    // Accumulate deltas into the new estimate of source region flux for this
727
    // iteration
728
    for (int g = 0; g < negroups_; g++) {
2,147,483,647✔
729
      srh.scalar_flux_new(g) += delta_psi_[g];
1,951,227,256✔
730
      srh.flux_moments_new(g) += delta_moments_[g];
1,951,227,256✔
731
    }
732

733
    // Accumulate the volume (ray segment distance), centroid, and spatial
734
    // momement estimates into the running totals for the iteration for this
735
    // source region. The centroid and spatial momements estimates are scaled
736
    // by the ray segment length as part of length averaging of the estimates.
737
    srh.volume() += distance;
546,952,890✔
738
    srh.centroid_iteration() += midpoint * distance;
546,952,890✔
739
    moment_matrix_estimate *= distance;
546,952,890✔
740
    srh.mom_matrix() += moment_matrix_estimate;
546,952,890✔
741

742
    srh.n_hits() += 1;
546,952,890✔
743
  }
744

745
  // Tally valid position inside the source region (e.g., midpoint of
746
  // the ray) if not done already
747
  if (!srh.position_recorded()) {
667,264,818✔
748
    srh.position() = midpoint;
811,076✔
749
    srh.position_recorded() = 1;
811,076✔
750
  }
751

752
  // Release lock
753
  srh.unlock();
667,264,818✔
754
}
667,264,818✔
755

756
// If traveling through a void region, the source term is either zero
757
// or an external source. As all external sources are currently assumed
758
// to be flat, we don't really need this function and could instead just call
759
// the "attenuate_flux_flat_source_void" function and get the same numerical and
760
// tally results. However, computation of the flux moments in void regions is
761
// nonetheless useful as this information is still used by the plotter when
762
// estimating the flux at specific pixel coordinates. Thus, plots will look
763
// nicer/more accurate if we record flux moments, so this function is useful.
764
void RandomRay::attenuate_flux_linear_source_void(
8,287,235✔
765
  SourceRegionHandle& srh, double distance, bool is_active, Position r)
766
{
767
  // The number of geometric intersections is counted for reporting purposes
768
  n_event()++;
8,287,235✔
769

770
  Position& centroid = srh.centroid();
8,287,235✔
771
  Position midpoint = r + u() * (distance / 2.0);
8,287,235✔
772

773
  // Determine the local position of the midpoint and the ray origin
774
  // relative to the source region's centroid
775
  Position rm_local;
8,287,235✔
776
  Position r0_local;
8,287,235✔
777

778
  // In the first few iterations of the simulation, the source region
779
  // may not yet have had any ray crossings, in which case there will
780
  // be no estimate of its centroid. We detect this by checking if it has
781
  // any accumulated volume. If its volume is zero, just use the midpoint
782
  // of the ray as the region's centroid.
783
  if (srh.volume_t()) {
8,287,235✔
784
    rm_local = midpoint - centroid;
8,084,186✔
785
    r0_local = r - centroid;
8,084,186✔
786
  } else {
787
    rm_local = {0.0, 0.0, 0.0};
203,049✔
788
    r0_local = -u() * 0.5 * distance;
406,098✔
789
  }
790
  double distance_2 = distance * distance;
8,287,235✔
791

792
  // Compared to linear flux attenuation through solid regions,
793
  // transport through a void region is greatly simplified. Here we
794
  // compute the updated flux moments.
795
  for (int g = 0; g < negroups_; g++) {
16,574,470✔
796
    float spatial_source = 0.f;
8,287,235✔
797
    if (settings::run_mode == RunMode::FIXED_SOURCE) {
8,287,235!
798
      spatial_source = srh.external_source(g);
8,287,235✔
799
    }
800
    float new_delta_psi = (angular_flux_[g] - spatial_source) * distance;
8,287,235✔
801
    float h1 = 0.5f;
8,287,235✔
802
    h1 = h1 * angular_flux_[g];
8,287,235✔
803
    h1 = h1 * distance_2;
8,287,235✔
804
    spatial_source = spatial_source * distance + new_delta_psi;
8,287,235✔
805

806
    // Store contributions for this group into arrays, so that they can
807
    // be accumulated into the source region's estimates inside of the locked
808
    // region.
809
    delta_moments_[g] = r0_local * spatial_source + u() * h1;
8,287,235!
810

811
    // If 2D mode is enabled, the z-component of the flux moments is forced
812
    // to zero
813
    if (source_shape_ == RandomRaySourceShape::LINEAR_XY) {
8,287,235!
814
      delta_moments_[g].z = 0.0;
×
815
    }
816
  }
817

818
  // If ray is in the active phase (not in dead zone), make contributions to
819
  // source region bookkeeping
820
  if (is_active_) {
8,287,235✔
821
    // Compute an estimate of the spatial moments matrix for the source
822
    // region based on parameters from this ray's crossing
823
    MomentMatrix moment_matrix_estimate;
6,892,402✔
824
    moment_matrix_estimate.compute_spatial_moments_matrix(
6,892,402✔
825
      rm_local, u(), distance);
6,892,402✔
826

827
    // Aquire lock for source region
828
    srh.lock();
3,759,492✔
829

830
    // Accumulate delta psi into new estimate of source region flux for
831
    // this iteration, and update flux momements
832
    for (int g = 0; g < negroups_; g++) {
13,784,804✔
833
      srh.scalar_flux_new(g) += angular_flux_[g] * distance;
6,892,402✔
834
      srh.flux_moments_new(g) += delta_moments_[g];
6,892,402✔
835
    }
836

837
    // Accumulate the volume (ray segment distance), centroid, and spatial
838
    // momement estimates into the running totals for the iteration for this
839
    // source region. The centroid and spatial momements estimates are scaled by
840
    // the ray segment length as part of length averaging of the estimates.
841
    srh.volume() += distance;
6,892,402✔
842
    srh.volume_sq() += distance_2;
6,892,402✔
843
    srh.centroid_iteration() += midpoint * distance;
6,892,402✔
844
    moment_matrix_estimate *= distance;
6,892,402✔
845
    srh.mom_matrix() += moment_matrix_estimate;
6,892,402✔
846

847
    // Tally valid position inside the source region (e.g., midpoint of
848
    // the ray) if not done already
849
    if (!srh.position_recorded()) {
6,892,402✔
850
      srh.position() = midpoint;
11,000✔
851
      srh.position_recorded() = 1;
11,000✔
852
    }
853

854
    srh.n_hits() += 1;
6,892,402✔
855

856
    // Release lock
857
    srh.unlock();
6,892,402✔
858
  }
859

860
  // Add source to incoming angular flux, assuming void region
861
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
8,287,235!
862
    for (int g = 0; g < negroups_; g++) {
16,574,470✔
863
      angular_flux_[g] += srh.external_source(g) * distance;
8,287,235✔
864
    }
865
  }
866
}
8,287,235✔
867

868
void RandomRay::restart_ray(FlatSourceDomain* domain, RayExchangeData& data,
15,049,765✔
869
  float* angular_flux, LocalCoord* coord, int* cell_last_data)
870
{
871

872
  domain_ = domain;
15,049,765✔
873
  distance_travelled_ = data.distance_travelled;
15,049,765✔
874
  owner_rank_ = mpi::rank;
15,049,765✔
875
  ntemperature_ = domain->ntemperature_;
15,049,765✔
876

877
  // Restore particle event counter from the transmitted ray
878
  // This preserves the event count across MPI rank boundaries
879
  n_event() = data.n_event;
15,049,765✔
880

881
  is_active_ = data.is_active;
15,049,765✔
882

883
  wgt() = 1.0;
15,049,765✔
884

885
  // set identifier for particle
886
  id() = data.ray_id;
15,049,765✔
887

888
  // Restore GeometryState scalar fields
889
  n_coord() = data.n_coord;
15,049,765✔
890
  cell_instance() = data.cell_instance;
15,049,765✔
891
  n_coord_last() = data.n_coord_last;
15,049,765✔
892
  material() = data.material;
15,049,765✔
893
  material_last() = data.material_last;
15,049,765✔
894
  sqrtkT() = data.sqrtkT;
15,049,765✔
895
  sqrtkT_last() = data.sqrtkT_last;
15,049,765✔
896
  surface() = data.surface;
15,049,765✔
897

898
  // Restore LocalCoord vector data (coord_)
899
  // The vectors were already sized to model::n_coord_levels in the
900
  // GeometryState constructor LocalCoord is POD so we can just copy the entire
901
  // structure
902
  const int n_coord_max = model::n_coord_levels;
15,049,765✔
903
  for (int i = 0; i < n_coord_max; i++) {
51,018,697✔
904
    this->coord(i) = coord[i];
35,968,932✔
905
    cell_last(i) = cell_last_data[i];
35,968,932✔
906
  }
907

908
  // Override the position and direction at ALL coordinate levels with the
909
  // buffered values These may have been adjusted when the ray left the previous
910
  // subdomain We need to update all levels to maintain consistency in the
911
  // coordinate hierarchy
912
  Position delta_r = data.position - r();
15,049,765✔
913
  for (int i = 0; i < n_coord(); i++) {
50,418,805✔
914
    this->coord(i).r() += delta_r;
35,369,040✔
915
  }
916

917
  u() = data.direction;
15,049,765!
918

919
#ifdef OPENMC_DAGMC_ENABLED
920
  // Restore DAGMC fields
921
  // Restore last_dir and rebuild the history by adding entities in reverse
922
  // order
923
  last_dir() = data.last_dir;
3,590,585✔
924
  for (int i = data.n_handles - 1; i >= 0; i--) {
3,590,585!
925
    history().add_entity(data.handles[i]);
926
  }
927
#endif
928

929
  // Set particle type and energy (for random ray, these are not actually used)
930
  type() = ParticleType::neutron();
15,049,765✔
931
  E() = 0.0;
15,049,765✔
932

933
  // No need to call exhaustive_find_cell() since we have the full geometry
934
  // state! Just verify we have valid cell information
935
  if (lowest_coord().cell() == C_NONE) {
15,049,765!
NEW
936
    this->mark_as_lost("Received particle " + std::to_string(id()) +
×
937
                       " with invalid cell information");
938
  }
939

940
  // Set birth cell attribute if not set
941
  if (cell_born() == C_NONE)
15,049,765!
942
    cell_born() = lowest_coord().cell();
15,049,765✔
943

944
  // Set ray's angular flux to value before subdomain change
945
  if (distance_travelled_ > 0.0 || is_active_) {
15,049,765!
946
    for (int g = 0; g < negroups_; g++) {
91,679,447✔
947
      angular_flux_[g] = angular_flux[g];
77,030,005✔
948
    }
949
  }
950
  // Initialize ray's starting angular flux to starting location's isotropic
951
  // source
952
  else {
953
    SourceRegionKey sr_key = domain_->lookup_source_region_key(*this);
400,323✔
954
    SourceRegionHandle srh =
400,323✔
955
      domain_->get_subdivided_source_region_handle(sr_key, r(), u());
400,323✔
956

957
    if (!srh.is_numerical_fp_artifact_) {
400,323!
958
      for (int g = 0; g < negroups_; g++) {
1,412,640✔
959
        angular_flux_[g] = srh.source(g);
1,012,317✔
960
      }
961
    }
962
  }
963
}
15,049,765✔
964

965
void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain)
3,197,320✔
966
{
967
  domain_ = domain;
3,197,320✔
968
  owner_rank_ = mpi::rank;
3,197,320✔
969
  ntemperature_ = domain->ntemperature_;
3,197,320✔
970

971
  // Reset particle event counter
972
  n_event() = 0;
3,197,320✔
973

974
  is_active_ = (distance_inactive_ <= 0.0);
3,197,320✔
975

976
  wgt() = 1.0;
3,197,320✔
977

978
  // set identifier for particle
979
  id() = ray_id;
3,197,320✔
980

981
  // generate source site using sample method
982
  SourceSite site;
3,197,320✔
983
  switch (sample_method_) {
3,197,320!
984
  case RandomRaySampleMethod::PRNG:
2,273,320✔
985
    site = sample_prng();
2,273,320✔
986
    break;
987
  case RandomRaySampleMethod::HALTON:
891,000✔
988
    site = sample_halton();
891,000✔
989
    break;
990
  case RandomRaySampleMethod::S2:
33,000✔
991
    site = sample_s2();
33,000✔
992
    break;
993
  default:
×
994
    fatal_error("Unknown sample method for random ray transport.");
×
995
  }
996

997
  site.E = 0.0;
3,197,320✔
998
  this->from_source(&site);
3,197,320✔
999

1000
  // Locate ray
1001
  if (lowest_coord().cell() == C_NONE) {
3,197,320!
1002
    if (!exhaustive_find_cell(*this)) {
3,197,320!
1003
      this->mark_as_lost(
×
1004
        "Could not find the cell containing particle " + std::to_string(id()));
×
1005
    }
1006

1007
    // Set birth cell attribute
1008
    if (cell_born() == C_NONE)
3,197,320!
1009
      cell_born() = lowest_coord().cell();
3,197,320✔
1010
  }
1011

1012
  SourceRegionKey sr_key = domain_->lookup_source_region_key(*this);
3,197,320✔
1013

1014
#ifdef OPENMC_MPI
1015
  if (mpi::n_procs > 1) {
1,163,120✔
1016
    // Check if ray sampling site belongs to subdomain
1017
    owner_rank_ = mpi::decomp_map.find_owner(
1,604,800✔
1018
      sr_key, r(), domain_->discovered_source_regions_);
802,400✔
1019

1020
    if (owner_rank_ != mpi::rank) {
802,400✔
1021
      for (int g = 0; g < negroups_; g++) {
1,412,640✔
1022
        angular_flux_[g] = 0.0;
1,012,317✔
1023
      }
1024
      pack_ray_for_buffer(0.0, r());
400,323✔
1025
      is_local_ = false;
400,323✔
1026
      return;
400,323✔
1027
    }
1028
  }
1029
#endif
1030

1031
  SourceRegionHandle srh =
2,796,997✔
1032
    domain_->get_subdivided_source_region_handle(sr_key, r(), u());
2,796,997✔
1033

1034
  if (!srh.is_numerical_fp_artifact_) {
2,796,997!
1035
    for (int g = 0; g < negroups_; g++) {
11,692,720✔
1036
      angular_flux_[g] = srh.source(g);
8,895,723✔
1037
    }
1038
  }
1039
}
2,034,200✔
1040

1041
SourceSite RandomRay::sample_prng()
2,273,320✔
1042
{
1043
  // set random number seed
1044
  int64_t particle_seed =
2,273,320✔
1045
    (simulation::current_batch - 1) * settings::n_particles + id();
2,273,320✔
1046
  init_particle_seeds(particle_seed, seeds());
2,273,320✔
1047
  stream() = STREAM_TRACKING;
2,273,320✔
1048

1049
  // Sample from ray source distribution
1050
  SourceSite site {ray_source_->sample(current_seed())};
2,273,320✔
1051

1052
  return site;
2,273,320✔
1053
}
1054

1055
SourceSite RandomRay::sample_halton()
891,000✔
1056
{
1057
  SourceSite site;
891,000✔
1058

1059
  // Set random number seed
1060
  int64_t batch_seed = (simulation::current_batch - 1) * settings::n_particles;
891,000✔
1061
  int64_t skip = id();
891,000✔
1062
  init_particle_seeds(batch_seed, seeds());
891,000✔
1063
  stream() = STREAM_TRACKING;
891,000✔
1064

1065
  // Calculate next samples in LDS across 5 dimensions
1066
  vector<double> samples = rhalton(5, current_seed(), skip = skip);
891,000✔
1067

1068
  // Get spatial box of ray_source_
1069
  SpatialBox* sb = dynamic_cast<SpatialBox*>(
1,782,000!
1070
    dynamic_cast<IndependentSource*>(RandomRay::ray_source_.get())->space());
891,000!
1071

1072
  // Sample spatial distribution
1073
  Position xi {samples[0], samples[1], samples[2]};
891,000✔
1074
  // make a small shift in position to avoid geometry floating point issues
1075
  Position shift {FP_COINCIDENT, FP_COINCIDENT, FP_COINCIDENT};
891,000✔
1076
  site.r = (sb->lower_left() + shift) +
891,000✔
1077
           xi * ((sb->upper_right() - shift) - (sb->lower_left() + shift));
891,000✔
1078

1079
  // Sample Polar cosine and azimuthal angles
1080
  double mu = 2.0 * samples[3] - 1.0;
891,000✔
1081
  double azi = 2.0 * PI * samples[4];
891,000✔
1082
  // Convert to Cartesian coordinates
1083
  double c = std::sqrt(1.0 - mu * mu);
891,000✔
1084
  site.u.x = mu;
891,000✔
1085
  site.u.y = std::cos(azi) * c;
891,000✔
1086
  site.u.z = std::sin(azi) * c;
891,000✔
1087

1088
  return site;
891,000✔
1089
}
891,000✔
1090

1091
bool RandomRay::has_left_subdomain()
2,147,483,647✔
1092
{
1093
  return !is_local_;
2,147,483,647✔
1094
}
1095

1096
void RandomRay::pack_ray_for_buffer(
15,049,765✔
1097
  double distance_buffer, Position position_buffer)
1098
{
1099
  exchange_data_.position = position_buffer;
15,049,765✔
1100
  exchange_data_.direction = u();
15,049,765✔
1101
  exchange_data_.angular_flux = angular_flux_;
15,049,765✔
1102
  exchange_data_.distance_travelled = distance_buffer;
15,049,765✔
1103
  exchange_data_.surface = surface();
15,049,765✔
1104
  exchange_data_.is_active = is_active_;
15,049,765✔
1105
  exchange_data_.ray_id = id();
15,049,765✔
1106
  exchange_data_.n_event = n_event();
15,049,765✔
1107

1108
  // Pack GeometryState scalar fields
1109
  exchange_data_.n_coord = n_coord();
15,049,765✔
1110
  exchange_data_.cell_instance = cell_instance();
15,049,765✔
1111
  exchange_data_.n_coord_last = n_coord_last();
15,049,765✔
1112
  exchange_data_.material = material();
15,049,765✔
1113
  exchange_data_.material_last = material_last();
15,049,765✔
1114
  exchange_data_.sqrtkT = sqrtkT();
15,049,765✔
1115
  exchange_data_.sqrtkT_last = sqrtkT_last();
15,049,765✔
1116

1117
  // Pack GeometryState vector fields
1118
  // LocalCoord is POD, so we can just copy the entire vector
1119
  // We always pack model::n_coord_levels elements to ensure consistent sizes
1120
  const int n_coord_max = model::n_coord_levels;
15,049,765✔
1121

1122
  exchange_data_.coord.resize(n_coord_max);
15,049,765✔
1123
  exchange_data_.cell_last.resize(n_coord_max);
15,049,765✔
1124

1125
  for (int i = 0; i < n_coord_max; i++) {
51,018,697✔
1126
    exchange_data_.coord[i] = coord(i);
35,968,932✔
1127
    exchange_data_.cell_last[i] = cell_last(i);
35,968,932✔
1128
  }
1129

1130
#ifdef OPENMC_DAGMC_ENABLED
1131
  // Pack DAGMC fields
1132
  // Extract up to MAX_N_HANDLES from the ray history by rolling back
1133
  exchange_data_.last_dir = last_dir();
3,590,585✔
1134
  exchange_data_.n_handles = 0;
3,590,585✔
1135

1136
  for (int i = 0; i < MAX_N_HANDLES; i++) {
3,590,585!
1137
    moab::EntityHandle handle;
3,590,585✔
1138
    if (history().get_last_intersection(handle) == moab::MB_SUCCESS) {
3,590,585!
1139
      exchange_data_.handles[i] = handle;
1140
      exchange_data_.n_handles++;
1141
      history().rollback_last_intersection();
1142
    } else {
1143
      // No more handles in history
1144
      break;
1145
    }
1146
  }
1147
#endif
1148
}
15,049,765✔
1149

1150
SourceSite RandomRay::sample_s2()
33,000✔
1151
{
1152
  // set random number seed
1153
  int64_t particle_seed =
33,000✔
1154
    (simulation::current_batch - 1) * settings::n_particles + id();
33,000✔
1155
  init_particle_seeds(particle_seed, seeds());
33,000✔
1156
  stream() = STREAM_TRACKING;
33,000✔
1157

1158
  // Get spatial component of the ray_source_
1159
  SpatialDistribution* space =
33,000!
1160
    dynamic_cast<IndependentSource*>(RandomRay::ray_source_.get())->space();
33,000!
1161

1162
  SourceSite site;
33,000✔
1163

1164
  // Sample spatial distribution
1165
  site.r = space->sample(current_seed()).first;
33,000✔
1166

1167
  // Sample either left or right for S2 (flashlight) transport.
1168
  site.u = {prn(current_seed()) < 0.5 ? -1.0 : 1.0, 0.0, 0.0};
33,000✔
1169

1170
  return site;
33,000✔
1171
}
1172

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

© 2026 Coveralls, Inc