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

openmc-dev / openmc / 24201667475

09 Apr 2026 04:34PM UTC coverage: 81.306% (-0.03%) from 81.337%
24201667475

Pull #3702

github

web-flow
Merge 85e3d3a8b into 1eb368bbd
Pull Request #3702: Random Ray Kinetic Simulation Mode

18160 of 26198 branches covered (69.32%)

Branch coverage included in aggregate %.

935 of 1075 new or added lines in 20 files covered. (86.98%)

74 existing lines in 8 files now uncovered.

58936 of 68624 relevant lines covered (85.88%)

52648244.8 hits per line

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

82.7
/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)
2,147,483,647✔
29
{
UNCOV
30
  constexpr float c1n = -1.0000013559236386308f;
×
UNCOV
31
  constexpr float c2n = 0.23151368626911062025f;
×
UNCOV
32
  constexpr float c3n = -0.061481916409314966140f;
×
UNCOV
33
  constexpr float c4n = 0.0098619906458127653020f;
×
UNCOV
34
  constexpr float c5n = -0.0012629460503540849940f;
×
UNCOV
35
  constexpr float c6n = 0.00010360973791574984608f;
×
UNCOV
36
  constexpr float c7n = -0.000013276571933735820960f;
×
37

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

UNCOV
47
  float x = -tau;
×
48

UNCOV
49
  float den = c7d;
×
UNCOV
50
  den = den * x + c6d;
×
UNCOV
51
  den = den * x + c5d;
×
UNCOV
52
  den = den * x + c4d;
×
UNCOV
53
  den = den * x + c3d;
×
UNCOV
54
  den = den * x + c2d;
×
UNCOV
55
  den = den * x + c1d;
×
UNCOV
56
  den = den * x + c0d;
×
57

UNCOV
58
  float num = c7n;
×
UNCOV
59
  num = num * x + c6n;
×
UNCOV
60
  num = num * x + c5n;
×
UNCOV
61
  num = num * x + c4n;
×
UNCOV
62
  num = num * x + c3n;
×
UNCOV
63
  num = num * x + c2n;
×
UNCOV
64
  num = num * x + c1n;
×
UNCOV
65
  num = num * x;
×
66

UNCOV
67
  return num / den;
×
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)
2,147,483,647✔
101
{
102
  // Numerator coefficients in rational approximation for 1/x - (1 - exp(-x)) /
103
  // x^2
104
  constexpr float d0n = 0.5f;
2,147,483,647✔
105
  constexpr float d1n = 0.176558112351595f;
2,147,483,647✔
106
  constexpr float d2n = 0.04041584305811143f;
2,147,483,647✔
107
  constexpr float d3n = 0.006178333902037397f;
2,147,483,647✔
108
  constexpr float d4n = 0.0006429894635552992f;
2,147,483,647✔
109
  constexpr float d5n = 0.00006064409107557148f;
2,147,483,647✔
110

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

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

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

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

138
  return num / den;
2,147,483,647✔
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)
2,147,483,647✔
147
{
148

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

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

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

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

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

179
  return num / den;
2,147,483,647✔
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)
1,584,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) {
6,831,000✔
189
    // Generate a random index in the range [0, i]
190
    int j = uniform_int_distribution(0, i, seed);
5,247,000✔
191
    std::swap(arr[i], arr[j]);
5,247,000✔
192
  }
193
}
1,584,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)
11,000✔
201
{
202
  if (dim > 10) {
11,000!
203
    fatal_error("Halton sampling dimension too large");
×
204
  }
205
  int64_t b, res, dig;
11,000✔
206
  double b2r, ans;
11,000✔
207
  const std::array<int64_t, 10> primes = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
11,000✔
208
  vector<double> halton(dim, 0.0);
11,000✔
209

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

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

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

230
  return halton;
11,000✔
231
}
11,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
double RandomRay::avg_miss_rate_;
245
int64_t RandomRay::n_source_regions_;
246
int64_t RandomRay::n_external_source_regions_;
247
uint64_t RandomRay::total_geometric_intersections_;
248

249
// Kinetic simulation variables
250
int RandomRay::bd_order_ {3}; // order 3 BD balances accuracy with speed nicely
251
RandomRayTimeMethod RandomRay::time_method_ {RandomRayTimeMethod::ISOTROPIC};
252

253
RandomRay::RandomRay()
15,242,320✔
254
  : angular_flux_(data::mg.num_energy_groups_),
30,484,640✔
255
    delta_psi_(data::mg.num_energy_groups_),
30,484,640✔
256
    negroups_(data::mg.num_energy_groups_),
15,242,320✔
257
    angular_flux_prime_(data::mg.num_energy_groups_)
15,242,320✔
258

259
{
260
  if (source_shape_ == RandomRaySourceShape::LINEAR ||
15,242,320✔
261
      source_shape_ == RandomRaySourceShape::LINEAR_XY) {
262
    delta_moments_.resize(negroups_);
1,056,550✔
263
  }
264
}
15,242,320✔
265

266
RandomRay::RandomRay(uint64_t ray_id, FlatSourceDomain* domain) : RandomRay()
15,242,320✔
267
{
268
  initialize_ray(ray_id, domain);
15,242,320✔
269
}
15,242,320✔
270

271
// Transports ray until termination criteria are met
272
uint64_t RandomRay::transport_history_based_single_ray()
15,242,320✔
273
{
274
  using namespace openmc;
15,242,320✔
275
  while (alive()) {
2,147,483,647!
276
    event_advance_ray();
2,147,483,647✔
277
    if (!alive())
2,147,483,647✔
278
      break;
279
    event_cross_surface();
2,147,483,647✔
280
    // If ray has too many events, display warning and kill it
281
    if (n_event() >= settings::max_particle_events) {
2,147,483,647!
282
      warning("Ray " + std::to_string(id()) +
×
283
              " underwent maximum number of events, terminating ray.");
284
      wgt() = 0.0;
×
285
    }
286
  }
287

288
  return n_event();
15,242,320✔
289
}
290

291
// Transports ray across a single source region
292
void RandomRay::event_advance_ray()
2,147,483,647✔
293
{
294
  // If geometry debug mode is on, check for cell overlaps
295
  if (settings::check_overlaps)
2,147,483,647!
296
    check_cell_overlap(*this);
×
297

298
  // Find the distance to the nearest boundary
299
  boundary() = distance_to_boundary(*this);
2,147,483,647✔
300
  double distance = boundary().distance();
2,147,483,647!
301

302
  if (distance < 0.0) {
2,147,483,647!
303
    mark_as_lost("Negative transport distance detected for particle " +
×
304
                 std::to_string(id()));
×
305
    return;
×
306
  }
307

308
  if (is_active_) {
2,147,483,647✔
309
    // If the ray is in the active length, need to check if it has
310
    // reached its maximum termination distance. If so, reduce
311
    // the ray traced length so that the ray does not overrun the
312
    // maximum numerical length (so as to avoid numerical bias).
313
    if (distance_travelled_ + distance >= distance_active_) {
2,147,483,647✔
314
      distance = distance_active_ - distance_travelled_;
15,242,320✔
315
      wgt() = 0.0;
15,242,320✔
316
    }
317

318
    distance_travelled_ += distance;
2,147,483,647✔
319
    attenuate_flux(distance, true);
2,147,483,647✔
320
  } else {
321
    // If the ray is still in the dead zone, need to check if it
322
    // has entered the active phase. If so, split into two segments (one
323
    // representing the final part of the dead zone, the other representing the
324
    // first part of the active length) and attenuate each. Otherwise, if the
325
    // full length of the segment is within the dead zone, attenuate as normal.
326
    if (distance_travelled_ + distance >= distance_inactive_) {
2,029,290,050✔
327
      is_active_ = true;
15,242,320✔
328
      double distance_dead = distance_inactive_ - distance_travelled_;
15,242,320✔
329
      attenuate_flux(distance_dead, false);
15,242,320✔
330

331
      double distance_alive = distance - distance_dead;
15,242,320✔
332

333
      // Ensure we haven't travelled past the active phase as well
334
      if (distance_alive > distance_active_) {
15,242,320!
335
        distance_alive = distance_active_;
×
336
        wgt() = 0.0;
×
337
      }
338

339
      attenuate_flux(distance_alive, true, distance_dead);
15,242,320✔
340
      distance_travelled_ = distance_alive;
15,242,320✔
341
    } else {
342
      distance_travelled_ += distance;
2,014,047,730✔
343
      attenuate_flux(distance, false);
2,014,047,730✔
344
    }
345
  }
346

347
  // Advance particle
348
  for (int j = 0; j < n_coord(); ++j) {
2,147,483,647✔
349
    coord(j).r() += distance * coord(j).u();
2,147,483,647✔
350
  }
351
}
352

353
void RandomRay::attenuate_flux(double distance, bool is_active, double offset)
2,147,483,647✔
354
{
355
  // Lookup base source region index
356
  int64_t sr = domain_->lookup_base_source_region_idx(*this);
2,147,483,647✔
357

358
  // Perform ray tracing across mesh
359
  // Determine the mesh index for the base source region, if any
360
  int mesh_idx = domain_->lookup_mesh_idx(sr);
2,147,483,647✔
361

362
  if (mesh_idx == C_NONE) {
2,147,483,647✔
363
    // If there's no mesh being applied to this cell, then
364
    // we just attenuate the flux as normal, and set
365
    // the mesh bin to 0
366
    attenuate_flux_inner(distance, is_active, sr, 0, r());
2,147,483,647✔
367
  } else {
368
    // If there is a mesh being applied to this cell, then
369
    // we loop over all the bin crossings and attenuate
370
    // separately.
371
    Mesh* mesh = model::meshes[mesh_idx].get();
2,147,483,647✔
372

373
    // We adjust the start and end positions of the ray slightly
374
    // to accomodate for floating point precision issues that tend
375
    // to occur at mesh boundaries that overlap with geometry lattice
376
    // boundaries.
377
    Position start = r() + (offset + TINY_BIT) * u();
2,147,483,647✔
378
    Position end = start + (distance - 2.0 * TINY_BIT) * u();
2,147,483,647✔
379
    double reduced_distance = (end - start).norm();
2,147,483,647✔
380

381
    // Ray trace through the mesh and record bins and lengths
382
    mesh_bins_.resize(0);
2,147,483,647✔
383
    mesh_fractional_lengths_.resize(0);
2,147,483,647✔
384
    mesh->bins_crossed(start, end, u(), mesh_bins_, mesh_fractional_lengths_);
2,147,483,647✔
385

386
    // Loop over all mesh bins and attenuate flux
387
    for (int b = 0; b < mesh_bins_.size(); b++) {
2,147,483,647✔
388
      double physical_length = reduced_distance * mesh_fractional_lengths_[b];
2,147,483,647✔
389
      attenuate_flux_inner(
2,147,483,647✔
390
        physical_length, is_active, sr, mesh_bins_[b], start);
2,147,483,647✔
391
      start += physical_length * u();
2,147,483,647✔
392
    }
393
  }
394
}
2,147,483,647✔
395

396
void RandomRay::attenuate_flux_inner(
2,147,483,647✔
397
  double distance, bool is_active, int64_t sr, int mesh_bin, Position r)
398
{
399
  SourceRegionKey sr_key {sr, mesh_bin};
2,147,483,647✔
400
  SourceRegionHandle srh;
2,147,483,647✔
401
  srh = domain_->get_subdivided_source_region_handle(sr_key, r, u());
2,147,483,647✔
402
  if (srh.is_numerical_fp_artifact_) {
2,147,483,647✔
403
    return;
858✔
404
  }
405

406
  switch (source_shape_) {
2,147,483,647!
407
  case RandomRaySourceShape::FLAT:
2,147,483,647✔
408
    if (srh.material() == MATERIAL_VOID) {
2,147,483,647✔
409
      attenuate_flux_flat_source_void(srh, distance, is_active, r);
8,367,713✔
410
    } else {
411
      attenuate_flux_flat_source(srh, distance, is_active, r);
2,147,483,647✔
412
    }
413
    break;
414
  case RandomRaySourceShape::LINEAR:
675,552,053✔
415
  case RandomRaySourceShape::LINEAR_XY:
675,552,053✔
416
    // TODO: time-dependent linear source regions
417
    if (srh.material() == MATERIAL_VOID) {
675,552,053✔
418
      attenuate_flux_linear_source_void(srh, distance, is_active, r);
8,287,235✔
419
    } else {
420
      attenuate_flux_linear_source(srh, distance, is_active, r);
667,264,818✔
421
    }
422
    break;
423
  default:
×
424
    fatal_error("Unknown source shape for random ray transport.");
×
425
  }
426
}
427

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

447
  // Get material
448
  int material = srh.material();
2,147,483,647✔
449
  int temp = srh.temperature_idx();
2,147,483,647✔
450

451
  // MOC incoming flux attenuation + source contribution/attenuation equation
UNCOV
452
  for (int g = 0; g < negroups_; g++) {
×
UNCOV
453
    float sigma_t =
×
UNCOV
454
      domain_->sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] *
×
UNCOV
455
      srh.density_mult();
×
UNCOV
456
    float tau = sigma_t * distance;
×
UNCOV
457
    float exponential = cjosey_exponential(tau); // exponential = 1 - exp(-tau)
×
UNCOV
458
    float new_delta_psi = (angular_flux_[g] - srh.source(g)) * exponential;
×
NEW
459
    if (settings::kinetic_simulation && !simulation::is_initial_condition) {
×
NEW
460
      if (RandomRay::time_method_ == RandomRayTimeMethod::ISOTROPIC) {
×
NEW
461
        new_delta_psi += srh.phi_prime(g) * exponential;
×
462
      } else if (RandomRay::time_method_ == RandomRayTimeMethod::PROPAGATION) {
2,147,483,647!
463
        // Source Derivative Propogation terms for Characteristic Equation
464
        float inverse_vbar = domain_->inverse_vbar_[material * negroups_ + g];
2,147,483,647✔
465
        float T1 = srh.T1(g);
2,147,483,647✔
466
        float new_delta_psi_prime = (angular_flux_prime_[g] - T1);
2,147,483,647✔
467
        new_delta_psi += T1 * inverse_vbar * exponential / sigma_t;
2,147,483,647✔
468
        new_delta_psi +=
2,147,483,647✔
469
          distance * inverse_vbar * new_delta_psi_prime * (1 - exponential);
2,147,483,647✔
470

471
        // Time Derivative Characteristic Equation
472
        angular_flux_prime_[g] -= new_delta_psi_prime * exponential;
2,147,483,647✔
473
      }
474
    }
UNCOV
475
    delta_psi_[g] = new_delta_psi;
×
UNCOV
476
    angular_flux_[g] -= new_delta_psi;
×
477
  }
478

479
  // If ray is in the active phase (not in dead zone), make contributions to
480
  // source region bookkeeping
481

482
  // Aquire lock for source region
483
  srh.lock();
2,147,483,647✔
484

485
  if (is_active) {
2,147,483,647✔
486
    // Accumulate delta psi into new estimate of source region flux for
487
    // this iteration
UNCOV
488
    for (int g = 0; g < negroups_; g++) {
×
UNCOV
489
      srh.scalar_flux_new(g) += delta_psi_[g];
×
490
    }
491

492
    // Accomulate volume (ray distance) into this iteration's estimate
493
    // of the source region's volume
494
    srh.volume() += distance;
2,147,483,647✔
495

496
    srh.n_hits() += 1;
2,147,483,647✔
497
  }
498

499
  // Tally valid position inside the source region (e.g., midpoint of
500
  // the ray) if not done already
501
  if (!srh.position_recorded()) {
2,147,483,647✔
502
    Position midpoint = r + u() * (distance / 2.0);
1,602,547✔
503
    srh.position() = midpoint;
1,602,547✔
504
    srh.position_recorded() = 1;
1,602,547✔
505
  }
506

507
  // Release lock
508
  srh.unlock();
2,147,483,647✔
509
}
2,147,483,647✔
510

511
// Alternative flux attenuation function for true void regions.
512
// TODO: Implement support for time-dependent voids
513
void RandomRay::attenuate_flux_flat_source_void(
8,367,713✔
514
  SourceRegionHandle& srh, double distance, bool is_active, Position r)
515
{
516
  // The number of geometric intersections is counted for reporting purposes
517
  n_event()++;
8,367,713✔
518

519
  int material = srh.material();
8,367,713✔
520

521
  // If ray is in the active phase (not in dead zone), make contributions to
522
  // source region bookkeeping
523
  if (is_active) {
8,367,713✔
524

525
    // Aquire lock for source region
526
    srh.lock();
3,820,426✔
527

528
    // Accumulate delta psi into new estimate of source region flux for
529
    // this iteration
530
    for (int g = 0; g < negroups_; g++) {
13,972,386✔
531
      srh.scalar_flux_new(g) += angular_flux_[g] * distance;
7,013,075✔
532
    }
533

534
    // Accomulate volume (ray distance) into this iteration's estimate
535
    // of the source region's volume
536
    srh.volume() += distance;
6,959,311✔
537
    srh.volume_sq() += distance * distance;
6,959,311✔
538
    srh.n_hits() += 1;
6,959,311✔
539

540
    // Tally valid position inside the source region (e.g., midpoint of
541
    // the ray) if not done already
542
    if (!srh.position_recorded()) {
6,959,311✔
543
      Position midpoint = r + u() * (distance / 2.0);
11,152✔
544
      srh.position() = midpoint;
11,152✔
545
      srh.position_recorded() = 1;
11,152✔
546
    }
547

548
    // Release lock
549
    srh.unlock();
3,820,426✔
550
  }
551

552
  // Add source to incoming angular flux, assuming void region
553
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
8,367,713!
554
    for (int g = 0; g < negroups_; g++) {
16,800,482✔
555
      angular_flux_[g] += srh.external_source(g) * distance;
8,432,769✔
556
    }
557
  }
558
}
8,367,713✔
559

560
void RandomRay::attenuate_flux_linear_source(
667,264,818✔
561
  SourceRegionHandle& srh, double distance, bool is_active, Position r)
562
{
563
  // The number of geometric intersections is counted for reporting purposes
564
  n_event()++;
667,264,818✔
565

566
  int material = srh.material();
667,264,818✔
567
  int temp = srh.temperature_idx();
667,264,818✔
568

569
  Position& centroid = srh.centroid();
667,264,818✔
570
  Position midpoint = r + u() * (distance / 2.0);
667,264,818✔
571

572
  // Determine the local position of the midpoint and the ray origin
573
  // relative to the source region's centroid
574
  Position rm_local;
667,264,818✔
575
  Position r0_local;
667,264,818✔
576

577
  // In the first few iterations of the simulation, the source region
578
  // may not yet have had any ray crossings, in which case there will
579
  // be no estimate of its centroid. We detect this by checking if it has
580
  // any accumulated volume. If its volume is zero, just use the midpoint
581
  // of the ray as the region's centroid.
582
  if (srh.volume_t()) {
667,264,818✔
583
    rm_local = midpoint - centroid;
640,949,089✔
584
    r0_local = r - centroid;
640,949,089✔
585
  } else {
586
    rm_local = {0.0, 0.0, 0.0};
26,315,729✔
587
    r0_local = -u() * 0.5 * distance;
52,631,458✔
588
  }
589
  double distance_2 = distance * distance;
667,264,818✔
590

591
  // Linear Source MOC incoming flux attenuation + source
592
  // contribution/attenuation equation
593
  for (int g = 0; g < negroups_; g++) {
2,147,483,647✔
594

595
    // Compute tau, the optical thickness of the ray segment
596
    float sigma_t =
2,147,483,647✔
597
      domain_->sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] *
2,147,483,647✔
598
      srh.density_mult();
2,147,483,647✔
599
    float tau = sigma_t * distance;
2,147,483,647✔
600

601
    // If tau is very small, set it to zero to avoid numerical issues.
602
    // The following computations will still work with tau = 0.
603
    if (tau < 1.0e-8f) {
2,147,483,647✔
604
      tau = 0.0f;
5,874✔
605
    }
606

607
    // Compute linear source terms, spatial and directional (dir),
608
    // calculated from the source gradients dot product with local centroid
609
    // and direction, respectively.
610
    float spatial_source =
2,147,483,647✔
611
      srh.source(g) + rm_local.dot(srh.source_gradients(g));
2,147,483,647✔
612
    float dir_source = u().dot(srh.source_gradients(g));
2,147,483,647✔
613

614
    float gn = exponentialG(tau);
2,147,483,647✔
615
    float f1 = 1.0f - tau * gn;
2,147,483,647✔
616
    float f2 = (2.0f * gn - f1) * distance_2;
2,147,483,647✔
617
    float new_delta_psi = (angular_flux_[g] - spatial_source) * f1 * distance -
2,147,483,647✔
618
                          0.5 * dir_source * f2;
2,147,483,647✔
619

620
    float h1 = f1 - gn;
2,147,483,647✔
621
    float g1 = 0.5f - h1;
2,147,483,647✔
622
    float g2 = exponentialG2(tau);
2,147,483,647✔
623
    g1 = g1 * spatial_source;
2,147,483,647✔
624
    g2 = g2 * dir_source * distance * 0.5f;
2,147,483,647✔
625
    h1 = h1 * angular_flux_[g];
2,147,483,647✔
626
    h1 = (g1 + g2 + h1) * distance_2;
2,147,483,647✔
627
    spatial_source = spatial_source * distance + new_delta_psi;
2,147,483,647✔
628

629
    // Store contributions for this group into arrays, so that they can
630
    // be accumulated into the source region's estimates inside of the locked
631
    // region.
632
    delta_psi_[g] = new_delta_psi;
2,147,483,647✔
633
    delta_moments_[g] = r0_local * spatial_source + u() * h1;
2,147,483,647✔
634

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

638
    // If 2D mode is enabled, the z-component of the flux moments is forced
639
    // to zero
640
    if (source_shape_ == RandomRaySourceShape::LINEAR_XY) {
2,147,483,647✔
641
      delta_moments_[g].z = 0.0;
462,208,043✔
642
    }
643
  }
644

645
  // Compute an estimate of the spatial moments matrix for the source
646
  // region based on parameters from this ray's crossing
647
  MomentMatrix moment_matrix_estimate;
667,264,818✔
648
  moment_matrix_estimate.compute_spatial_moments_matrix(
667,264,818✔
649
    rm_local, u(), distance);
667,264,818✔
650

651
  // Aquire lock for source region
652
  srh.lock();
667,264,818✔
653

654
  // If ray is in the active phase (not in dead zone), make contributions to
655
  // source region bookkeeping
656

657
  if (is_active) {
667,264,818✔
658
    // Accumulate deltas into the new estimate of source region flux for this
659
    // iteration
660
    for (int g = 0; g < negroups_; g++) {
2,147,483,647✔
661
      srh.scalar_flux_new(g) += delta_psi_[g];
1,951,227,256✔
662
      srh.flux_moments_new(g) += delta_moments_[g];
1,951,227,256✔
663
    }
664

665
    // Accumulate the volume (ray segment distance), centroid, and spatial
666
    // momement estimates into the running totals for the iteration for this
667
    // source region. The centroid and spatial momements estimates are scaled
668
    // by the ray segment length as part of length averaging of the estimates.
669
    srh.volume() += distance;
546,952,890✔
670
    srh.centroid_iteration() += midpoint * distance;
546,952,890✔
671
    moment_matrix_estimate *= distance;
546,952,890✔
672
    srh.mom_matrix() += moment_matrix_estimate;
546,952,890✔
673

674
    srh.n_hits() += 1;
546,952,890✔
675
  }
676

677
  // Tally valid position inside the source region (e.g., midpoint of
678
  // the ray) if not done already
679
  if (!srh.position_recorded()) {
667,264,818✔
680
    srh.position() = midpoint;
810,854✔
681
    srh.position_recorded() = 1;
810,854✔
682
  }
683

684
  // Release lock
685
  srh.unlock();
667,264,818✔
686
}
667,264,818✔
687

688
// If traveling through a void region, the source term is either zero
689
// or an external source. As all external sources are currently assumed
690
// to be flat, we don't really need this function and could instead just call
691
// the "attenuate_flux_flat_source_void" function and get the same numerical and
692
// tally results. However, computation of the flux moments in void regions is
693
// nonetheless useful as this information is still used by the plotter when
694
// estimating the flux at specific pixel coordinates. Thus, plots will look
695
// nicer/more accurate if we record flux moments, so this function is useful.
696
void RandomRay::attenuate_flux_linear_source_void(
8,287,235✔
697
  SourceRegionHandle& srh, double distance, bool is_active, Position r)
698
{
699
  // The number of geometric intersections is counted for reporting purposes
700
  n_event()++;
8,287,235✔
701

702
  Position& centroid = srh.centroid();
8,287,235✔
703
  Position midpoint = r + u() * (distance / 2.0);
8,287,235✔
704

705
  // Determine the local position of the midpoint and the ray origin
706
  // relative to the source region's centroid
707
  Position rm_local;
8,287,235✔
708
  Position r0_local;
8,287,235✔
709

710
  // In the first few iterations of the simulation, the source region
711
  // may not yet have had any ray crossings, in which case there will
712
  // be no estimate of its centroid. We detect this by checking if it has
713
  // any accumulated volume. If its volume is zero, just use the midpoint
714
  // of the ray as the region's centroid.
715
  if (srh.volume_t()) {
8,287,235✔
716
    rm_local = midpoint - centroid;
8,084,186✔
717
    r0_local = r - centroid;
8,084,186✔
718
  } else {
719
    rm_local = {0.0, 0.0, 0.0};
203,049✔
720
    r0_local = -u() * 0.5 * distance;
406,098✔
721
  }
722
  double distance_2 = distance * distance;
8,287,235✔
723

724
  // Compared to linear flux attenuation through solid regions,
725
  // transport through a void region is greatly simplified. Here we
726
  // compute the updated flux moments.
727
  for (int g = 0; g < negroups_; g++) {
16,574,470✔
728
    float spatial_source = 0.f;
8,287,235✔
729
    if (settings::run_mode == RunMode::FIXED_SOURCE) {
8,287,235!
730
      spatial_source = srh.external_source(g);
8,287,235✔
731
    }
732
    float new_delta_psi = (angular_flux_[g] - spatial_source) * distance;
8,287,235✔
733
    float h1 = 0.5f;
8,287,235✔
734
    h1 = h1 * angular_flux_[g];
8,287,235✔
735
    h1 = h1 * distance_2;
8,287,235✔
736
    spatial_source = spatial_source * distance + new_delta_psi;
8,287,235✔
737

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

743
    // If 2D mode is enabled, the z-component of the flux moments is forced
744
    // to zero
745
    if (source_shape_ == RandomRaySourceShape::LINEAR_XY) {
8,287,235!
746
      delta_moments_[g].z = 0.0;
×
747
    }
748
  }
749

750
  // If ray is in the active phase (not in dead zone), make contributions to
751
  // source region bookkeeping
752
  if (is_active) {
8,287,235✔
753
    // Compute an estimate of the spatial moments matrix for the source
754
    // region based on parameters from this ray's crossing
755
    MomentMatrix moment_matrix_estimate;
6,892,402✔
756
    moment_matrix_estimate.compute_spatial_moments_matrix(
6,892,402✔
757
      rm_local, u(), distance);
6,892,402✔
758

759
    // Aquire lock for source region
760
    srh.lock();
3,759,492✔
761

762
    // Accumulate delta psi into new estimate of source region flux for
763
    // this iteration, and update flux momements
764
    for (int g = 0; g < negroups_; g++) {
13,784,804✔
765
      srh.scalar_flux_new(g) += angular_flux_[g] * distance;
6,892,402✔
766
      srh.flux_moments_new(g) += delta_moments_[g];
6,892,402✔
767
    }
768

769
    // Accumulate the volume (ray segment distance), centroid, and spatial
770
    // momement estimates into the running totals for the iteration for this
771
    // source region. The centroid and spatial momements estimates are scaled by
772
    // the ray segment length as part of length averaging of the estimates.
773
    srh.volume() += distance;
6,892,402✔
774
    srh.volume_sq() += distance_2;
6,892,402✔
775
    srh.centroid_iteration() += midpoint * distance;
6,892,402✔
776
    moment_matrix_estimate *= distance;
6,892,402✔
777
    srh.mom_matrix() += moment_matrix_estimate;
6,892,402✔
778

779
    // Tally valid position inside the source region (e.g., midpoint of
780
    // the ray) if not done already
781
    if (!srh.position_recorded()) {
6,892,402✔
782
      srh.position() = midpoint;
11,000✔
783
      srh.position_recorded() = 1;
11,000✔
784
    }
785

786
    srh.n_hits() += 1;
6,892,402✔
787

788
    // Release lock
789
    srh.unlock();
6,892,402✔
790
  }
791

792
  // Add source to incoming angular flux, assuming void region
793
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
8,287,235!
794
    for (int g = 0; g < negroups_; g++) {
16,574,470✔
795
      angular_flux_[g] += srh.external_source(g) * distance;
8,287,235✔
796
    }
797
  }
798
}
8,287,235✔
799

800
void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain)
15,242,320✔
801
{
802
  domain_ = domain;
15,242,320✔
803
  ntemperature_ = domain->ntemperature_;
15,242,320✔
804

805
  // Reset particle event counter
806
  n_event() = 0;
15,242,320✔
807

808
  is_active_ = (distance_inactive_ <= 0.0);
15,242,320✔
809

810
  wgt() = 1.0;
15,242,320✔
811

812
  // set identifier for particle
813
  id() = ray_id;
15,242,320✔
814

815
  // generate source site using sample method
816
  SourceSite site;
15,242,320!
817
  switch (sample_method_) {
15,242,320!
818
  case RandomRaySampleMethod::PRNG:
15,198,320✔
819
    site = sample_prng();
15,198,320✔
820
    break;
821
  case RandomRaySampleMethod::HALTON:
11,000✔
822
    site = sample_halton();
11,000✔
823
    break;
824
  case RandomRaySampleMethod::S2:
33,000✔
825
    site = sample_s2();
33,000✔
826
    break;
827
  default:
×
828
    fatal_error("Unknown sample method for random ray transport.");
×
829
  }
830

831
  site.E = 0.0;
15,242,320✔
832
  this->from_source(&site);
15,242,320✔
833

834
  // Locate ray
835
  if (lowest_coord().cell() == C_NONE) {
15,242,320!
836
    if (!exhaustive_find_cell(*this)) {
15,242,320!
837
      this->mark_as_lost(
×
838
        "Could not find the cell containing particle " + std::to_string(id()));
×
839
    }
840

841
    // Set birth cell attribute
842
    if (cell_born() == C_NONE)
15,242,320!
843
      cell_born() = lowest_coord().cell();
15,242,320✔
844
  }
845

846
  SourceRegionKey sr_key = domain_->lookup_source_region_key(*this);
15,242,320✔
847
  SourceRegionHandle srh =
15,242,320✔
848
    domain_->get_subdivided_source_region_handle(sr_key, r(), u());
15,242,320✔
849

850
  // Initialize ray's starting angular flux to starting location's isotropic
851
  // source
852
  if (!srh.is_numerical_fp_artifact_) {
15,242,320!
853
    for (int g = 0; g < negroups_; g++) {
130,189,360✔
854
      angular_flux_[g] = srh.source(g);
114,947,040✔
855
    }
856
    if (settings::kinetic_simulation && !simulation::is_initial_condition &&
15,242,320✔
857
        RandomRay::time_method_ == RandomRayTimeMethod::PROPAGATION) {
9,350,000✔
858
      for (int g = 0; g < negroups_; g++) {
8,305,000✔
859
        angular_flux_prime_[g] = srh.T1(g);
8,030,000✔
860
      }
861
    }
862
  }
863
}
15,242,320✔
864

865
SourceSite RandomRay::sample_prng()
15,198,320✔
866
{
867
  // set random number seed
868
  int64_t particle_seed =
15,198,320✔
869
    (simulation::current_batch - 1) * settings::n_particles + id();
15,198,320✔
870
  init_particle_seeds(particle_seed, seeds());
15,198,320✔
871
  stream() = STREAM_TRACKING;
15,198,320✔
872

873
  // Sample from ray source distribution
874
  SourceSite site {ray_source_->sample(current_seed())};
15,198,320✔
875

876
  return site;
15,198,320✔
877
}
878

879
SourceSite RandomRay::sample_halton()
11,000✔
880
{
881
  SourceSite site;
11,000✔
882

883
  // Set random number seed
884
  int64_t batch_seed = (simulation::current_batch - 1) * settings::n_particles;
11,000✔
885
  int64_t skip = id();
11,000✔
886
  init_particle_seeds(batch_seed, seeds());
11,000✔
887
  stream() = STREAM_TRACKING;
11,000✔
888

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

892
  // Get spatial box of ray_source_
893
  SpatialBox* sb = dynamic_cast<SpatialBox*>(
22,000!
894
    dynamic_cast<IndependentSource*>(RandomRay::ray_source_.get())->space());
11,000!
895

896
  // Sample spatial distribution
897
  Position xi {samples[0], samples[1], samples[2]};
11,000✔
898
  // make a small shift in position to avoid geometry floating point issues
899
  Position shift {FP_COINCIDENT, FP_COINCIDENT, FP_COINCIDENT};
11,000✔
900
  site.r = (sb->lower_left() + shift) +
11,000✔
901
           xi * ((sb->upper_right() - shift) - (sb->lower_left() + shift));
11,000✔
902

903
  // Sample Polar cosine and azimuthal angles
904
  double mu = 2.0 * samples[3] - 1.0;
11,000✔
905
  double azi = 2.0 * PI * samples[4];
11,000✔
906
  // Convert to Cartesian coordinates
907
  double c = std::sqrt(1.0 - mu * mu);
11,000✔
908
  site.u.x = mu;
11,000✔
909
  site.u.y = std::cos(azi) * c;
11,000✔
910
  site.u.z = std::sin(azi) * c;
11,000✔
911

912
  return site;
11,000✔
913
}
11,000✔
914

915
SourceSite RandomRay::sample_s2()
33,000✔
916
{
917
  // set random number seed
918
  int64_t particle_seed =
33,000✔
919
    (simulation::current_batch - 1) * settings::n_particles + id();
33,000✔
920
  init_particle_seeds(particle_seed, seeds());
33,000✔
921
  stream() = STREAM_TRACKING;
33,000✔
922

923
  // Get spatial component of the ray_source_
924
  SpatialDistribution* space =
33,000!
925
    dynamic_cast<IndependentSource*>(RandomRay::ray_source_.get())->space();
33,000!
926

927
  SourceSite site;
33,000✔
928

929
  // Sample spatial distribution
930
  site.r = space->sample(current_seed()).first;
33,000✔
931

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

935
  return site;
33,000✔
936
}
937

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