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

openmc-dev / openmc / 21034277178

15 Jan 2026 02:11PM UTC coverage: 81.871% (-0.2%) from 82.044%
21034277178

Pull #3702

github

web-flow
Merge d0e725296 into 179048b80
Pull Request #3702: Random Ray Kinetic Simulation Mode

17650 of 24558 branches covered (71.87%)

Branch coverage included in aggregate %.

1008 of 1149 new or added lines in 20 files covered. (87.73%)

166 existing lines in 15 files now uncovered.

56377 of 65861 relevant lines covered (85.6%)

47768466.83 hits per line

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

83.86
/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 "openmc/distribution_spatial.h"
14
#include "openmc/random_dist.h"
15
#include "openmc/source.h"
16

17
namespace openmc {
18

19
//==============================================================================
20
// Non-method functions
21
//==============================================================================
22

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

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

UNCOV
45
  float x = -tau;
×
46

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

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

UNCOV
65
  return num / den;
×
66
}
67

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

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

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

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

119
  float x = tau;
1,699,794,288✔
120

121
  float num = d5n;
1,699,794,288✔
122
  num = num * x + d4n;
1,699,794,288✔
123
  num = num * x + d3n;
1,699,794,288✔
124
  num = num * x + d2n;
1,699,794,288✔
125
  num = num * x + d1n;
1,699,794,288✔
126
  num = num * x + d0n;
1,699,794,288✔
127

128
  float den = d6d;
1,699,794,288✔
129
  den = den * x + d5d;
1,699,794,288✔
130
  den = den * x + d4d;
1,699,794,288✔
131
  den = den * x + d3d;
1,699,794,288✔
132
  den = den * x + d2d;
1,699,794,288✔
133
  den = den * x + d1d;
1,699,794,288✔
134
  den = den * x + d0d;
1,699,794,288✔
135

136
  return num / den;
1,699,794,288✔
137
}
138

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

147
  // Coefficients for numerator in rational approximation
148
  constexpr float g1n = -0.08335775885589858f;
1,699,794,288✔
149
  constexpr float g2n = -0.003603942303847604f;
1,699,794,288✔
150
  constexpr float g3n = 0.0037673183263550827f;
1,699,794,288✔
151
  constexpr float g4n = 0.00001124183494990467f;
1,699,794,288✔
152
  constexpr float g5n = 0.00016837426505799449f;
1,699,794,288✔
153

154
  // Coefficients for denominator in rational approximation
155
  constexpr float g1d = 0.7454048371823628f;
1,699,794,288✔
156
  constexpr float g2d = 0.23794300531408347f;
1,699,794,288✔
157
  constexpr float g3d = 0.05367250964303789f;
1,699,794,288✔
158
  constexpr float g4d = 0.006125197988351906f;
1,699,794,288✔
159
  constexpr float g5d = 0.0010102514456857377f;
1,699,794,288✔
160

161
  float x = tau;
1,699,794,288✔
162

163
  float num = g5n;
1,699,794,288✔
164
  num = num * x + g4n;
1,699,794,288✔
165
  num = num * x + g3n;
1,699,794,288✔
166
  num = num * x + g2n;
1,699,794,288✔
167
  num = num * x + g1n;
1,699,794,288✔
168
  num = num * x;
1,699,794,288✔
169

170
  float den = g5d;
1,699,794,288✔
171
  den = den * x + g4d;
1,699,794,288✔
172
  den = den * x + g3d;
1,699,794,288✔
173
  den = den * x + g2d;
1,699,794,288✔
174
  den = den * x + g1d;
1,699,794,288✔
175
  den = den * x + 1.0f;
1,699,794,288✔
176

177
  return num / den;
1,699,794,288✔
178
}
179

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

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

208
  vector<int64_t> perm;
8,000✔
209
  for (int D = 0; D < dim; ++D) {
48,000✔
210
    b = primes[D];
40,000✔
211
    perm.resize(b);
40,000✔
212
    b2r = 1.0 / b;
40,000✔
213
    res = skip;
40,000✔
214
    ans = 0.0;
40,000✔
215

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

225
    halton[D] = ans;
40,000✔
226
  }
227

228
  return halton;
16,000✔
229
}
8,000✔
230

231
//==============================================================================
232
// RandomRay implementation
233
//==============================================================================
234

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

242
double RandomRay::avg_miss_rate_;
243
int64_t RandomRay::n_source_regions_;
244
int64_t RandomRay::n_external_source_regions_;
245
uint64_t RandomRay::total_geometric_intersections_;
246

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

251
RandomRay::RandomRay()
10,925,520✔
252
  : angular_flux_(data::mg.num_energy_groups_),
10,925,520✔
253
    delta_psi_(data::mg.num_energy_groups_),
10,925,520✔
254
    negroups_(data::mg.num_energy_groups_),
10,925,520✔
255
    angular_flux_prime_(data::mg.num_energy_groups_)
32,776,560✔
256

257
{
258
  if (source_shape_ == RandomRaySourceShape::LINEAR ||
10,925,520✔
259
      source_shape_ == RandomRaySourceShape::LINEAR_XY) {
10,295,920✔
260
    delta_moments_.resize(negroups_);
720,400✔
261
  }
262
}
10,925,520✔
263

264
RandomRay::RandomRay(uint64_t ray_id, FlatSourceDomain* domain) : RandomRay()
10,925,520✔
265
{
266
  initialize_ray(ray_id, domain);
10,925,520✔
267
}
10,925,520✔
268

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

286
  return n_event();
10,925,520✔
287
}
288

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

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

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

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

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

329
      double distance_alive = distance - distance_dead;
10,925,520✔
330

331
      // Ensure we haven't travelled past the active phase as well
332
      if (distance_alive > distance_active_) {
10,925,520!
333
        distance_alive = distance_active_;
×
334
        wgt() = 0.0;
×
335
      }
336

337
      attenuate_flux(distance_alive, true, distance_dead);
10,925,520✔
338
      distance_travelled_ = distance_alive;
10,925,520✔
339
    } else {
340
      distance_travelled_ += distance;
1,448,600,866✔
341
      attenuate_flux(distance, false);
1,448,600,866✔
342
    }
343
  }
344

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

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

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

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

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

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

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

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

404
  switch (source_shape_) {
2,147,483,647!
405
  case RandomRaySourceShape::FLAT:
2,147,483,647✔
406
    if (srh.material() == MATERIAL_VOID) {
2,147,483,647✔
407
      attenuate_flux_flat_source_void(srh, distance, is_active, r);
6,103,352✔
408
    } else {
409
      attenuate_flux_flat_source(srh, distance, is_active, r);
2,147,483,647✔
410
    }
411
    break;
2,147,483,647✔
412
  case RandomRaySourceShape::LINEAR:
463,031,000✔
413
  case RandomRaySourceShape::LINEAR_XY:
414
    // TODO: time-dependent linear source regions
415
    if (srh.material() == MATERIAL_VOID) {
463,031,000✔
416
      attenuate_flux_linear_source_void(srh, distance, is_active, r);
6,027,080✔
417
    } else {
418
      attenuate_flux_linear_source(srh, distance, is_active, r);
457,003,920✔
419
    }
420
    break;
463,031,000✔
421
  default:
×
422
    fatal_error("Unknown source shape for random ray transport.");
×
423
  }
424
}
425

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

445
  // Get material
446
  int material = srh.material();
2,147,483,647✔
447

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

467
        // Time Derivative Characteristic Equation
468
        angular_flux_prime_[g] -= new_delta_psi_prime * exponential;
2,147,483,647✔
469
      }
470
    }
UNCOV
471
    delta_psi_[g] = new_delta_psi;
×
UNCOV
472
    angular_flux_[g] -= new_delta_psi;
×
473
  }
474

475
  // If ray is in the active phase (not in dead zone), make contributions to
476
  // source region bookkeeping
477

478
  // Aquire lock for source region
479
  srh.lock();
2,147,483,647✔
480

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

488
    // Accomulate volume (ray distance) into this iteration's estimate
489
    // of the source region's volume
490
    srh.volume() += distance;
2,147,483,647✔
491

492
    srh.n_hits() += 1;
2,147,483,647✔
493
  }
494

495
  // Tally valid position inside the source region (e.g., midpoint of
496
  // the ray) if not done already
497
  if (!srh.position_recorded()) {
2,147,483,647✔
498
    Position midpoint = r + u() * (distance / 2.0);
1,163,079✔
499
    srh.position() = midpoint;
1,163,079✔
500
    srh.position_recorded() = 1;
1,163,079✔
501
  }
502

503
  // Release lock
504
  srh.unlock();
2,147,483,647✔
505
}
2,147,483,647✔
506

507
// Alternative flux attenuation function for true void regions.
508
// TODO: Implement support for time-dependent voids
509
void RandomRay::attenuate_flux_flat_source_void(
6,103,352✔
510
  SourceRegionHandle& srh, double distance, bool is_active, Position r)
511
{
512
  // The number of geometric intersections is counted for reporting purposes
513
  n_event()++;
6,103,352✔
514

515
  int material = srh.material();
6,103,352✔
516

517
  // If ray is in the active phase (not in dead zone), make contributions to
518
  // source region bookkeeping
519
  if (is_active) {
6,103,352✔
520

521
    // Aquire lock for source region
522
    srh.lock();
5,075,980✔
523

524
    // Accumulate delta psi into new estimate of source region flux for
525
    // this iteration
526
    for (int g = 0; g < negroups_; g++) {
10,205,724✔
527
      srh.scalar_flux_new(g) += angular_flux_[g] * distance;
5,129,744✔
528
    }
529

530
    // Accomulate volume (ray distance) into this iteration's estimate
531
    // of the source region's volume
532
    srh.volume() += distance;
5,075,980✔
533
    srh.volume_sq() += distance * distance;
5,075,980✔
534
    srh.n_hits() += 1;
5,075,980✔
535

536
    // Tally valid position inside the source region (e.g., midpoint of
537
    // the ray) if not done already
538
    if (!srh.position_recorded()) {
5,075,980✔
539
      Position midpoint = r + u() * (distance / 2.0);
8,128✔
540
      srh.position() = midpoint;
8,128✔
541
      srh.position_recorded() = 1;
8,128✔
542
    }
543

544
    // Release lock
545
    srh.unlock();
5,075,980✔
546
  }
547

548
  // Add source to incoming angular flux, assuming void region
549
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
6,103,352!
550
    for (int g = 0; g < negroups_; g++) {
12,271,760✔
551
      angular_flux_[g] += srh.external_source(g) * distance;
6,168,408✔
552
    }
553
  }
554
}
6,103,352✔
555

556
void RandomRay::attenuate_flux_linear_source(
457,003,920✔
557
  SourceRegionHandle& srh, double distance, bool is_active, Position r)
558
{
559
  // The number of geometric intersections is counted for reporting purposes
560
  n_event()++;
457,003,920✔
561

562
  int material = srh.material();
457,003,920✔
563

564
  Position& centroid = srh.centroid();
457,003,920✔
565
  Position midpoint = r + u() * (distance / 2.0);
457,003,920✔
566

567
  // Determine the local position of the midpoint and the ray origin
568
  // relative to the source region's centroid
569
  Position rm_local;
457,003,920✔
570
  Position r0_local;
457,003,920✔
571

572
  // In the first few iterations of the simulation, the source region
573
  // may not yet have had any ray crossings, in which case there will
574
  // be no estimate of its centroid. We detect this by checking if it has
575
  // any accumulated volume. If its volume is zero, just use the midpoint
576
  // of the ray as the region's centroid.
577
  if (srh.volume_t()) {
457,003,920✔
578
    rm_local = midpoint - centroid;
440,689,000✔
579
    r0_local = r - centroid;
440,689,000✔
580
  } else {
581
    rm_local = {0.0, 0.0, 0.0};
16,314,920✔
582
    r0_local = -u() * 0.5 * distance;
16,314,920✔
583
  }
584
  double distance_2 = distance * distance;
457,003,920✔
585

586
  // Linear Source MOC incoming flux attenuation + source
587
  // contribution/attenuation equation
588
  for (int g = 0; g < negroups_; g++) {
2,147,483,647✔
589

590
    // Compute tau, the optical thickness of the ray segment
591
    float sigma_t =
592
      domain_->sigma_t_[material * negroups_ + g] * srh.density_mult();
1,699,794,288✔
593
    float tau = sigma_t * distance;
1,699,794,288✔
594

595
    // If tau is very small, set it to zero to avoid numerical issues.
596
    // The following computations will still work with tau = 0.
597
    if (tau < 1.0e-8f) {
1,699,794,288✔
598
      tau = 0.0f;
4,272✔
599
    }
600

601
    // Compute linear source terms, spatial and directional (dir),
602
    // calculated from the source gradients dot product with local centroid
603
    // and direction, respectively.
604
    float spatial_source =
605
      srh.source(g) + rm_local.dot(srh.source_gradients(g));
1,699,794,288✔
606
    float dir_source = u().dot(srh.source_gradients(g));
1,699,794,288✔
607

608
    float gn = exponentialG(tau);
1,699,794,288✔
609
    float f1 = 1.0f - tau * gn;
1,699,794,288✔
610
    float f2 = (2.0f * gn - f1) * distance_2;
1,699,794,288✔
611
    float new_delta_psi = (angular_flux_[g] - spatial_source) * f1 * distance -
1,699,794,288✔
612
                          0.5 * dir_source * f2;
1,699,794,288✔
613

614
    float h1 = f1 - gn;
1,699,794,288✔
615
    float g1 = 0.5f - h1;
1,699,794,288✔
616
    float g2 = exponentialG2(tau);
1,699,794,288✔
617
    g1 = g1 * spatial_source;
1,699,794,288✔
618
    g2 = g2 * dir_source * distance * 0.5f;
1,699,794,288✔
619
    h1 = h1 * angular_flux_[g];
1,699,794,288✔
620
    h1 = (g1 + g2 + h1) * distance_2;
1,699,794,288✔
621
    spatial_source = spatial_source * distance + new_delta_psi;
1,699,794,288✔
622

623
    // Store contributions for this group into arrays, so that they can
624
    // be accumulated into the source region's estimates inside of the locked
625
    // region.
626
    delta_psi_[g] = new_delta_psi;
1,699,794,288✔
627
    delta_moments_[g] = r0_local * spatial_source + u() * h1;
1,699,794,288✔
628

629
    // Update the angular flux for this group
630
    angular_flux_[g] -= new_delta_psi * sigma_t;
1,699,794,288✔
631

632
    // If 2D mode is enabled, the z-component of the flux moments is forced
633
    // to zero
634
    if (source_shape_ == RandomRaySourceShape::LINEAR_XY) {
1,699,794,288✔
635
      delta_moments_[g].z = 0.0;
336,151,304✔
636
    }
637
  }
638

639
  // Compute an estimate of the spatial moments matrix for the source
640
  // region based on parameters from this ray's crossing
641
  MomentMatrix moment_matrix_estimate;
642
  moment_matrix_estimate.compute_spatial_moments_matrix(
457,003,920✔
643
    rm_local, u(), distance);
457,003,920✔
644

645
  // Aquire lock for source region
646
  srh.lock();
457,003,920✔
647

648
  // If ray is in the active phase (not in dead zone), make contributions to
649
  // source region bookkeeping
650

651
  if (is_active) {
457,003,920✔
652
    // Accumulate deltas into the new estimate of source region flux for this
653
    // iteration
654
    for (int g = 0; g < negroups_; g++) {
1,746,224,272✔
655
      srh.scalar_flux_new(g) += delta_psi_[g];
1,371,985,024✔
656
      srh.flux_moments_new(g) += delta_moments_[g];
1,371,985,024✔
657
    }
658

659
    // Accumulate the volume (ray segment distance), centroid, and spatial
660
    // momement estimates into the running totals for the iteration for this
661
    // source region. The centroid and spatial momements estimates are scaled
662
    // by the ray segment length as part of length averaging of the estimates.
663
    srh.volume() += distance;
374,239,248✔
664
    srh.centroid_iteration() += midpoint * distance;
374,239,248✔
665
    moment_matrix_estimate *= distance;
374,239,248✔
666
    srh.mom_matrix() += moment_matrix_estimate;
374,239,248✔
667

668
    srh.n_hits() += 1;
374,239,248✔
669
  }
670

671
  // Tally valid position inside the source region (e.g., midpoint of
672
  // the ray) if not done already
673
  if (!srh.position_recorded()) {
457,003,920✔
674
    srh.position() = midpoint;
589,136✔
675
    srh.position_recorded() = 1;
589,136✔
676
  }
677

678
  // Release lock
679
  srh.unlock();
457,003,920✔
680
}
457,003,920✔
681

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

696
  Position& centroid = srh.centroid();
6,027,080✔
697
  Position midpoint = r + u() * (distance / 2.0);
6,027,080✔
698

699
  // Determine the local position of the midpoint and the ray origin
700
  // relative to the source region's centroid
701
  Position rm_local;
6,027,080✔
702
  Position r0_local;
6,027,080✔
703

704
  // In the first few iterations of the simulation, the source region
705
  // may not yet have had any ray crossings, in which case there will
706
  // be no estimate of its centroid. We detect this by checking if it has
707
  // any accumulated volume. If its volume is zero, just use the midpoint
708
  // of the ray as the region's centroid.
709
  if (srh.volume_t()) {
6,027,080✔
710
    rm_local = midpoint - centroid;
5,879,408✔
711
    r0_local = r - centroid;
5,879,408✔
712
  } else {
713
    rm_local = {0.0, 0.0, 0.0};
147,672✔
714
    r0_local = -u() * 0.5 * distance;
147,672✔
715
  }
716
  double distance_2 = distance * distance;
6,027,080✔
717

718
  // Compared to linear flux attenuation through solid regions,
719
  // transport through a void region is greatly simplified. Here we
720
  // compute the updated flux moments.
721
  for (int g = 0; g < negroups_; g++) {
12,054,160✔
722
    float spatial_source = 0.f;
6,027,080✔
723
    if (settings::run_mode == RunMode::FIXED_SOURCE) {
6,027,080!
724
      spatial_source = srh.external_source(g);
6,027,080✔
725
    }
726
    float new_delta_psi = (angular_flux_[g] - spatial_source) * distance;
6,027,080✔
727
    float h1 = 0.5f;
6,027,080✔
728
    h1 = h1 * angular_flux_[g];
6,027,080✔
729
    h1 = h1 * distance_2;
6,027,080✔
730
    spatial_source = spatial_source * distance + new_delta_psi;
6,027,080✔
731

732
    // Store contributions for this group into arrays, so that they can
733
    // be accumulated into the source region's estimates inside of the locked
734
    // region.
735
    delta_moments_[g] = r0_local * spatial_source + u() * h1;
6,027,080✔
736

737
    // If 2D mode is enabled, the z-component of the flux moments is forced
738
    // to zero
739
    if (source_shape_ == RandomRaySourceShape::LINEAR_XY) {
6,027,080!
740
      delta_moments_[g].z = 0.0;
×
741
    }
742
  }
743

744
  // If ray is in the active phase (not in dead zone), make contributions to
745
  // source region bookkeeping
746
  if (is_active) {
6,027,080✔
747
    // Compute an estimate of the spatial moments matrix for the source
748
    // region based on parameters from this ray's crossing
749
    MomentMatrix moment_matrix_estimate;
750
    moment_matrix_estimate.compute_spatial_moments_matrix(
5,012,656✔
751
      rm_local, u(), distance);
5,012,656✔
752

753
    // Aquire lock for source region
754
    srh.lock();
5,012,656✔
755

756
    // Accumulate delta psi into new estimate of source region flux for
757
    // this iteration, and update flux momements
758
    for (int g = 0; g < negroups_; g++) {
10,025,312✔
759
      srh.scalar_flux_new(g) += angular_flux_[g] * distance;
5,012,656✔
760
      srh.flux_moments_new(g) += delta_moments_[g];
5,012,656✔
761
    }
762

763
    // Accumulate the volume (ray segment distance), centroid, and spatial
764
    // momement estimates into the running totals for the iteration for this
765
    // source region. The centroid and spatial momements estimates are scaled by
766
    // the ray segment length as part of length averaging of the estimates.
767
    srh.volume() += distance;
5,012,656✔
768
    srh.volume_sq() += distance_2;
5,012,656✔
769
    srh.centroid_iteration() += midpoint * distance;
5,012,656✔
770
    moment_matrix_estimate *= distance;
5,012,656✔
771
    srh.mom_matrix() += moment_matrix_estimate;
5,012,656✔
772

773
    // Tally valid position inside the source region (e.g., midpoint of
774
    // the ray) if not done already
775
    if (!srh.position_recorded()) {
5,012,656✔
776
      srh.position() = midpoint;
8,000✔
777
      srh.position_recorded() = 1;
8,000✔
778
    }
779

780
    srh.n_hits() += 1;
5,012,656✔
781

782
    // Release lock
783
    srh.unlock();
5,012,656✔
784
  }
785

786
  // Add source to incoming angular flux, assuming void region
787
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
6,027,080!
788
    for (int g = 0; g < negroups_; g++) {
12,054,160✔
789
      angular_flux_[g] += srh.external_source(g) * distance;
6,027,080✔
790
    }
791
  }
792
}
6,027,080✔
793

794
void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain)
10,925,520✔
795
{
796
  domain_ = domain;
10,925,520✔
797

798
  // Reset particle event counter
799
  n_event() = 0;
10,925,520✔
800

801
  is_active_ = (distance_inactive_ <= 0.0);
10,925,520✔
802

803
  wgt() = 1.0;
10,925,520✔
804

805
  // set identifier for particle
806
  id() = ray_id;
10,925,520✔
807

808
  // generate source site using sample method
809
  SourceSite site;
10,925,520✔
810
  switch (sample_method_) {
10,925,520!
811
  case RandomRaySampleMethod::PRNG:
10,917,520✔
812
    site = sample_prng();
10,917,520✔
813
    break;
10,917,520✔
814
  case RandomRaySampleMethod::HALTON:
8,000✔
815
    site = sample_halton();
8,000✔
816
    break;
8,000✔
817
  default:
×
818
    fatal_error("Unknown sample method for random ray transport.");
×
819
  }
820

821
  site.E = 0.0;
10,925,520✔
822
  this->from_source(&site);
10,925,520✔
823

824
  // Locate ray
825
  if (lowest_coord().cell() == C_NONE) {
10,925,520!
826
    if (!exhaustive_find_cell(*this)) {
10,925,520!
827
      this->mark_as_lost(
×
828
        "Could not find the cell containing particle " + std::to_string(id()));
×
829
    }
830

831
    // Set birth cell attribute
832
    if (cell_born() == C_NONE)
10,925,520!
833
      cell_born() = lowest_coord().cell();
10,925,520✔
834
  }
835

836
  SourceRegionKey sr_key = domain_->lookup_source_region_key(*this);
10,925,520✔
837
  SourceRegionHandle srh =
838
    domain_->get_subdivided_source_region_handle(sr_key, r(), u());
10,925,520✔
839

840
  // Initialize ray's starting angular flux to starting location's isotropic
841
  // source
842
  if (!srh.is_numerical_fp_artifact_) {
10,925,520!
843
    for (int g = 0; g < negroups_; g++) {
93,787,760✔
844
      angular_flux_[g] = srh.source(g);
82,862,240✔
845
    }
846
    if (settings::kinetic_simulation && !simulation::is_initial_condition &&
10,925,520✔
847
        RandomRay::time_method_ == RandomRayTimeMethod::PROPAGATION) {
6,800,000✔
848
      for (int g = 0; g < negroups_; g++) {
6,040,000✔
849
        angular_flux_prime_[g] = srh.T1(g);
5,840,000✔
850
      }
851
    }
852
  }
853
}
10,925,520✔
854

855
SourceSite RandomRay::sample_prng()
10,917,520✔
856
{
857
  // set random number seed
858
  int64_t particle_seed =
859
    (simulation::current_batch - 1) * settings::n_particles + id();
10,917,520✔
860
  init_particle_seeds(particle_seed, seeds());
10,917,520✔
861
  stream() = STREAM_TRACKING;
10,917,520✔
862

863
  // Sample from ray source distribution
864
  SourceSite site {ray_source_->sample(current_seed())};
10,917,520✔
865

866
  return site;
10,917,520✔
867
}
868

869
SourceSite RandomRay::sample_halton()
8,000✔
870
{
871
  SourceSite site;
8,000✔
872

873
  // Set random number seed
874
  int64_t batch_seed = (simulation::current_batch - 1) * settings::n_particles;
8,000✔
875
  int64_t skip = id();
8,000✔
876
  init_particle_seeds(batch_seed, seeds());
8,000✔
877
  stream() = STREAM_TRACKING;
8,000✔
878

879
  // Calculate next samples in LDS across 5 dimensions
880
  vector<double> samples = rhalton(5, current_seed(), skip = skip);
8,000✔
881

882
  // Get spatial box of ray_source_
883
  SpatialBox* sb = dynamic_cast<SpatialBox*>(
8,000!
884
    dynamic_cast<IndependentSource*>(RandomRay::ray_source_.get())->space());
8,000!
885

886
  // Sample spatial distribution
887
  Position xi {samples[0], samples[1], samples[2]};
8,000✔
888
  // make a small shift in position to avoid geometry floating point issues
889
  Position shift {FP_COINCIDENT, FP_COINCIDENT, FP_COINCIDENT};
8,000✔
890
  site.r = (sb->lower_left() + shift) +
891
           xi * ((sb->upper_right() - shift) - (sb->lower_left() + shift));
8,000✔
892

893
  // Sample Polar cosine and azimuthal angles
894
  double mu = 2.0 * samples[3] - 1.0;
8,000✔
895
  double azi = 2.0 * PI * samples[4];
8,000✔
896
  // Convert to Cartesian coordinates
897
  double c = std::sqrt(1.0 - mu * mu);
8,000✔
898
  site.u.x = mu;
8,000✔
899
  site.u.y = std::cos(azi) * c;
8,000✔
900
  site.u.z = std::sin(azi) * c;
8,000✔
901

902
  return site;
16,000✔
903
}
8,000✔
904

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