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

openmc-dev / openmc / 22282433317

22 Feb 2026 06:05PM UTC coverage: 81.033% (-0.7%) from 81.688%
22282433317

Pull #3817

github

web-flow
Merge 155a36c12 into 83a30f686
Pull Request #3817: Fix MeshFilter.get_pandas_dataframe to handle all mesh types

16085 of 22646 branches covered (71.03%)

Branch coverage included in aggregate %.

23 of 25 new or added lines in 4 files covered. (92.0%)

1149 existing lines in 38 files now uncovered.

55961 of 66264 relevant lines covered (84.45%)

9160039.43 hits per line

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

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

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

13
#include <numeric>
14

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

19
namespace openmc {
20

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

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

38
  constexpr float c0d = 1.0f;
176,835,455✔
39
  constexpr float c1d = -0.73151337729389001396f;
176,835,455✔
40
  constexpr float c2d = 0.26058381273536471371f;
176,835,455✔
41
  constexpr float c3d = -0.059892419041316836940f;
176,835,455✔
42
  constexpr float c4d = 0.0099070188241094279067f;
176,835,455✔
43
  constexpr float c5d = -0.0012623388962473160860f;
176,835,455✔
44
  constexpr float c6d = 0.00010361277635498731388f;
176,835,455✔
45
  constexpr float c7d = -0.000013276569500666698498f;
176,835,455✔
46

47
  float x = -tau;
176,835,455✔
48

49
  float den = c7d;
176,835,455✔
50
  den = den * x + c6d;
176,835,455✔
51
  den = den * x + c5d;
176,835,455✔
52
  den = den * x + c4d;
176,835,455✔
53
  den = den * x + c3d;
176,835,455✔
54
  den = den * x + c2d;
176,835,455✔
55
  den = den * x + c1d;
176,835,455✔
56
  den = den * x + c0d;
176,835,455✔
57

58
  float num = c7n;
176,835,455✔
59
  num = num * x + c6n;
176,835,455✔
60
  num = num * x + c5n;
176,835,455✔
61
  num = num * x + c4n;
176,835,455✔
62
  num = num * x + c3n;
176,835,455✔
63
  num = num * x + c2n;
176,835,455✔
64
  num = num * x + c1n;
176,835,455✔
65
  num = num * x;
176,835,455✔
66

67
  return num / den;
176,835,455✔
68
}
69

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

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

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

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

121
  float x = tau;
219,544,182✔
122

123
  float num = d5n;
219,544,182✔
124
  num = num * x + d4n;
219,544,182✔
125
  num = num * x + d3n;
219,544,182✔
126
  num = num * x + d2n;
219,544,182✔
127
  num = num * x + d1n;
219,544,182✔
128
  num = num * x + d0n;
219,544,182✔
129

130
  float den = d6d;
219,544,182✔
131
  den = den * x + d5d;
219,544,182✔
132
  den = den * x + d4d;
219,544,182✔
133
  den = den * x + d3d;
219,544,182✔
134
  den = den * x + d2d;
219,544,182✔
135
  den = den * x + d1d;
219,544,182✔
136
  den = den * x + d0d;
219,544,182✔
137

138
  return num / den;
219,544,182✔
139
}
140

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

149
  // Coefficients for numerator in rational approximation
150
  constexpr float g1n = -0.08335775885589858f;
219,544,182✔
151
  constexpr float g2n = -0.003603942303847604f;
219,544,182✔
152
  constexpr float g3n = 0.0037673183263550827f;
219,544,182✔
153
  constexpr float g4n = 0.00001124183494990467f;
219,544,182✔
154
  constexpr float g5n = 0.00016837426505799449f;
219,544,182✔
155

156
  // Coefficients for denominator in rational approximation
157
  constexpr float g1d = 0.7454048371823628f;
219,544,182✔
158
  constexpr float g2d = 0.23794300531408347f;
219,544,182✔
159
  constexpr float g3d = 0.05367250964303789f;
219,544,182✔
160
  constexpr float g4d = 0.006125197988351906f;
219,544,182✔
161
  constexpr float g5d = 0.0010102514456857377f;
219,544,182✔
162

163
  float x = tau;
219,544,182✔
164

165
  float num = g5n;
219,544,182✔
166
  num = num * x + g4n;
219,544,182✔
167
  num = num * x + g3n;
219,544,182✔
168
  num = num * x + g2n;
219,544,182✔
169
  num = num * x + g1n;
219,544,182✔
170
  num = num * x;
219,544,182✔
171

172
  float den = g5d;
219,544,182✔
173
  den = den * x + g4d;
219,544,182✔
174
  den = den * x + g3d;
219,544,182✔
175
  den = den * x + g2d;
219,544,182✔
176
  den = den * x + g1d;
219,544,182✔
177
  den = den * x + 1.0f;
219,544,182✔
178

179
  return num / den;
219,544,182✔
180
}
181

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

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

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

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

227
    halton[D] = ans;
5,000✔
228
  }
229

230
  return halton;
2,000✔
231
}
1,000✔
232

233
//==============================================================================
234
// RandomRay implementation
235
//==============================================================================
236

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

244
RandomRay::RandomRay()
195,600✔
245
  : angular_flux_(data::mg.num_energy_groups_),
195,600✔
246
    delta_psi_(data::mg.num_energy_groups_),
195,600✔
247
    negroups_(data::mg.num_energy_groups_)
586,800✔
248
{
249
  if (source_shape_ == RandomRaySourceShape::LINEAR ||
195,600✔
250
      source_shape_ == RandomRaySourceShape::LINEAR_XY) {
110,900✔
251
    delta_moments_.resize(negroups_);
96,050✔
252
  }
253
}
195,600✔
254

255
RandomRay::RandomRay(uint64_t ray_id, FlatSourceDomain* domain) : RandomRay()
195,600✔
256
{
257
  initialize_ray(ray_id, domain);
195,600✔
258
}
195,600✔
259

260
// Transports ray until termination criteria are met
261
uint64_t RandomRay::transport_history_based_single_ray()
195,600✔
262
{
263
  using namespace openmc;
264
  while (alive()) {
85,162,734!
265
    event_advance_ray();
85,162,734✔
266
    if (!alive())
85,162,734✔
267
      break;
195,600✔
268
    event_cross_surface();
84,967,134✔
269
    // If ray has too many events, display warning and kill it
270
    if (n_event() >= settings::max_particle_events) {
84,967,134!
271
      warning("Ray " + std::to_string(id()) +
×
272
              " underwent maximum number of events, terminating ray.");
273
      wgt() = 0.0;
×
274
    }
275
  }
276

277
  return n_event();
195,600✔
278
}
279

280
// Transports ray across a single source region
281
void RandomRay::event_advance_ray()
85,162,734✔
282
{
283
  // If geometry debug mode is on, check for cell overlaps
284
  if (settings::check_overlaps)
85,162,734!
285
    check_cell_overlap(*this);
×
286

287
  // Find the distance to the nearest boundary
288
  boundary() = distance_to_boundary(*this);
85,162,734✔
289
  double distance = boundary().distance();
85,162,734✔
290

291
  if (distance < 0.0) {
85,162,734!
292
    mark_as_lost("Negative transport distance detected for particle " +
×
293
                 std::to_string(id()));
×
294
    return;
×
295
  }
296

297
  if (is_active_) {
85,162,734✔
298
    // If the ray is in the active length, need to check if it has
299
    // reached its maximum termination distance. If so, reduce
300
    // the ray traced length so that the ray does not overrun the
301
    // maximum numerical length (so as to avoid numerical bias).
302
    if (distance_travelled_ + distance >= distance_active_) {
69,278,424✔
303
      distance = distance_active_ - distance_travelled_;
195,600✔
304
      wgt() = 0.0;
195,600✔
305
    }
306

307
    distance_travelled_ += distance;
69,278,424✔
308
    attenuate_flux(distance, true);
69,278,424✔
309
  } else {
310
    // If the ray is still in the dead zone, need to check if it
311
    // has entered the active phase. If so, split into two segments (one
312
    // representing the final part of the dead zone, the other representing the
313
    // first part of the active length) and attenuate each. Otherwise, if the
314
    // full length of the segment is within the dead zone, attenuate as normal.
315
    if (distance_travelled_ + distance >= distance_inactive_) {
15,884,310✔
316
      is_active_ = true;
195,600✔
317
      double distance_dead = distance_inactive_ - distance_travelled_;
195,600✔
318
      attenuate_flux(distance_dead, false);
195,600✔
319

320
      double distance_alive = distance - distance_dead;
195,600✔
321

322
      // Ensure we haven't travelled past the active phase as well
323
      if (distance_alive > distance_active_) {
195,600!
324
        distance_alive = distance_active_;
×
325
        wgt() = 0.0;
×
326
      }
327

328
      attenuate_flux(distance_alive, true, distance_dead);
195,600✔
329
      distance_travelled_ = distance_alive;
195,600✔
330
    } else {
331
      distance_travelled_ += distance;
15,688,710✔
332
      attenuate_flux(distance, false);
15,688,710✔
333
    }
334
  }
335

336
  // Advance particle
337
  for (int j = 0; j < n_coord(); ++j) {
327,147,891✔
338
    coord(j).r() += distance * coord(j).u();
241,985,157✔
339
  }
340
}
341

342
void RandomRay::attenuate_flux(double distance, bool is_active, double offset)
85,358,334✔
343
{
344
  // Lookup base source region index
345
  int64_t sr = domain_->lookup_base_source_region_idx(*this);
85,358,334✔
346

347
  // Perform ray tracing across mesh
348
  // Determine the mesh index for the base source region, if any
349
  int mesh_idx = domain_->lookup_mesh_idx(sr);
85,358,334✔
350

351
  if (mesh_idx == C_NONE) {
85,358,334✔
352
    // If there's no mesh being applied to this cell, then
353
    // we just attenuate the flux as normal, and set
354
    // the mesh bin to 0
355
    attenuate_flux_inner(distance, is_active, sr, 0, r());
42,200,019✔
356
  } else {
357
    // If there is a mesh being applied to this cell, then
358
    // we loop over all the bin crossings and attenuate
359
    // separately.
360
    Mesh* mesh = model::meshes[mesh_idx].get();
43,158,315✔
361

362
    // We adjust the start and end positions of the ray slightly
363
    // to accomodate for floating point precision issues that tend
364
    // to occur at mesh boundaries that overlap with geometry lattice
365
    // boundaries.
366
    Position start = r() + (offset + TINY_BIT) * u();
43,158,315✔
367
    Position end = start + (distance - 2.0 * TINY_BIT) * u();
43,158,315✔
368
    double reduced_distance = (end - start).norm();
43,158,315✔
369

370
    // Ray trace through the mesh and record bins and lengths
371
    mesh_bins_.resize(0);
43,158,315✔
372
    mesh_fractional_lengths_.resize(0);
43,158,315✔
373
    mesh->bins_crossed(start, end, u(), mesh_bins_, mesh_fractional_lengths_);
43,158,315✔
374

375
    // Loop over all mesh bins and attenuate flux
376
    for (int b = 0; b < mesh_bins_.size(); b++) {
128,555,509✔
377
      double physical_length = reduced_distance * mesh_fractional_lengths_[b];
85,397,194✔
378
      attenuate_flux_inner(
85,397,194✔
379
        physical_length, is_active, sr, mesh_bins_[b], start);
85,397,194✔
380
      start += physical_length * u();
85,397,194✔
381
    }
382
  }
383
}
85,358,334✔
384

385
void RandomRay::attenuate_flux_inner(
127,597,213✔
386
  double distance, bool is_active, int64_t sr, int mesh_bin, Position r)
387
{
388
  SourceRegionKey sr_key {sr, mesh_bin};
127,597,213✔
389
  SourceRegionHandle srh;
127,597,213✔
390
  srh = domain_->get_subdivided_source_region_handle(sr_key, r, u());
127,597,213✔
391
  if (srh.is_numerical_fp_artifact_) {
127,597,213✔
392
    return;
8✔
393
  }
394

395
  switch (source_shape_) {
127,597,205!
396
  case RandomRaySourceShape::FLAT:
66,183,382✔
397
    if (srh.material() == MATERIAL_VOID) {
66,183,382✔
398
      attenuate_flux_flat_source_void(srh, distance, is_active, r);
754,787✔
399
    } else {
400
      attenuate_flux_flat_source(srh, distance, is_active, r);
65,428,595✔
401
    }
402
    break;
66,183,382✔
403
  case RandomRaySourceShape::LINEAR:
61,413,823✔
404
  case RandomRaySourceShape::LINEAR_XY:
405
    if (srh.material() == MATERIAL_VOID) {
61,413,823✔
406
      attenuate_flux_linear_source_void(srh, distance, is_active, r);
753,385✔
407
    } else {
408
      attenuate_flux_linear_source(srh, distance, is_active, r);
60,660,438✔
409
    }
410
    break;
61,413,823✔
411
  default:
×
412
    fatal_error("Unknown source shape for random ray transport.");
×
413
  }
414
}
415

416
// This function forms the inner loop of the random ray transport process.
417
// It is responsible for several tasks. Based on the incoming angular flux
418
// of the ray and the source term in the region, the outgoing angular flux
419
// is computed. The delta psi between the incoming and outgoing fluxes is
420
// contributed to the estimate of the total scalar flux in the source region.
421
// Additionally, the contribution of the ray path to the stochastically
422
// estimated volume is also kept track of. All tasks involving writing
423
// to the data for the source region are done with a lock over the entire
424
// source region.  Locks are used instead of atomics as all energy groups
425
// must be written, such that locking once is typically much more efficient
426
// than use of many atomic operations corresponding to each energy group
427
// individually (at least on CPU). Several other bookkeeping tasks are also
428
// performed when inside the lock.
429
void RandomRay::attenuate_flux_flat_source(
65,428,595✔
430
  SourceRegionHandle& srh, double distance, bool is_active, Position r)
431
{
432
  // The number of geometric intersections is counted for reporting purposes
433
  n_event()++;
65,428,595✔
434

435
  // Get material
436
  int material = srh.material();
65,428,595✔
437
  int temp = srh.temperature_idx();
65,428,595✔
438

439
  // MOC incoming flux attenuation + source contribution/attenuation equation
440
  for (int g = 0; g < negroups_; g++) {
242,264,050✔
441
    float sigma_t =
442
      domain_->sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] *
176,835,455✔
443
      srh.density_mult();
176,835,455✔
444
    float tau = sigma_t * distance;
176,835,455✔
445
    float exponential = cjosey_exponential(tau); // exponential = 1 - exp(-tau)
176,835,455✔
446
    float new_delta_psi = (angular_flux_[g] - srh.source(g)) * exponential;
176,835,455✔
447
    delta_psi_[g] = new_delta_psi;
176,835,455✔
448
    angular_flux_[g] -= new_delta_psi;
176,835,455✔
449
  }
450

451
  // If ray is in the active phase (not in dead zone), make contributions to
452
  // source region bookkeeping
453

454
  // Aquire lock for source region
455
  srh.lock();
65,428,595✔
456

457
  if (is_active) {
65,428,595✔
458
    // Accumulate delta psi into new estimate of source region flux for
459
    // this iteration
460
    for (int g = 0; g < negroups_; g++) {
195,527,756✔
461
      srh.scalar_flux_new(g) += delta_psi_[g];
141,843,472✔
462
    }
463

464
    // Accomulate volume (ray distance) into this iteration's estimate
465
    // of the source region's volume
466
    srh.volume() += distance;
53,684,284✔
467

468
    srh.n_hits() += 1;
53,684,284✔
469
  }
470

471
  // Tally valid position inside the source region (e.g., midpoint of
472
  // the ray) if not done already
473
  if (!srh.position_recorded()) {
65,428,595✔
474
    Position midpoint = r + u() * (distance / 2.0);
139,442✔
475
    srh.position() = midpoint;
139,442✔
476
    srh.position_recorded() = 1;
139,442✔
477
  }
478

479
  // Release lock
480
  srh.unlock();
65,428,595✔
481
}
65,428,595✔
482

483
// Alternative flux attenuation function for true void regions.
484
void RandomRay::attenuate_flux_flat_source_void(
754,787✔
485
  SourceRegionHandle& srh, double distance, bool is_active, Position r)
486
{
487
  // The number of geometric intersections is counted for reporting purposes
488
  n_event()++;
754,787✔
489

490
  int material = srh.material();
754,787✔
491

492
  // If ray is in the active phase (not in dead zone), make contributions to
493
  // source region bookkeeping
494
  if (is_active) {
754,787✔
495

496
    // Aquire lock for source region
497
    srh.lock();
627,777✔
498

499
    // Accumulate delta psi into new estimate of source region flux for
500
    // this iteration
501
    for (int g = 0; g < negroups_; g++) {
1,255,554✔
502
      srh.scalar_flux_new(g) += angular_flux_[g] * distance;
627,777✔
503
    }
504

505
    // Accomulate volume (ray distance) into this iteration's estimate
506
    // of the source region's volume
507
    srh.volume() += distance;
627,777✔
508
    srh.volume_sq() += distance * distance;
627,777✔
509
    srh.n_hits() += 1;
627,777✔
510

511
    // Tally valid position inside the source region (e.g., midpoint of
512
    // the ray) if not done already
513
    if (!srh.position_recorded()) {
627,777✔
514
      Position midpoint = r + u() * (distance / 2.0);
1,008✔
515
      srh.position() = midpoint;
1,008✔
516
      srh.position_recorded() = 1;
1,008✔
517
    }
518

519
    // Release lock
520
    srh.unlock();
627,777✔
521
  }
522

523
  // Add source to incoming angular flux, assuming void region
524
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
754,787!
525
    for (int g = 0; g < negroups_; g++) {
1,509,574✔
526
      angular_flux_[g] += srh.external_source(g) * distance;
754,787✔
527
    }
528
  }
529
}
754,787✔
530

531
void RandomRay::attenuate_flux_linear_source(
60,660,438✔
532
  SourceRegionHandle& srh, double distance, bool is_active, Position r)
533
{
534
  // The number of geometric intersections is counted for reporting purposes
535
  n_event()++;
60,660,438✔
536

537
  int material = srh.material();
60,660,438✔
538
  int temp = srh.temperature_idx();
60,660,438✔
539

540
  Position& centroid = srh.centroid();
60,660,438✔
541
  Position midpoint = r + u() * (distance / 2.0);
60,660,438✔
542

543
  // Determine the local position of the midpoint and the ray origin
544
  // relative to the source region's centroid
545
  Position rm_local;
60,660,438✔
546
  Position r0_local;
60,660,438✔
547

548
  // In the first few iterations of the simulation, the source region
549
  // may not yet have had any ray crossings, in which case there will
550
  // be no estimate of its centroid. We detect this by checking if it has
551
  // any accumulated volume. If its volume is zero, just use the midpoint
552
  // of the ray as the region's centroid.
553
  if (srh.volume_t()) {
60,660,438✔
554
    rm_local = midpoint - centroid;
58,268,099✔
555
    r0_local = r - centroid;
58,268,099✔
556
  } else {
557
    rm_local = {0.0, 0.0, 0.0};
2,392,339✔
558
    r0_local = -u() * 0.5 * distance;
2,392,339✔
559
  }
560
  double distance_2 = distance * distance;
60,660,438✔
561

562
  // Linear Source MOC incoming flux attenuation + source
563
  // contribution/attenuation equation
564
  for (int g = 0; g < negroups_; g++) {
280,204,620✔
565

566
    // Compute tau, the optical thickness of the ray segment
567
    float sigma_t =
568
      domain_->sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] *
219,544,182✔
569
      srh.density_mult();
219,544,182✔
570
    float tau = sigma_t * distance;
219,544,182✔
571

572
    // If tau is very small, set it to zero to avoid numerical issues.
573
    // The following computations will still work with tau = 0.
574
    if (tau < 1.0e-8f) {
219,544,182✔
575
      tau = 0.0f;
534✔
576
    }
577

578
    // Compute linear source terms, spatial and directional (dir),
579
    // calculated from the source gradients dot product with local centroid
580
    // and direction, respectively.
581
    float spatial_source =
582
      srh.source(g) + rm_local.dot(srh.source_gradients(g));
219,544,182✔
583
    float dir_source = u().dot(srh.source_gradients(g));
219,544,182✔
584

585
    float gn = exponentialG(tau);
219,544,182✔
586
    float f1 = 1.0f - tau * gn;
219,544,182✔
587
    float f2 = (2.0f * gn - f1) * distance_2;
219,544,182✔
588
    float new_delta_psi = (angular_flux_[g] - spatial_source) * f1 * distance -
219,544,182✔
589
                          0.5 * dir_source * f2;
219,544,182✔
590

591
    float h1 = f1 - gn;
219,544,182✔
592
    float g1 = 0.5f - h1;
219,544,182✔
593
    float g2 = exponentialG2(tau);
219,544,182✔
594
    g1 = g1 * spatial_source;
219,544,182✔
595
    g2 = g2 * dir_source * distance * 0.5f;
219,544,182✔
596
    h1 = h1 * angular_flux_[g];
219,544,182✔
597
    h1 = (g1 + g2 + h1) * distance_2;
219,544,182✔
598
    spatial_source = spatial_source * distance + new_delta_psi;
219,544,182✔
599

600
    // Store contributions for this group into arrays, so that they can
601
    // be accumulated into the source region's estimates inside of the locked
602
    // region.
603
    delta_psi_[g] = new_delta_psi;
219,544,182✔
604
    delta_moments_[g] = r0_local * spatial_source + u() * h1;
219,544,182✔
605

606
    // Update the angular flux for this group
607
    angular_flux_[g] -= new_delta_psi * sigma_t;
219,544,182✔
608

609
    // If 2D mode is enabled, the z-component of the flux moments is forced
610
    // to zero
611
    if (source_shape_ == RandomRaySourceShape::LINEAR_XY) {
219,544,182✔
612
      delta_moments_[g].z = 0.0;
42,018,913✔
613
    }
614
  }
615

616
  // Compute an estimate of the spatial moments matrix for the source
617
  // region based on parameters from this ray's crossing
618
  MomentMatrix moment_matrix_estimate;
619
  moment_matrix_estimate.compute_spatial_moments_matrix(
60,660,438✔
620
    rm_local, u(), distance);
60,660,438✔
621

622
  // Aquire lock for source region
623
  srh.lock();
60,660,438✔
624

625
  // If ray is in the active phase (not in dead zone), make contributions to
626
  // source region bookkeeping
627

628
  if (is_active) {
60,660,438✔
629
    // Accumulate deltas into the new estimate of source region flux for this
630
    // iteration
631
    for (int g = 0; g < negroups_; g++) {
227,107,286✔
632
      srh.scalar_flux_new(g) += delta_psi_[g];
177,384,296✔
633
      srh.flux_moments_new(g) += delta_moments_[g];
177,384,296✔
634
    }
635

636
    // Accumulate the volume (ray segment distance), centroid, and spatial
637
    // momement estimates into the running totals for the iteration for this
638
    // source region. The centroid and spatial momements estimates are scaled
639
    // by the ray segment length as part of length averaging of the estimates.
640
    srh.volume() += distance;
49,722,990✔
641
    srh.centroid_iteration() += midpoint * distance;
49,722,990✔
642
    moment_matrix_estimate *= distance;
49,722,990✔
643
    srh.mom_matrix() += moment_matrix_estimate;
49,722,990✔
644

645
    srh.n_hits() += 1;
49,722,990✔
646
  }
647

648
  // Tally valid position inside the source region (e.g., midpoint of
649
  // the ray) if not done already
650
  if (!srh.position_recorded()) {
60,660,438✔
651
    srh.position() = midpoint;
73,714✔
652
    srh.position_recorded() = 1;
73,714✔
653
  }
654

655
  // Release lock
656
  srh.unlock();
60,660,438✔
657
}
60,660,438✔
658

659
// If traveling through a void region, the source term is either zero
660
// or an external source. As all external sources are currently assumed
661
// to be flat, we don't really need this function and could instead just call
662
// the "attenuate_flux_flat_source_void" function and get the same numerical and
663
// tally results. However, computation of the flux moments in void regions is
664
// nonetheless useful as this information is still used by the plotter when
665
// estimating the flux at specific pixel coordinates. Thus, plots will look
666
// nicer/more accurate if we record flux moments, so this function is useful.
667
void RandomRay::attenuate_flux_linear_source_void(
753,385✔
668
  SourceRegionHandle& srh, double distance, bool is_active, Position r)
669
{
670
  // The number of geometric intersections is counted for reporting purposes
671
  n_event()++;
753,385✔
672

673
  Position& centroid = srh.centroid();
753,385✔
674
  Position midpoint = r + u() * (distance / 2.0);
753,385✔
675

676
  // Determine the local position of the midpoint and the ray origin
677
  // relative to the source region's centroid
678
  Position rm_local;
753,385✔
679
  Position r0_local;
753,385✔
680

681
  // In the first few iterations of the simulation, the source region
682
  // may not yet have had any ray crossings, in which case there will
683
  // be no estimate of its centroid. We detect this by checking if it has
684
  // any accumulated volume. If its volume is zero, just use the midpoint
685
  // of the ray as the region's centroid.
686
  if (srh.volume_t()) {
753,385✔
687
    rm_local = midpoint - centroid;
734,926✔
688
    r0_local = r - centroid;
734,926✔
689
  } else {
690
    rm_local = {0.0, 0.0, 0.0};
18,459✔
691
    r0_local = -u() * 0.5 * distance;
18,459✔
692
  }
693
  double distance_2 = distance * distance;
753,385✔
694

695
  // Compared to linear flux attenuation through solid regions,
696
  // transport through a void region is greatly simplified. Here we
697
  // compute the updated flux moments.
698
  for (int g = 0; g < negroups_; g++) {
1,506,770✔
699
    float spatial_source = 0.f;
753,385✔
700
    if (settings::run_mode == RunMode::FIXED_SOURCE) {
753,385!
701
      spatial_source = srh.external_source(g);
753,385✔
702
    }
703
    float new_delta_psi = (angular_flux_[g] - spatial_source) * distance;
753,385✔
704
    float h1 = 0.5f;
753,385✔
705
    h1 = h1 * angular_flux_[g];
753,385✔
706
    h1 = h1 * distance_2;
753,385✔
707
    spatial_source = spatial_source * distance + new_delta_psi;
753,385✔
708

709
    // Store contributions for this group into arrays, so that they can
710
    // be accumulated into the source region's estimates inside of the locked
711
    // region.
712
    delta_moments_[g] = r0_local * spatial_source + u() * h1;
753,385✔
713

714
    // If 2D mode is enabled, the z-component of the flux moments is forced
715
    // to zero
716
    if (source_shape_ == RandomRaySourceShape::LINEAR_XY) {
753,385!
717
      delta_moments_[g].z = 0.0;
×
718
    }
719
  }
720

721
  // If ray is in the active phase (not in dead zone), make contributions to
722
  // source region bookkeeping
723
  if (is_active) {
753,385✔
724
    // Compute an estimate of the spatial moments matrix for the source
725
    // region based on parameters from this ray's crossing
726
    MomentMatrix moment_matrix_estimate;
727
    moment_matrix_estimate.compute_spatial_moments_matrix(
626,582✔
728
      rm_local, u(), distance);
626,582✔
729

730
    // Aquire lock for source region
731
    srh.lock();
626,582✔
732

733
    // Accumulate delta psi into new estimate of source region flux for
734
    // this iteration, and update flux momements
735
    for (int g = 0; g < negroups_; g++) {
1,253,164✔
736
      srh.scalar_flux_new(g) += angular_flux_[g] * distance;
626,582✔
737
      srh.flux_moments_new(g) += delta_moments_[g];
626,582✔
738
    }
739

740
    // Accumulate the volume (ray segment distance), centroid, and spatial
741
    // momement estimates into the running totals for the iteration for this
742
    // source region. The centroid and spatial momements estimates are scaled by
743
    // the ray segment length as part of length averaging of the estimates.
744
    srh.volume() += distance;
626,582✔
745
    srh.volume_sq() += distance_2;
626,582✔
746
    srh.centroid_iteration() += midpoint * distance;
626,582✔
747
    moment_matrix_estimate *= distance;
626,582✔
748
    srh.mom_matrix() += moment_matrix_estimate;
626,582✔
749

750
    // Tally valid position inside the source region (e.g., midpoint of
751
    // the ray) if not done already
752
    if (!srh.position_recorded()) {
626,582✔
753
      srh.position() = midpoint;
1,000✔
754
      srh.position_recorded() = 1;
1,000✔
755
    }
756

757
    srh.n_hits() += 1;
626,582✔
758

759
    // Release lock
760
    srh.unlock();
626,582✔
761
  }
762

763
  // Add source to incoming angular flux, assuming void region
764
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
753,385!
765
    for (int g = 0; g < negroups_; g++) {
1,506,770✔
766
      angular_flux_[g] += srh.external_source(g) * distance;
753,385✔
767
    }
768
  }
769
}
753,385✔
770

771
void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain)
195,600✔
772
{
773
  domain_ = domain;
195,600✔
774
  ntemperature_ = domain->ntemperature_;
195,600✔
775

776
  // Reset particle event counter
777
  n_event() = 0;
195,600✔
778

779
  is_active_ = (distance_inactive_ <= 0.0);
195,600✔
780

781
  wgt() = 1.0;
195,600✔
782

783
  // set identifier for particle
784
  id() = ray_id;
195,600✔
785

786
  // generate source site using sample method
787
  SourceSite site;
195,600✔
788
  switch (sample_method_) {
195,600!
789
  case RandomRaySampleMethod::PRNG:
191,600✔
790
    site = sample_prng();
191,600✔
791
    break;
191,600✔
792
  case RandomRaySampleMethod::HALTON:
1,000✔
793
    site = sample_halton();
1,000✔
794
    break;
1,000✔
795
  case RandomRaySampleMethod::S2:
3,000✔
796
    site = sample_s2();
3,000✔
797
    break;
3,000✔
UNCOV
798
  default:
×
UNCOV
799
    fatal_error("Unknown sample method for random ray transport.");
×
800
  }
801

802
  site.E = 0.0;
195,600✔
803
  this->from_source(&site);
195,600✔
804

805
  // Locate ray
806
  if (lowest_coord().cell() == C_NONE) {
195,600!
807
    if (!exhaustive_find_cell(*this)) {
195,600!
UNCOV
808
      this->mark_as_lost(
×
UNCOV
809
        "Could not find the cell containing particle " + std::to_string(id()));
×
810
    }
811

812
    // Set birth cell attribute
813
    if (cell_born() == C_NONE)
195,600!
814
      cell_born() = lowest_coord().cell();
195,600✔
815
  }
816

817
  SourceRegionKey sr_key = domain_->lookup_source_region_key(*this);
195,600✔
818
  SourceRegionHandle srh =
819
    domain_->get_subdivided_source_region_handle(sr_key, r(), u());
195,600✔
820

821
  // Initialize ray's starting angular flux to starting location's isotropic
822
  // source
823
  if (!srh.is_numerical_fp_artifact_) {
195,600!
824
    for (int g = 0; g < negroups_; g++) {
761,200✔
825
      angular_flux_[g] = srh.source(g);
565,600✔
826
    }
827
  }
828
}
195,600✔
829

830
SourceSite RandomRay::sample_prng()
191,600✔
831
{
832
  // set random number seed
833
  int64_t particle_seed =
834
    (simulation::current_batch - 1) * settings::n_particles + id();
191,600✔
835
  init_particle_seeds(particle_seed, seeds());
191,600✔
836
  stream() = STREAM_TRACKING;
191,600✔
837

838
  // Sample from ray source distribution
839
  SourceSite site {ray_source_->sample(current_seed())};
191,600✔
840

841
  return site;
191,600✔
842
}
843

844
SourceSite RandomRay::sample_halton()
1,000✔
845
{
846
  SourceSite site;
1,000✔
847

848
  // Set random number seed
849
  int64_t batch_seed = (simulation::current_batch - 1) * settings::n_particles;
1,000✔
850
  int64_t skip = id();
1,000✔
851
  init_particle_seeds(batch_seed, seeds());
1,000✔
852
  stream() = STREAM_TRACKING;
1,000✔
853

854
  // Calculate next samples in LDS across 5 dimensions
855
  vector<double> samples = rhalton(5, current_seed(), skip = skip);
1,000✔
856

857
  // Get spatial box of ray_source_
858
  SpatialBox* sb = dynamic_cast<SpatialBox*>(
1,000!
859
    dynamic_cast<IndependentSource*>(RandomRay::ray_source_.get())->space());
1,000!
860

861
  // Sample spatial distribution
862
  Position xi {samples[0], samples[1], samples[2]};
1,000✔
863
  // make a small shift in position to avoid geometry floating point issues
864
  Position shift {FP_COINCIDENT, FP_COINCIDENT, FP_COINCIDENT};
1,000✔
865
  site.r = (sb->lower_left() + shift) +
866
           xi * ((sb->upper_right() - shift) - (sb->lower_left() + shift));
1,000✔
867

868
  // Sample Polar cosine and azimuthal angles
869
  double mu = 2.0 * samples[3] - 1.0;
1,000✔
870
  double azi = 2.0 * PI * samples[4];
1,000✔
871
  // Convert to Cartesian coordinates
872
  double c = std::sqrt(1.0 - mu * mu);
1,000✔
873
  site.u.x = mu;
1,000✔
874
  site.u.y = std::cos(azi) * c;
1,000✔
875
  site.u.z = std::sin(azi) * c;
1,000✔
876

877
  return site;
2,000✔
878
}
1,000✔
879

880
SourceSite RandomRay::sample_s2()
3,000✔
881
{
882
  // set random number seed
883
  int64_t particle_seed =
884
    (simulation::current_batch - 1) * settings::n_particles + id();
3,000✔
885
  init_particle_seeds(particle_seed, seeds());
3,000✔
886
  stream() = STREAM_TRACKING;
3,000✔
887

888
  // Get spatial component of the ray_source_
889
  SpatialDistribution* space =
890
    dynamic_cast<IndependentSource*>(RandomRay::ray_source_.get())->space();
3,000!
891

892
  SourceSite site;
3,000✔
893

894
  // Sample spatial distribution
895
  site.r = space->sample(current_seed()).first;
3,000✔
896

897
  // Sample either left or right for S2 (flashlight) transport.
898
  site.u = {prn(current_seed()) < 0.5 ? -1.0 : 1.0, 0.0, 0.0};
3,000✔
899

900
  return site;
3,000✔
901
}
902

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

© 2026 Coveralls, Inc