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

openmc-dev / openmc / 13688481331

06 Mar 2025 12:35AM UTC coverage: 85.002% (-0.04%) from 85.042%
13688481331

Pull #3333

github

web-flow
Merge 5654c92ef into ced892912
Pull Request #3333: Random Ray Source Region Mesh Subdivision (Cell-Under-Voxel Geometry)

560 of 646 new or added lines in 11 files covered. (86.69%)

361 existing lines in 16 files now uncovered.

51473 of 60555 relevant lines covered (85.0%)

36077311.57 hits per line

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

96.3
/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)
1,106,997,892✔
27
{
28
  constexpr float c1n = -1.0000013559236386308f;
1,106,997,892✔
29
  constexpr float c2n = 0.23151368626911062025f;
1,106,997,892✔
30
  constexpr float c3n = -0.061481916409314966140f;
1,106,997,892✔
31
  constexpr float c4n = 0.0098619906458127653020f;
1,106,997,892✔
32
  constexpr float c5n = -0.0012629460503540849940f;
1,106,997,892✔
33
  constexpr float c6n = 0.00010360973791574984608f;
1,106,997,892✔
34
  constexpr float c7n = -0.000013276571933735820960f;
1,106,997,892✔
35

36
  constexpr float c0d = 1.0f;
1,106,997,892✔
37
  constexpr float c1d = -0.73151337729389001396f;
1,106,997,892✔
38
  constexpr float c2d = 0.26058381273536471371f;
1,106,997,892✔
39
  constexpr float c3d = -0.059892419041316836940f;
1,106,997,892✔
40
  constexpr float c4d = 0.0099070188241094279067f;
1,106,997,892✔
41
  constexpr float c5d = -0.0012623388962473160860f;
1,106,997,892✔
42
  constexpr float c6d = 0.00010361277635498731388f;
1,106,997,892✔
43
  constexpr float c7d = -0.000013276569500666698498f;
1,106,997,892✔
44

45
  float x = -tau;
1,106,997,892✔
46

47
  float den = c7d;
1,106,997,892✔
48
  den = den * x + c6d;
1,106,997,892✔
49
  den = den * x + c5d;
1,106,997,892✔
50
  den = den * x + c4d;
1,106,997,892✔
51
  den = den * x + c3d;
1,106,997,892✔
52
  den = den * x + c2d;
1,106,997,892✔
53
  den = den * x + c1d;
1,106,997,892✔
54
  den = den * x + c0d;
1,106,997,892✔
55

56
  float num = c7n;
1,106,997,892✔
57
  num = num * x + c6n;
1,106,997,892✔
58
  num = num * x + c5n;
1,106,997,892✔
59
  num = num * x + c4n;
1,106,997,892✔
60
  num = num * x + c3n;
1,106,997,892✔
61
  num = num * x + c2n;
1,106,997,892✔
62
  num = num * x + c1n;
1,106,997,892✔
63
  num = num * x;
1,106,997,892✔
64

65
  return num / den;
1,106,997,892✔
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,185,279,722✔
99
{
100
  // Numerator coefficients in rational approximation for 1/x - (1 - exp(-x)) /
101
  // x^2
102
  constexpr float d0n = 0.5f;
1,185,279,722✔
103
  constexpr float d1n = 0.176558112351595f;
1,185,279,722✔
104
  constexpr float d2n = 0.04041584305811143f;
1,185,279,722✔
105
  constexpr float d3n = 0.006178333902037397f;
1,185,279,722✔
106
  constexpr float d4n = 0.0006429894635552992f;
1,185,279,722✔
107
  constexpr float d5n = 0.00006064409107557148f;
1,185,279,722✔
108

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

119
  float x = tau;
1,185,279,722✔
120

121
  float num = d5n;
1,185,279,722✔
122
  num = num * x + d4n;
1,185,279,722✔
123
  num = num * x + d3n;
1,185,279,722✔
124
  num = num * x + d2n;
1,185,279,722✔
125
  num = num * x + d1n;
1,185,279,722✔
126
  num = num * x + d0n;
1,185,279,722✔
127

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

136
  return num / den;
1,185,279,722✔
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,185,279,722✔
145
{
146

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

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

161
  float x = tau;
1,185,279,722✔
162

163
  float num = g5n;
1,185,279,722✔
164
  num = num * x + g4n;
1,185,279,722✔
165
  num = num * x + g3n;
1,185,279,722✔
166
  num = num * x + g2n;
1,185,279,722✔
167
  num = num * x + g1n;
1,185,279,722✔
168
  num = num * x;
1,185,279,722✔
169

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

177
  return num / den;
1,185,279,722✔
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,584,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) {
6,831,000✔
187
    // Generate a random index in the range [0, i]
188
    int j = uniform_int_distribution(0, i, seed);
5,247,000✔
189
    std::swap(arr[i], arr[j]);
5,247,000✔
190
  }
191
}
1,584,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)
11,000✔
199
{
200
  if (dim > 10) {
11,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};
11,000✔
206
  vector<double> halton(dim, 0.0);
11,000✔
207

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

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

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

228
  return halton;
22,000✔
229
}
11,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
bool RandomRay::mesh_subdivision_enabled_ {false};
241
RandomRaySampleMethod RandomRay::sample_method_ {RandomRaySampleMethod::PRNG};
242

243
RandomRay::RandomRay()
1,772,100✔
244
  : angular_flux_(data::mg.num_energy_groups_),
1,772,100✔
245
    delta_psi_(data::mg.num_energy_groups_),
1,772,100✔
246
    negroups_(data::mg.num_energy_groups_)
5,316,300✔
247
{
248
  if (source_shape_ == RandomRaySourceShape::LINEAR ||
1,772,100✔
249
      source_shape_ == RandomRaySourceShape::LINEAR_XY) {
1,005,400✔
250
    delta_moments_.resize(negroups_);
891,550✔
251
  }
252
}
1,772,100✔
253

254
RandomRay::RandomRay(uint64_t ray_id, FlatSourceDomain* domain) : RandomRay()
1,772,100✔
255
{
256
  initialize_ray(ray_id, domain);
1,772,100✔
257
}
1,772,100✔
258

259
// Transports ray until termination criteria are met
260
uint64_t RandomRay::transport_history_based_single_ray()
1,772,100✔
261
{
262
  using namespace openmc;
263
  while (alive()) {
731,562,007✔
264
    event_advance_ray();
731,562,007✔
265
    if (!alive())
731,562,007✔
266
      break;
1,772,100✔
267
    event_cross_surface();
729,789,907✔
268
  }
269

270
  return n_event();
1,772,100✔
271
}
272

273
// Transports ray across a single source region
274
void RandomRay::event_advance_ray()
731,562,007✔
275
{
276
  // Find the distance to the nearest boundary
277
  boundary() = distance_to_boundary(*this);
731,562,007✔
278
  double distance = boundary().distance;
731,562,007✔
279

280
  if (distance <= 0.0) {
731,562,007✔
281
    mark_as_lost("Negative transport distance detected for particle " +
×
282
                 std::to_string(id()));
×
283
    return;
×
284
  }
285

286
  if (is_active_) {
731,562,007✔
287
    // If the ray is in the active length, need to check if it has
288
    // reached its maximum termination distance. If so, reduce
289
    // the ray traced length so that the ray does not overrun the
290
    // maximum numerical length (so as to avoid numerical bias).
291
    if (distance_travelled_ + distance >= distance_active_) {
591,331,268✔
292
      distance = distance_active_ - distance_travelled_;
1,772,100✔
293
      wgt() = 0.0;
1,772,100✔
294
    }
295

296
    distance_travelled_ += distance;
591,331,268✔
297
    attenuate_flux(distance, true);
591,331,268✔
298
  } else {
299
    // If the ray is still in the dead zone, need to check if it
300
    // has entered the active phase. If so, split into two segments (one
301
    // representing the final part of the dead zone, the other representing the
302
    // first part of the active length) and attenuate each. Otherwise, if the
303
    // full length of the segment is within the dead zone, attenuate as normal.
304
    if (distance_travelled_ + distance >= distance_inactive_) {
140,230,739✔
305
      is_active_ = true;
1,772,100✔
306
      double distance_dead = distance_inactive_ - distance_travelled_;
1,772,100✔
307
      attenuate_flux(distance_dead, false);
1,772,100✔
308

309
      double distance_alive = distance - distance_dead;
1,772,100✔
310

311
      // Ensure we haven't travelled past the active phase as well
312
      if (distance_alive > distance_active_) {
1,772,100✔
313
        distance_alive = distance_active_;
×
314
        wgt() = 0.0;
×
315
      }
316

317
      attenuate_flux(distance_alive, true, distance_dead);
1,772,100✔
318
      distance_travelled_ = distance_alive;
1,772,100✔
319
    } else {
320
      distance_travelled_ += distance;
138,458,639✔
321
      attenuate_flux(distance, false);
138,458,639✔
322
    }
323
  }
324

325
  // Advance particle
326
  for (int j = 0; j < n_coord(); ++j) {
2,147,483,647✔
327
    coord(j).r += distance * coord(j).u;
2,147,483,647✔
328
  }
329
}
330

331
void RandomRay::attenuate_flux(double distance, bool is_active, double offset)
733,334,107✔
332
{
333
  // Determine source region index etc.
334
  int i_cell = lowest_coord().cell;
733,334,107✔
335

336
  // The base source region is the spatial region index
337
  int64_t sr = domain_->source_region_offsets_[i_cell] + cell_instance();
733,334,107✔
338

339
  // Perform ray tracing across mesh
340
  if (mesh_subdivision_enabled_) {
733,334,107✔
341
    // Determine the mesh index for the base source region, if any
342
    int mesh_idx = domain_->base_source_regions_.mesh(sr);
389,816,966✔
343

344
    if (mesh_idx == C_NONE) {
389,816,966✔
345
      // If there's no mesh being applied to this cell, then
346
      // we just attenuate the flux as normal, and set
347
      // the mesh bin to 0
NEW
348
      attenuate_flux_inner(distance, is_active, sr, 0, r());
×
349
    } else {
350
      // If there is a mesh being applied to this cell, then
351
      // we loop over all the bin crossings and attenuate
352
      // separately.
353
      Mesh* mesh = model::meshes[mesh_idx].get();
389,816,966✔
354
      vector<int> bins;
389,816,966✔
355
      vector<double> fractional_lengths;
389,816,966✔
356

357
      // We adjust the start and end positions of the ray slightly
358
      // to accomodate for floating point precision issues that tend
359
      // to occur at mesh boundaries that overlap with geometry lattice
360
      // boundaries.
361
      Position start = r() + (offset + TINY_BIT) * u();
389,816,966✔
362
      Position end = start + (distance - 2.0 * TINY_BIT) * u();
389,816,966✔
363
      double reduced_distance = (end - start).norm();
389,816,966✔
364

365
      // Ray trace through the mesh and record bins and lengths
366
      mesh->bins_crossed(start, end, u(), bins, fractional_lengths);
389,816,966✔
367

368
      // Loop over all mesh bins and attenuate flux
369
      for (int b = 0; b < bins.size(); b++) {
1,195,660,697✔
370
        double physical_length = reduced_distance * fractional_lengths[b];
805,843,731✔
371
        attenuate_flux_inner(physical_length, is_active, sr, bins[b], start);
805,843,731✔
372
        start += physical_length * u();
805,843,731✔
373
      }
374
    }
389,816,966✔
375
  } else {
376
    attenuate_flux_inner(distance, is_active, sr, C_NONE, r());
343,517,141✔
377
  }
378
}
733,334,107✔
379

380
void RandomRay::attenuate_flux_inner(
1,149,360,872✔
381
  double distance, bool is_active, int64_t sr, int mesh_bin, Position r)
382
{
383
  SourceRegionHandle srh;
1,149,360,872✔
384
  if (mesh_subdivision_enabled_) {
1,149,360,872✔
385
    srh = domain_->get_subdivided_source_region_handle(
805,843,731✔
386
      sr, mesh_bin, r, distance, u());
805,843,731✔
387
    if (srh.is_numerical_fp_artifact_) {
805,843,731✔
388
      return;
88✔
389
    }
390
  } else {
391
    srh = domain_->source_regions_.get_source_region_handle(sr);
343,517,141✔
392
  }
393

394
  switch (source_shape_) {
1,149,360,784✔
395
  case RandomRaySourceShape::FLAT:
570,996,459✔
396
    if (this->material() == MATERIAL_VOID) {
570,996,459✔
397
      attenuate_flux_flat_source_void(srh, distance, is_active, r);
8,287,235✔
398
    } else {
399
      attenuate_flux_flat_source(srh, distance, is_active, r);
562,709,224✔
400
    }
401
    break;
570,996,459✔
402
  case RandomRaySourceShape::LINEAR:
578,364,325✔
403
  case RandomRaySourceShape::LINEAR_XY:
404
    if (this->material() == MATERIAL_VOID) {
578,364,325✔
405
      attenuate_flux_linear_source_void(srh, distance, is_active, r);
8,287,235✔
406
    } else {
407
      attenuate_flux_linear_source(srh, distance, is_active, r);
570,077,090✔
408
    }
409
    break;
578,364,325✔
410
  default:
×
411
    fatal_error("Unknown source shape for random ray transport.");
×
412
  }
413
}
414

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

434
  // Get material
435
  int material = this->material();
562,709,224✔
436

437
  // MOC incoming flux attenuation + source contribution/attenuation equation
438
  for (int g = 0; g < negroups_; g++) {
1,669,707,116✔
439
    float sigma_t = domain_->sigma_t_[material * negroups_ + g];
1,106,997,892✔
440
    float tau = sigma_t * distance;
1,106,997,892✔
441
    float exponential = cjosey_exponential(tau); // exponential = 1 - exp(-tau)
1,106,997,892✔
442
    float new_delta_psi = (angular_flux_[g] - srh.source(g)) * exponential;
1,106,997,892✔
443
    delta_psi_[g] = new_delta_psi;
1,106,997,892✔
444
    angular_flux_[g] -= new_delta_psi;
1,106,997,892✔
445
  }
446

447
  // If ray is in the active phase (not in dead zone), make contributions to
448
  // source region bookkeeping
449

450
  // Aquire lock for source region
451
  srh.lock().lock();
562,709,224✔
452

453
  if (is_active) {
562,709,224✔
454
    // Accumulate delta psi into new estimate of source region flux for
455
    // this iteration
456
    for (int g = 0; g < negroups_; g++) {
1,322,530,858✔
457
      srh.scalar_flux_new(g) += delta_psi_[g];
862,564,703✔
458
    }
459

460
    // Accomulate volume (ray distance) into this iteration's estimate
461
    // of the source region's volume
462
    srh.volume() += distance;
459,966,155✔
463

464
    srh.n_hits() += 1;
459,966,155✔
465
  }
466

467
  // Tally valid position inside the source region (e.g., midpoint of
468
  // the ray) if not done already
469
  if (!srh.position_recorded()) {
562,709,224✔
470
    Position midpoint = r + u() * (distance / 2.0);
982,289✔
471
    srh.position() = midpoint;
982,289✔
472
    srh.position_recorded() = 1;
982,289✔
473
  }
474

475
  // Release lock
476
  srh.lock().unlock();
562,709,224✔
477
}
562,709,224✔
478

479
// Alternative flux attenuation function for true void regions.
480
void RandomRay::attenuate_flux_flat_source_void(
8,287,235✔
481
  SourceRegionHandle& srh, double distance, bool is_active, Position r)
482
{
483
  // The number of geometric intersections is counted for reporting purposes
484
  n_event()++;
8,287,235✔
485

486
  int material = this->material();
8,287,235✔
487

488
  // If ray is in the active phase (not in dead zone), make contributions to
489
  // source region bookkeeping
490
  if (is_active) {
8,287,235✔
491

492
    // Aquire lock for source region
493
    srh.lock().lock();
6,892,402✔
494

495
    // Accumulate delta psi into new estimate of source region flux for
496
    // this iteration
497
    for (int g = 0; g < negroups_; g++) {
13,784,804✔
498
      srh.scalar_flux_new(g) += angular_flux_[g] * distance;
6,892,402✔
499
    }
500

501
    // Accomulate volume (ray distance) into this iteration's estimate
502
    // of the source region's volume
503
    srh.volume() += distance;
6,892,402✔
504
    srh.volume_sq() += distance * distance;
6,892,402✔
505
    srh.n_hits() += 1;
6,892,402✔
506

507
    // Tally valid position inside the source region (e.g., midpoint of
508
    // the ray) if not done already
509
    if (!srh.position_recorded()) {
6,892,402✔
510
      Position midpoint = r + u() * (distance / 2.0);
11,000✔
511
      srh.position() = midpoint;
11,000✔
512
      srh.position_recorded() = 1;
11,000✔
513
    }
514

515
    // Release lock
516
    srh.lock().unlock();
6,892,402✔
517
  }
518

519
  // Add source to incoming angular flux, assuming void region
520
  for (int g = 0; g < negroups_; g++) {
16,574,470✔
521
    angular_flux_[g] += srh.external_source(g) * distance;
8,287,235✔
522
  }
523
}
8,287,235✔
524

525
void RandomRay::attenuate_flux_linear_source(
570,077,090✔
526
  SourceRegionHandle& srh, double distance, bool is_active, Position r)
527
{
528
  // The number of geometric intersections is counted for reporting purposes
529
  n_event()++;
570,077,090✔
530

531
  int material = this->material();
570,077,090✔
532

533
  Position& centroid = srh.centroid();
570,077,090✔
534
  Position midpoint = r + u() * (distance / 2.0);
570,077,090✔
535

536
  // Determine the local position of the midpoint and the ray origin
537
  // relative to the source region's centroid
538
  Position rm_local;
570,077,090✔
539
  Position r0_local;
570,077,090✔
540

541
  // In the first few iterations of the simulation, the source region
542
  // may not yet have had any ray crossings, in which case there will
543
  // be no estimate of its centroid. We detect this by checking if it has
544
  // any accumulated volume. If its volume is zero, just use the midpoint
545
  // of the ray as the region's centroid.
546
  if (srh.volume_t()) {
570,077,090✔
547
    rm_local = midpoint - centroid;
552,821,027✔
548
    r0_local = r - centroid;
552,821,027✔
549
  } else {
550
    rm_local = {0.0, 0.0, 0.0};
17,256,063✔
551
    r0_local = -u() * 0.5 * distance;
17,256,063✔
552
  }
553
  double distance_2 = distance * distance;
570,077,090✔
554

555
  // Linear Source MOC incoming flux attenuation + source
556
  // contribution/attenuation equation
557
  for (int g = 0; g < negroups_; g++) {
1,755,356,812✔
558

559
    // Compute tau, the optical thickness of the ray segment
560
    float sigma_t = domain_->sigma_t_[material * negroups_ + g];
1,185,279,722✔
561
    float tau = sigma_t * distance;
1,185,279,722✔
562

563
    // If tau is very small, set it to zero to avoid numerical issues.
564
    // The following computations will still work with tau = 0.
565
    if (tau < 1.0e-8f) {
1,185,279,722✔
566
      tau = 0.0f;
5,874✔
567
    }
568

569
    // Compute linear source terms, spatial and directional (dir),
570
    // calculated from the source gradients dot product with local centroid
571
    // and direction, respectively.
572
    float spatial_source =
573
      srh.source(g) + rm_local.dot(srh.source_gradients(g));
1,185,279,722✔
574
    float dir_source = u().dot(srh.source_gradients(g));
1,185,279,722✔
575

576
    float gn = exponentialG(tau);
1,185,279,722✔
577
    float f1 = 1.0f - tau * gn;
1,185,279,722✔
578
    float f2 = (2.0f * gn - f1) * distance_2;
1,185,279,722✔
579
    float new_delta_psi = (angular_flux_[g] - spatial_source) * f1 * distance -
1,185,279,722✔
580
                          0.5 * dir_source * f2;
1,185,279,722✔
581

582
    float h1 = f1 - gn;
1,185,279,722✔
583
    float g1 = 0.5f - h1;
1,185,279,722✔
584
    float g2 = exponentialG2(tau);
1,185,279,722✔
585
    g1 = g1 * spatial_source;
1,185,279,722✔
586
    g2 = g2 * dir_source * distance * 0.5f;
1,185,279,722✔
587
    h1 = h1 * angular_flux_[g];
1,185,279,722✔
588
    h1 = (g1 + g2 + h1) * distance_2;
1,185,279,722✔
589
    spatial_source = spatial_source * distance + new_delta_psi;
1,185,279,722✔
590

591
    // Store contributions for this group into arrays, so that they can
592
    // be accumulated into the source region's estimates inside of the locked
593
    // region.
594
    delta_psi_[g] = new_delta_psi;
1,185,279,722✔
595
    delta_moments_[g] = r0_local * spatial_source + u() * h1;
1,185,279,722✔
596

597
    // Update the angular flux for this group
598
    angular_flux_[g] -= new_delta_psi * sigma_t;
1,185,279,722✔
599

600
    // If 2D mode is enabled, the z-component of the flux moments is forced
601
    // to zero
602
    if (source_shape_ == RandomRaySourceShape::LINEAR_XY) {
1,185,279,722✔
603
      delta_moments_[g].z = 0.0;
462,208,043✔
604
    }
605
  }
606

607
  // Compute an estimate of the spatial moments matrix for the source
608
  // region based on parameters from this ray's crossing
609
  MomentMatrix moment_matrix_estimate;
610
  moment_matrix_estimate.compute_spatial_moments_matrix(
570,077,090✔
611
    rm_local, u(), distance);
570,077,090✔
612

613
  // Aquire lock for source region
614
  srh.lock().lock();
570,077,090✔
615

616
  // If ray is in the active phase (not in dead zone), make contributions to
617
  // source region bookkeeping
618

619
  if (is_active) {
570,077,090✔
620
    // Accumulate deltas into the new estimate of source region flux for this
621
    // iteration
622
    for (int g = 0; g < negroups_; g++) {
1,393,559,662✔
623
      srh.scalar_flux_new(g) += delta_psi_[g];
927,520,616✔
624
      srh.flux_moments_new(g) += delta_moments_[g];
927,520,616✔
625
    }
626

627
    // Accumulate the volume (ray segment distance), centroid, and spatial
628
    // momement estimates into the running totals for the iteration for this
629
    // source region. The centroid and spatial momements estimates are scaled
630
    // by the ray segment length as part of length averaging of the estimates.
631
    srh.volume() += distance;
466,039,046✔
632
    srh.centroid_iteration() += midpoint * distance;
466,039,046✔
633
    moment_matrix_estimate *= distance;
466,039,046✔
634
    srh.mom_matrix() += moment_matrix_estimate;
466,039,046✔
635

636
    srh.n_hits() += 1;
466,039,046✔
637
  }
638

639
  // Tally valid position inside the source region (e.g., midpoint of
640
  // the ray) if not done already
641
  if (!srh.position_recorded()) {
570,077,090✔
642
    srh.position() = midpoint;
809,006✔
643
    srh.position_recorded() = 1;
809,006✔
644
  }
645

646
  // Release lock
647
  srh.lock().unlock();
570,077,090✔
648
}
570,077,090✔
649

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

664
  Position& centroid = srh.centroid();
8,287,235✔
665
  Position midpoint = r + u() * (distance / 2.0);
8,287,235✔
666

667
  // Determine the local position of the midpoint and the ray origin
668
  // relative to the source region's centroid
669
  Position rm_local;
8,287,235✔
670
  Position r0_local;
8,287,235✔
671

672
  // In the first few iterations of the simulation, the source region
673
  // may not yet have had any ray crossings, in which case there will
674
  // be no estimate of its centroid. We detect this by checking if it has
675
  // any accumulated volume. If its volume is zero, just use the midpoint
676
  // of the ray as the region's centroid.
677
  if (srh.volume_t()) {
8,287,235✔
678
    rm_local = midpoint - centroid;
8,084,186✔
679
    r0_local = r - centroid;
8,084,186✔
680
  } else {
681
    rm_local = {0.0, 0.0, 0.0};
203,049✔
682
    r0_local = -u() * 0.5 * distance;
203,049✔
683
  }
684
  double distance_2 = distance * distance;
8,287,235✔
685

686
  // Compared to linear flux attenuation through solid regions,
687
  // transport through a void region is greatly simplified. Here we
688
  // compute the updated flux moments.
689
  for (int g = 0; g < negroups_; g++) {
16,574,470✔
690
    float spatial_source = srh.external_source(g);
8,287,235✔
691
    float new_delta_psi = (angular_flux_[g] - spatial_source) * distance;
8,287,235✔
692
    float h1 = 0.5f;
8,287,235✔
693
    h1 = h1 * angular_flux_[g];
8,287,235✔
694
    h1 = h1 * distance_2;
8,287,235✔
695
    spatial_source = spatial_source * distance + new_delta_psi;
8,287,235✔
696

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

702
    // If 2D mode is enabled, the z-component of the flux moments is forced
703
    // to zero
704
    if (source_shape_ == RandomRaySourceShape::LINEAR_XY) {
8,287,235✔
705
      delta_moments_[g].z = 0.0;
×
706
    }
707
  }
708

709
  // If ray is in the active phase (not in dead zone), make contributions to
710
  // source region bookkeeping
711
  if (is_active) {
8,287,235✔
712
    // Compute an estimate of the spatial moments matrix for the source
713
    // region based on parameters from this ray's crossing
714
    MomentMatrix moment_matrix_estimate;
715
    moment_matrix_estimate.compute_spatial_moments_matrix(
6,892,402✔
716
      rm_local, u(), distance);
6,892,402✔
717

718
    // Aquire lock for source region
719
    srh.lock().lock();
6,892,402✔
720

721
    // Accumulate delta psi into new estimate of source region flux for
722
    // this iteration, and update flux momements
723
    for (int g = 0; g < negroups_; g++) {
13,784,804✔
724
      srh.scalar_flux_new(g) += angular_flux_[g] * distance;
6,892,402✔
725
      srh.flux_moments_new(g) += delta_moments_[g];
6,892,402✔
726
    }
727

728
    // Accumulate the volume (ray segment distance), centroid, and spatial
729
    // momement estimates into the running totals for the iteration for this
730
    // source region. The centroid and spatial momements estimates are scaled by
731
    // the ray segment length as part of length averaging of the estimates.
732
    srh.volume() += distance;
6,892,402✔
733
    srh.volume_sq() += distance_2;
6,892,402✔
734
    srh.centroid_iteration() += midpoint * distance;
6,892,402✔
735
    moment_matrix_estimate *= distance;
6,892,402✔
736
    srh.mom_matrix() += moment_matrix_estimate;
6,892,402✔
737

738
    // Tally valid position inside the source region (e.g., midpoint of
739
    // the ray) if not done already
740
    if (!srh.position_recorded()) {
6,892,402✔
741
      srh.position() = midpoint;
11,000✔
742
      srh.position_recorded() = 1;
11,000✔
743
    }
744

745
    srh.n_hits() += 1;
6,892,402✔
746

747
    // Release lock
748
    srh.lock().unlock();
6,892,402✔
749
  }
750

751
  // Add source to incoming angular flux, assuming void region
752
  for (int g = 0; g < negroups_; g++) {
16,574,470✔
753
    angular_flux_[g] += srh.external_source(g) * distance;
8,287,235✔
754
  }
755
}
8,287,235✔
756

757
void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain)
1,772,100✔
758
{
759
  domain_ = domain;
1,772,100✔
760

761
  // Reset particle event counter
762
  n_event() = 0;
1,772,100✔
763

764
  is_active_ = (distance_inactive_ <= 0.0);
1,772,100✔
765

766
  wgt() = 1.0;
1,772,100✔
767

768
  // set identifier for particle
769
  id() = ray_id;
1,772,100✔
770

771
  // generate source site using sample method
772
  SourceSite site;
1,772,100✔
773
  switch (sample_method_) {
1,772,100✔
774
  case RandomRaySampleMethod::PRNG:
1,761,100✔
775
    site = sample_prng();
1,761,100✔
776
    break;
1,761,100✔
777
  case RandomRaySampleMethod::HALTON:
11,000✔
778
    site = sample_halton();
11,000✔
779
    break;
11,000✔
780
  default:
×
781
    fatal_error("Unknown sample method for random ray transport.");
×
782
  }
783

784
  site.E = lower_bound_index(
1,772,100✔
785
    data::mg.rev_energy_bins_.begin(), data::mg.rev_energy_bins_.end(), site.E);
786
  site.E = negroups_ - site.E - 1.;
1,772,100✔
787
  this->from_source(&site);
1,772,100✔
788

789
  // Locate ray
790
  if (lowest_coord().cell == C_NONE) {
1,772,100✔
791
    if (!exhaustive_find_cell(*this)) {
1,772,100✔
792
      this->mark_as_lost(
×
793
        "Could not find the cell containing particle " + std::to_string(id()));
×
794
    }
795

796
    // Set birth cell attribute
797
    if (cell_born() == C_NONE)
1,772,100✔
798
      cell_born() = lowest_coord().cell;
1,772,100✔
799
  }
800

801
  // Initialize ray's starting angular flux to starting location's isotropic
802
  // source
803
  int i_cell = lowest_coord().cell;
1,772,100✔
804
  int64_t sr = domain_->source_region_offsets_[i_cell] + cell_instance();
1,772,100✔
805

806
  SourceRegionHandle srh;
1,772,100✔
807
  if (mesh_subdivision_enabled_) {
1,772,100✔
808
    int mesh_idx = domain_->base_source_regions_.mesh(sr);
1,060,400✔
809
    int mesh_bin;
810
    if (mesh_idx == C_NONE) {
1,060,400✔
NEW
811
      mesh_bin = 0;
×
812
    } else {
813
      Mesh* mesh = model::meshes[mesh_idx].get();
1,060,400✔
814
      mesh_bin = mesh->get_bin(r());
1,060,400✔
815
    }
816
    srh =
817
      domain_->get_subdivided_source_region_handle(sr, mesh_bin, r(), 0.0, u());
1,060,400✔
818
  } else {
819
    srh = domain_->source_regions_.get_source_region_handle(sr);
711,700✔
820
  }
821

822
  if (!srh.is_numerical_fp_artifact_) {
1,772,100✔
823
    for (int g = 0; g < negroups_; g++) {
4,897,200✔
824
      angular_flux_[g] = srh.source(g);
3,125,100✔
825
    }
826
  }
827
}
1,772,100✔
828

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

837
  // Sample from ray source distribution
838
  SourceSite site {ray_source_->sample(current_seed())};
1,761,100✔
839

840
  return site;
1,761,100✔
841
}
842

843
SourceSite RandomRay::sample_halton()
11,000✔
844
{
845
  SourceSite site;
11,000✔
846

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

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

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

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

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

876
  return site;
22,000✔
877
}
11,000✔
878

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