• 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

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

3
#include "openmc/capi.h"
4
#include "openmc/eigenvalue.h"
5
#include "openmc/geometry.h"
6
#include "openmc/message_passing.h"
7
#include "openmc/mgxs_interface.h"
8
#include "openmc/output.h"
9
#include "openmc/plot.h"
10
#include "openmc/random_ray/flat_source_domain.h"
11
#include "openmc/random_ray/random_ray.h"
12
#include "openmc/simulation.h"
13
#include "openmc/source.h"
14
#include "openmc/tallies/filter.h"
15
#include "openmc/tallies/tally.h"
16
#include "openmc/tallies/tally_scoring.h"
17
#include "openmc/timer.h"
18
#include "openmc/weight_windows.h"
19

20
namespace openmc {
21

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

26
// Enforces restrictions on inputs in random ray mode.  While there are
27
// many features that don't make sense in random ray mode, and are therefore
28
// unsupported, we limit our testing/enforcement operations only to inputs
29
// that may cause erroneous/misleading output or crashes from the solver.
30
void validate_random_ray_inputs()
694✔
31
{
32
  // Validate tallies
33
  ///////////////////////////////////////////////////////////////////
34
  for (auto& tally : model::tallies) {
1,773✔
35

36
    // Validate score types
37
    for (auto score_bin : tally->scores_) {
2,488✔
38
      switch (score_bin) {
1,409!
39
      case SCORE_FLUX:
40
      case SCORE_TOTAL:
41
      case SCORE_FISSION:
42
      case SCORE_NU_FISSION:
43
      case SCORE_EVENTS:
44
      case SCORE_KAPPA_FISSION:
45
        break;
46

47
      // TODO: add support for prompt and delayed fission
48
      case SCORE_PRECURSORS: {
44✔
49
        if (settings::kinetic_simulation && settings::create_delayed_neutrons) {
44!
50
          break;
51
        } else {
NEW
52
          fatal_error("Invalid score specified in tallies.xml. Precursors can "
×
53
                      "only be scored for kinetic simulations.");
54
        }
55
      }
56
      default:
×
57
        fatal_error(
×
58
          "Invalid score specified. Only flux, total, fission, nu-fission, "
59
          "kappa-fission and event scores are supported in random ray mode. "
60
          "(precursors are supported for kinetic simulations when delayed "
61
          "neutrons are turned on).");
62
      }
63
    }
64

65
    // Validate filter types
66
    for (auto f : tally->filters()) {
2,347✔
67
      auto& filter = *model::tally_filters[f];
1,268✔
68

69
      switch (filter.type()) {
1,268!
70
      case FilterType::CELL:
71
      case FilterType::CELL_INSTANCE:
72
      case FilterType::DISTRIBCELL:
73
      case FilterType::ENERGY:
74
      case FilterType::MATERIAL:
75
      case FilterType::MESH:
76
      case FilterType::UNIVERSE:
77
      case FilterType::PARTICLE:
78
        break;
79
      case FilterType::DELAYED_GROUP:
44✔
80
        if (settings::kinetic_simulation) {
44!
81
          break;
82
        } else {
NEW
83
          fatal_error("Invalid filter specified in tallies.xml. Kinetic "
×
84
                      "simulations is required "
85
                      "to tally with a delayed_group filter.");
86
        }
87
      default:
×
88
        fatal_error("Invalid filter specified. Only cell, cell_instance, "
×
89
                    "distribcell, energy, material, mesh, and universe filters "
90
                    "are supported in random ray mode (delayed_group is "
91
                    "supported for kinetic simulations).");
92
      }
93
    }
94
  }
95

96
  // TODO: validate kinetic data is present
97
  //  Validate MGXS data
98
  ///////////////////////////////////////////////////////////////////
99
  for (auto& material : data::mg.macro_xs_) {
2,587✔
100
    if (!material.is_isotropic) {
1,893!
101
      fatal_error("Anisotropic MGXS detected. Only isotropic XS data sets "
×
102
                  "supported in random ray mode.");
103
    }
104
    for (int g = 0; g < data::mg.num_energy_groups_; g++) {
14,149✔
105
      if (material.exists_in_model) {
12,256✔
106
        // Temperature and angle indices, if using multiple temperature
107
        // data sets and/or anisotropic data sets.
108
        // TODO: Currently assumes we are only using single temp/single angle
109
        // data.
110
        const int t = 0;
12,212✔
111
        const int a = 0;
12,212✔
112
        double sigma_t =
12,212✔
113
          material.get_xs(MgxsType::TOTAL, g, NULL, NULL, NULL, t, a);
12,212✔
114
        if (sigma_t <= 0.0) {
12,212!
115
          fatal_error("No zero or negative total macroscopic cross sections "
×
116
                      "allowed in random ray mode. If the intention is to make "
117
                      "a void material, use a cell fill of 'None' instead.");
118
        }
119
      }
120
    }
121
  }
122

123
  // Validate ray source
124
  ///////////////////////////////////////////////////////////////////
125

126
  // Check for independent source
127
  IndependentSource* is =
694!
128
    dynamic_cast<IndependentSource*>(RandomRay::ray_source_.get());
694!
129
  if (!is) {
694!
130
    fatal_error("Invalid ray source definition. Ray source must provided and "
×
131
                "be of type IndependentSource.");
132
  }
133

134
  // Check for box source
135
  SpatialDistribution* space_dist = is->space();
694!
136
  SpatialBox* sb = dynamic_cast<SpatialBox*>(space_dist);
694!
137
  if (!sb) {
694!
138
    fatal_error(
×
139
      "Invalid ray source definition -- only box sources are allowed.");
140
  }
141

142
  // Check that box source is not restricted to fissionable areas
143
  if (sb->only_fissionable()) {
694!
144
    fatal_error(
×
145
      "Invalid ray source definition -- fissionable spatial distribution "
146
      "not allowed.");
147
  }
148

149
  // Check for isotropic source
150
  UnitSphereDistribution* angle_dist = is->angle();
694!
151
  Isotropic* id = dynamic_cast<Isotropic*>(angle_dist);
694!
152
  if (!id) {
694!
153
    fatal_error("Invalid ray source definition -- only isotropic sources are "
×
154
                "allowed.");
155
  }
156

157
  // Validate external sources
158
  ///////////////////////////////////////////////////////////////////
159
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
694✔
160
    if (model::external_sources.size() < 1) {
298!
161
      fatal_error("Must provide a particle source (in addition to ray source) "
×
162
                  "in fixed source random ray mode.");
163
    }
164

165
    for (int i = 0; i < model::external_sources.size(); i++) {
596✔
166
      Source* s = model::external_sources[i].get();
298!
167

168
      // Check for independent source
169
      IndependentSource* is = dynamic_cast<IndependentSource*>(s);
298!
170

171
      if (!is) {
298!
172
        fatal_error(
×
173
          "Only IndependentSource external source types are allowed in "
174
          "random ray mode");
175
      }
176

177
      // Check for isotropic source
178
      UnitSphereDistribution* angle_dist = is->angle();
298!
179
      Isotropic* id = dynamic_cast<Isotropic*>(angle_dist);
298!
180
      if (!id) {
298!
181
        fatal_error(
×
182
          "Invalid source definition -- only isotropic external sources are "
183
          "allowed in random ray mode.");
184
      }
185

186
      // Validate that a domain ID was specified OR that it is a point source
187
      auto sp = dynamic_cast<SpatialPoint*>(is->space());
298!
188
      if (is->domain_ids().size() == 0 && !sp) {
298!
189
        fatal_error("Fixed sources must be point source or spatially "
×
190
                    "constrained by domain id (cell, material, or universe) in "
191
                    "random ray mode.");
192
      } else if (is->domain_ids().size() > 0 && sp) {
298✔
193
        // If both a domain constraint and a non-default point source location
194
        // are specified, notify user that domain constraint takes precedence.
195
        if (sp->r().x == 0.0 && sp->r().y == 0.0 && sp->r().z == 0.0) {
253!
196
          warning("Fixed source has both a domain constraint and a point "
506✔
197
                  "type spatial distribution. The domain constraint takes "
198
                  "precedence in random ray mode -- point source coordinate "
199
                  "will be ignored.");
200
        }
201
      }
202

203
      // Check that a discrete energy distribution was used
204
      Distribution* d = is->energy();
298!
205
      Discrete* dd = dynamic_cast<Discrete*>(d);
298!
206
      if (!dd) {
298!
207
        fatal_error(
×
208
          "Only discrete (multigroup) energy distributions are allowed for "
209
          "external sources in random ray mode.");
210
      }
211
    }
212
  }
213

214
  // Validate plotting files
215
  ///////////////////////////////////////////////////////////////////
216
  for (int p = 0; p < model::plots.size(); p++) {
694!
217

218
    // Get handle to OpenMC plot object
219
    const auto& openmc_plottable = model::plots[p];
×
220
    Plot* openmc_plot = dynamic_cast<Plot*>(openmc_plottable.get());
×
221

222
    // Random ray plots only support voxel plots
223
    if (!openmc_plot) {
×
224
      warning(fmt::format(
×
225
        "Plot {} will not be used for end of simulation data plotting -- only "
226
        "voxel plotting is allowed in random ray mode.",
227
        openmc_plottable->id()));
×
228
      continue;
×
229
    } else if (openmc_plot->type_ != Plot::PlotType::voxel) {
×
230
      warning(fmt::format(
×
231
        "Plot {} will not be used for end of simulation data plotting -- only "
232
        "voxel plotting is allowed in random ray mode.",
233
        openmc_plottable->id()));
×
234
      continue;
×
235
    }
236
  }
237

238
  // Warn about slow MPI domain replication, if detected
239
  ///////////////////////////////////////////////////////////////////
240
#ifdef OPENMC_MPI
241
  if (mpi::n_procs > 1) {
253✔
242
    warning(
496✔
243
      "MPI parallelism is not supported by the random ray solver. All work "
244
      "will be performed by rank 0. Domain decomposition may be implemented in "
245
      "the future to provide efficient MPI scaling.");
246
  }
247
#endif
248

249
  // Warn about instability resulting from linear sources in small regions
250
  // when generating weight windows with FW-CADIS and an overlaid mesh.
251
  ///////////////////////////////////////////////////////////////////
252
  if (RandomRay::source_shape_ == RandomRaySourceShape::LINEAR &&
694✔
253
      variance_reduction::weight_windows.size() > 0) {
242✔
254
    warning(
22✔
255
      "Linear sources may result in negative fluxes in small source regions "
256
      "generated by mesh subdivision. Negative sources may result in low "
257
      "quality FW-CADIS weight windows. We recommend you use flat source mode "
258
      "when generating weight windows with an overlaid mesh tally.");
259
  }
260
}
694✔
261

NEW
262
void openmc_reset_random_ray()
×
263
{
NEW
264
  FlatSourceDomain::volume_estimator_ = RandomRayVolumeEstimator::HYBRID;
×
NEW
265
  FlatSourceDomain::volume_normalized_flux_tallies_ = false;
×
NEW
266
  FlatSourceDomain::adjoint_ = false;
×
NEW
267
  FlatSourceDomain::mesh_domain_map_.clear();
×
NEW
268
  RandomRay::ray_source_.reset();
×
NEW
269
  RandomRay::source_shape_ = RandomRaySourceShape::FLAT;
×
NEW
270
  RandomRay::sample_method_ = RandomRaySampleMethod::PRNG;
×
NEW
271
  RandomRay::bd_order_ = 3;
×
NEW
272
  RandomRay::time_method_ = RandomRayTimeMethod::ISOTROPIC;
×
NEW
273
}
×
274

275
void write_random_ray_hdf5(hid_t group)
1,542✔
276
{
277
  hid_t random_ray_group = create_group(group, "random_ray");
1,542✔
278
  switch (RandomRay::source_shape_) {
1,542!
279
  case RandomRaySourceShape::FLAT:
1,256✔
280
    write_dataset(random_ray_group, "source_shape", "flat");
1,256✔
281
    break;
1,256✔
282
  case RandomRaySourceShape::LINEAR:
253✔
283
    write_dataset(random_ray_group, "source_shape", "linear");
253✔
284
    break;
253✔
285
  case RandomRaySourceShape::LINEAR_XY:
33✔
286
    write_dataset(random_ray_group, "source_shape", "linear xy");
33✔
287
    break;
33✔
288
  default:
289
    break;
290
  }
291

292
  switch (FlatSourceDomain::volume_estimator_) {
1,542!
293
  case RandomRayVolumeEstimator::SIMULATION_AVERAGED:
22✔
294
    write_dataset(random_ray_group, "volume_estimator", "simulation averaged");
22✔
295
    break;
22✔
296
  case RandomRayVolumeEstimator::NAIVE:
68✔
297
    write_dataset(random_ray_group, "volume_estimator", "naive");
68✔
298
    break;
68✔
299
  case RandomRayVolumeEstimator::HYBRID:
1,452✔
300
    write_dataset(random_ray_group, "volume_estimator", "hybrid");
1,452✔
301
    break;
1,452✔
302
  default:
303
    break;
304
  }
305

306
  write_dataset(
1,542✔
307
    random_ray_group, "distance_active", RandomRay::distance_active_);
308
  write_dataset(
1,542✔
309
    random_ray_group, "distance_inactive", RandomRay::distance_inactive_);
310
  write_dataset(random_ray_group, "volume_normalized_flux_tallies",
1,542✔
311
    FlatSourceDomain::volume_normalized_flux_tallies_);
312
  write_dataset(random_ray_group, "adjoint_mode", FlatSourceDomain::adjoint_);
1,542✔
313

314
  write_dataset(random_ray_group, "avg_miss_rate", RandomRay::avg_miss_rate_);
1,542✔
315
  write_dataset(
1,542✔
316
    random_ray_group, "n_source_regions", RandomRay::n_source_regions_);
317
  write_dataset(random_ray_group, "n_external_source_regions",
1,542✔
318
    RandomRay::n_external_source_regions_);
319
  write_dataset(random_ray_group, "n_geometric_intersections",
1,542✔
320
    RandomRay::total_geometric_intersections_);
321
  int64_t n_integrations =
1,542✔
322
    RandomRay::total_geometric_intersections_ * data::mg.num_energy_groups_;
1,542✔
323
  write_dataset(random_ray_group, "n_integrations", n_integrations);
1,542✔
324

325
  if (settings::kinetic_simulation && !simulation::is_initial_condition) {
1,542✔
326
    write_dataset(random_ray_group, "bd_order", RandomRay::bd_order_);
660✔
327
    switch (RandomRay::time_method_) {
660!
328
    case RandomRayTimeMethod::ISOTROPIC:
440✔
329
      write_dataset(random_ray_group, "time_method", "isotropic");
440✔
330
      break;
440✔
331
    case RandomRayTimeMethod::PROPAGATION:
220✔
332
      write_dataset(random_ray_group, "time_method", "propogation");
220✔
333
      break;
220✔
334
    default:
335
      break;
336
    }
337
  }
338
  close_group(random_ray_group);
1,542✔
339
}
1,542✔
340

341
void print_adjoint_header()
112✔
342
{
343
  if (!FlatSourceDomain::adjoint_)
112✔
344
    // If we're going to do an adjoint simulation afterwards, report that this
345
    // is the initial forward flux solve.
346
    header("FORWARD FLUX SOLVE", 3);
56✔
347
  else
348
    // Otherwise report that we are doing the adjoint simulation
349
    header("ADJOINT FLUX SOLVE", 3);
56✔
350
}
112✔
351

352
//-----------------------------------------------------------------------------
353
// Non-member functions for kinetic simulations
354

355
void rename_time_step_file(
2,160✔
356
  std::string base_filename, std::string extension, int i)
357
{
358
  // Rename file
359
  std::string old_filename_ =
2,160✔
360
    fmt::format("{0}{1}{2}", settings::path_output, base_filename, extension);
2,160✔
361
  std::string new_filename_ = fmt::format(
2,160✔
362
    "{0}{1}_{2}{3}", settings::path_output, base_filename, i, extension);
2,160✔
363

364
  const char* old_fname = old_filename_.c_str();
2,160✔
365
  const char* new_fname = new_filename_.c_str();
2,160✔
366
  std::rename(old_fname, new_fname);
2,160✔
367
}
2,160✔
368

369
//==============================================================================
370
// RandomRaySimulation implementation
371
//==============================================================================
372

373
RandomRaySimulation::RandomRaySimulation()
942✔
374
  : negroups_(data::mg.num_energy_groups_),
942!
375
    ndgroups_(data::mg.num_delayed_groups_)
942!
376
{
377
  // There are no source sites in random ray mode, so be sure to disable to
378
  // ensure we don't attempt to write source sites to statepoint
379
  settings::source_write = false;
942✔
380

381
  // Random ray mode does not have an inner loop over generations within a
382
  // batch, so set the current gen to 1
383
  simulation::current_gen = 1;
942✔
384

385
  switch (RandomRay::source_shape_) {
942!
386
  case RandomRaySourceShape::FLAT:
567✔
387
    domain_ = make_unique<FlatSourceDomain>();
567✔
388
    break;
567✔
389
  case RandomRaySourceShape::LINEAR:
375✔
390
  case RandomRaySourceShape::LINEAR_XY:
375✔
391
    domain_ = make_unique<LinearSourceDomain>();
375✔
392
    break;
375✔
393
  default:
×
394
    fatal_error("Unknown random ray source shape");
×
395
  }
396

397
  // Convert OpenMC native MGXS into a more efficient format
398
  // internal to the random ray solver
399
  domain_->flatten_xs();
942✔
400

401
  // Check if adjoint calculation is needed. If it is, we will run the forward
402
  // calculation first and then the adjoint calculation later.
403
  adjoint_needed_ = FlatSourceDomain::adjoint_;
942✔
404

405
  // Adjoint is always false for the forward calculation
406
  FlatSourceDomain::adjoint_ = false;
942✔
407

408
  // The first simulation is run after initialization
409
  is_first_simulation_ = true;
942✔
410

411
  // Initialize vectors used for baking in the initial condition during time
412
  // stepping
413
  if (settings::kinetic_simulation) {
942✔
414
    // Initialize vars used for time-consistent seed approach
415
    static_avg_k_eff_;
416
    static_k_eff_;
417
    static_fission_rate_;
418
  }
419
}
942✔
420

421
void RandomRaySimulation::apply_fixed_sources_and_mesh_domains()
942✔
422
{
423
  domain_->apply_meshes();
942✔
424
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
942✔
425
    // Transfer external source user inputs onto random ray source regions
426
    domain_->convert_external_sources();
406✔
427
    domain_->count_external_source_regions();
406✔
428
  }
429
}
942✔
430

431
void RandomRaySimulation::prepare_fixed_sources_adjoint()
76✔
432
{
433
  domain_->source_regions_.adjoint_reset();
76✔
434
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
76✔
435
    domain_->set_adjoint_sources();
61✔
436
  }
437
}
76✔
438

439
void RandomRaySimulation::prepare_adjoint_simulation()
76✔
440
{
441
  // Reset all simulation values
442
  domain_->source_regions_.simulation_reset();
76✔
443

444
  // Configure the domain for adjoint simulation
445
  FlatSourceDomain::adjoint_ = true;
76✔
446

447
  // Reset k-eff
448
  domain_->k_eff_ = 1.0;
76✔
449

450
  // Initialize adjoint fixed sources, if present
451
  prepare_fixed_sources_adjoint();
76✔
452

453
  // Transpose scattering matrix
454
  domain_->transpose_scattering_matrix();
76✔
455

456
  // Swap nu_sigma_f and chi
457
  domain_->nu_sigma_f_.swap(domain_->chi_);
76✔
458
}
76✔
459

460
void RandomRaySimulation::simulate()
2,098✔
461
{
462
  if (!is_first_simulation_) {
2,098✔
463
    if (mpi::master && adjoint_needed_)
1,156✔
464
      openmc::print_adjoint_header();
56✔
465

466
    // Reset the timers and reinitialize the general OpenMC datastructures if
467
    // this is after the first simulation
468
    reset_timers();
1,156✔
469

470
    // Initialize OpenMC general data structures
471
    openmc_simulation_init();
1,156✔
472
  }
473

474
  // Begin main simulation timer
475
  simulation::time_total.start();
2,098✔
476

477
  // Random ray power iteration loop
478
  while (simulation::current_batch < settings::n_batches) {
203,208✔
479
    // Initialize the current batch
480
    initialize_batch();
199,012✔
481
    initialize_generation();
199,012✔
482

483
    // MPI not supported in random ray solver, so all work is done by rank 0
484
    // TODO: Implement domain decomposition for MPI parallelism
485
    if (mpi::master) {
199,012✔
486

487
      // Reset total starting particle weight used for normalizing tallies
488
      simulation::total_weight = 1.0;
146,532✔
489

490
      // Update source term (scattering + fission (+ delayed if kinetic))
491
      domain_->update_all_neutron_sources();
146,532✔
492

493
      // Reset scalar fluxes, iteration volume tallies, and region hit flags
494
      // to zero
495
      domain_->batch_reset();
146,532✔
496

497
      // At the beginning of the simulation, if mesh subdivision is in use, we
498
      // need to swap the main source region container into the base container,
499
      // as the main source region container will be used to hold the true
500
      // subdivided source regions. The base container will therefore only
501
      // contain the external source region information, the mesh indices,
502
      // material properties, and initial guess values for the flux/source.
503

504
      // Start timer for transport
505
      simulation::time_transport.start();
146,532✔
506

507
// Transport sweep over all random rays for the iteration
508
#pragma omp parallel for schedule(dynamic)                                     \
79,932✔
509
  reduction(+ : total_geometric_intersections_)
79,932✔
510
      for (int i = 0; i < settings::n_particles; i++) {
6,994,600✔
511
        RandomRay ray(i, domain_.get());
6,928,000✔
512
        total_geometric_intersections_ +=
13,856,000✔
513
          ray.transport_history_based_single_ray();
6,928,000✔
514
      }
6,928,000✔
515

516
      simulation::time_transport.stop();
146,532✔
517

518
      // Add any newly discovered source regions to the main source region
519
      // container.
520
      domain_->finalize_discovered_source_regions();
146,532✔
521

522
      // Normalize scalar flux and update volumes
523
      domain_->normalize_scalar_flux_and_volumes(
146,532✔
524
        settings::n_particles * RandomRay::distance_active_);
525

526
      // Add source to scalar flux, compute number of FSR hits
527
      int64_t n_hits = domain_->add_source_to_scalar_flux();
146,532✔
528

529
      // Apply transport stabilization factors
530
      domain_->apply_transport_stabilization();
146,532✔
531

532
      if (settings::run_mode == RunMode::EIGENVALUE) {
146,532✔
533
        // Compute random ray k-eff for initial condition.
534
        // This keff will be preserved
535
        if (simulation::is_initial_condition) {
136,510✔
536
          domain_->compute_k_eff();
43,010✔
537
          if (settings::kinetic_simulation && simulation::k_eff_correction) {
43,010✔
538
            static_fission_rate_.push_back(domain_->fission_rate_);
18,700✔
539
            static_k_eff_.push_back(domain_->k_eff_);
18,700✔
540
          }
541
        } else {
542
          domain_->k_eff_ = static_k_eff_[simulation::current_batch - 1];
93,500✔
543
          domain_->fission_rate_ =
93,500✔
544
            static_fission_rate_[simulation::current_batch - 1];
93,500✔
545
        }
546

547
        // Store random ray k-eff into OpenMC's native k-eff variable
548
        global_tally_tracklength = domain_->k_eff_;
136,510✔
549
      }
550

551
      // Compute precursors if delayed neutrons are turned on
552
      if (settings::kinetic_simulation && settings::create_delayed_neutrons)
146,532!
553
        domain_->compute_all_precursors();
130,900✔
554

555
      // Execute all tallying tasks, if this is an active batch
556
      if (simulation::current_batch > settings::n_inactive) {
146,532✔
557

558
        // Add this iteration's scalar flux estimate to final accumulated
559
        // estimate
560
        domain_->accumulate_iteration_quantities();
72,001✔
561

562
        // Use above mapping to contribute FSR flux data to appropriate
563
        // tallies
564
        domain_->random_ray_tally();
72,001✔
565
      }
566

567
      // Set phi_old = phi_new
568
      domain_->flux_swap();
146,532✔
569
      if (settings::kinetic_simulation && settings::create_delayed_neutrons) {
146,532!
570
        domain_->precursors_swap();
130,900✔
571
      }
572

573
      // Check for any obvious insabilities/nans/infs
574
      instability_check(n_hits, domain_->k_eff_, avg_miss_rate_);
146,532✔
575
    } // End MPI master work
576

577
    // Store simulation metrics
578
    RandomRay::avg_miss_rate_ = avg_miss_rate_ / settings::n_batches;
199,012✔
579
    RandomRay::total_geometric_intersections_ = total_geometric_intersections_;
199,012✔
580
    RandomRay::n_external_source_regions_ = domain_->n_external_source_regions_;
199,012✔
581
    RandomRay::n_source_regions_ = domain_->n_source_regions();
199,012✔
582

583
    // Finalize the current batch
584
    finalize_generation();
199,012✔
585
    finalize_batch();
199,012✔
586
  } // End random ray power iteration loop
587

588
  domain_->count_external_source_regions();
2,098✔
589

590
  // End main simulation timer
591
  simulation::time_total.stop();
2,098✔
592

593
  // Normalize and save the final forward quantities
594
  domain_->normalize_final_quantities();
2,098✔
595

596
  // Finalize OpenMC
597
  openmc_simulation_finalize();
2,098✔
598

599
  // Output all simulation results
600
  output_simulation_results();
2,098✔
601

602
  // Toggle that the simulation object has been initialized after the first
603
  // simulation
604
  if (is_first_simulation_)
2,098✔
605
    is_first_simulation_ = false;
942✔
606
}
2,098✔
607

608
void RandomRaySimulation::initialize_time_step(int i)
1,080✔
609
{
610
  if (simulation::k_eff_correction)
1,080✔
611
    static_avg_k_eff_ = simulation::keff;
180✔
612
  domain_->k_eff_ = static_avg_k_eff_;
1,080✔
613

614
  // Increment current timestep and simuation time
615
  simulation::current_timestep = i + 1;
1,080✔
616

617
  // Propagate previous converted solution for kinetic simulation
618
  domain_->source_regions_.simulation_reset();
1,080✔
619
  domain_->propagate_final_quantities();
1,080✔
620
  domain_->source_regions_.time_step_reset();
1,080✔
621

622
  if (!simulation::is_initial_condition) {
1,080✔
623
    // Compute RHS backward differences
624
    domain_->compute_rhs_bd_quantities();
900✔
625

626
    // Update time dependent cross section based on the density
627
    domain_->update_material_density(i);
900✔
628

629
    simulation::current_time += settings::dt;
900✔
630
  }
631
}
1,080✔
632

633
void RandomRaySimulation::finalize_time_step()
1,080✔
634
{
635
  if (simulation::is_initial_condition) {
1,080✔
636
    // Initialize the BD arrays if initial condition
637
    domain_->store_time_step_quantities(false);
180✔
638
    // Toggle off initial condition and source correction
639
    simulation::is_initial_condition = false;
180✔
640
    simulation::k_eff_correction = false;
180✔
641
  } else {
642
    // Else, store final quantities for the current time step
643
    domain_->store_time_step_quantities();
900✔
644
  }
645

646
  // Rename statepoint and tallies file for the current time step
647
  rename_time_step_file(fmt::format("statepoint.{0}", settings::n_batches),
2,160✔
648
    ".h5", simulation::current_timestep);
649
  if (settings::output_tallies)
1,080!
650
    rename_time_step_file("tallies", ".out", simulation::current_timestep);
2,160✔
651
}
1,080✔
652

653
void RandomRaySimulation::output_simulation_results() const
2,098✔
654
{
655
  // Print random ray results
656
  if (mpi::master) {
2,098✔
657
    print_results_random_ray();
1,542✔
658
    if (model::plots.size() > 0) {
1,542!
659
      domain_->output_to_vtk();
×
660
    }
661
  }
662
}
2,098✔
663

664
// Apply a few sanity checks to catch obvious cases of numerical instability.
665
// Instability typically only occurs if ray density is extremely low.
666
void RandomRaySimulation::instability_check(
146,532✔
667
  int64_t n_hits, double k_eff, double& avg_miss_rate) const
668
{
669
  double percent_missed = ((domain_->n_source_regions() - n_hits) /
146,532!
670
                            static_cast<double>(domain_->n_source_regions())) *
146,532✔
671
                          100.0;
146,532✔
672
  avg_miss_rate += percent_missed;
146,532✔
673

674
  if (mpi::master) {
146,532!
675
    if (percent_missed > 10.0) {
146,532✔
676
      warning(fmt::format(
1,914✔
677
        "Very high FSR miss rate detected ({:.3f}%). Instability may occur. "
678
        "Increase ray density by adding more rays and/or active distance.",
679
        percent_missed));
680
    } else if (percent_missed > 1.0) {
145,575✔
681
      warning(
2!
682
        fmt::format("Elevated FSR miss rate detected ({:.3f}%). Increasing "
4!
683
                    "ray density by adding more rays and/or active "
684
                    "distance may improve simulation efficiency.",
685
          percent_missed));
686
    }
687

688
    if (k_eff > 10.0 || k_eff < 0.01 || !(std::isfinite(k_eff))) {
146,532!
689
      fatal_error(fmt::format("Instability detected: k-eff = {:.5f}", k_eff));
×
690
    }
691
  }
692
}
146,532✔
693

694
// Print random ray simulation results
695
void RandomRaySimulation::print_results_random_ray() const
1,542✔
696
{
697
  using namespace simulation;
1,542✔
698

699
  if (settings::verbosity >= 6) {
1,542!
700
    double total_integrations =
1,542✔
701
      RandomRay::total_geometric_intersections_ * negroups_;
1,542✔
702
    double time_per_integration =
1,542✔
703
      simulation::time_transport.elapsed() / total_integrations;
1,542✔
704
    double misc_time = time_total.elapsed() - time_update_src.elapsed() -
1,542✔
705
                       time_transport.elapsed() - time_tallies.elapsed() -
1,542✔
706
                       time_bank_sendrecv.elapsed();
1,542✔
707

708
    if (settings::kinetic_simulation && !simulation::is_initial_condition) {
1,542✔
709
      misc_time -= time_update_bd_vectors.elapsed();
660✔
710
    }
711
    header("Simulation Statistics", 4);
1,542✔
712
    fmt::print(
1,542✔
713
      " Total Iterations                  = {}\n", settings::n_batches);
714
    fmt::print(
1,542✔
715
      " Number of Rays per Iteration      = {}\n", settings::n_particles);
716
    fmt::print(" Inactive Distance                 = {} cm\n",
1,542✔
717
      RandomRay::distance_inactive_);
718
    fmt::print(" Active Distance                   = {} cm\n",
1,542✔
719
      RandomRay::distance_active_);
720
    fmt::print(" Source Regions (SRs)              = {}\n",
1,542✔
721
      RandomRay::n_source_regions_);
722
    fmt::print(" SRs Containing External Sources   = {}\n",
1,542✔
723
      RandomRay::n_external_source_regions_);
724
    fmt::print(" Total Geometric Intersections     = {:.4e}\n",
3,084✔
725
      static_cast<double>(RandomRay::total_geometric_intersections_));
1,542✔
726
    fmt::print("   Avg per Iteration               = {:.4e}\n",
3,084✔
727
      static_cast<double>(RandomRay::total_geometric_intersections_) /
1,542✔
728
        settings::n_batches);
729
    fmt::print("   Avg per Iteration per SR        = {:.2f}\n",
3,084✔
730
      static_cast<double>(RandomRay::total_geometric_intersections_) /
1,542✔
731
        static_cast<double>(settings::n_batches) /
1,542✔
732
        RandomRay::n_source_regions_);
733
    fmt::print(" Avg SR Miss Rate per Iteration    = {:.4f}%\n",
1,542✔
734
      RandomRay::avg_miss_rate_);
735
    fmt::print(" Energy Groups                     = {}\n", negroups_);
1,542✔
736
    if (settings::kinetic_simulation)
1,542✔
737
      fmt::print(" Delay Groups                      = {}\n", ndgroups_);
924✔
738
    fmt::print(
1,542✔
739
      " Total Integrations                = {:.4e}\n", total_integrations);
740
    fmt::print("   Avg per Iteration               = {:.4e}\n",
3,084✔
741
      total_integrations / settings::n_batches);
1,542✔
742

743
    std::string estimator;
1,542!
744
    switch (domain_->volume_estimator_) {
1,542!
745
    case RandomRayVolumeEstimator::SIMULATION_AVERAGED:
22✔
746
      estimator = "Simulation Averaged";
22✔
747
      break;
748
    case RandomRayVolumeEstimator::NAIVE:
68✔
749
      estimator = "Naive";
68✔
750
      break;
751
    case RandomRayVolumeEstimator::HYBRID:
1,452✔
752
      estimator = "Hybrid";
1,452✔
753
      break;
754
    default:
×
755
      fatal_error("Invalid volume estimator type");
×
756
    }
757
    fmt::print(" Volume Estimator Type             = {}\n", estimator);
1,542✔
758

759
    std::string adjoint_true = (FlatSourceDomain::adjoint_) ? "ON" : "OFF";
4,570✔
760
    fmt::print(" Adjoint Flux Mode                 = {}\n", adjoint_true);
1,542✔
761

762
    std::string shape;
3,084!
763
    switch (RandomRay::source_shape_) {
1,542!
764
    case RandomRaySourceShape::FLAT:
1,256✔
765
      shape = "Flat";
1,256✔
766
      break;
767
    case RandomRaySourceShape::LINEAR:
253✔
768
      shape = "Linear";
253✔
769
      break;
770
    case RandomRaySourceShape::LINEAR_XY:
33✔
771
      shape = "Linear XY";
33✔
772
      break;
773
    default:
×
774
      fatal_error("Invalid random ray source shape");
×
775
    }
776
    fmt::print(" Source Shape                      = {}\n", shape);
1,542✔
777
    std::string sample_method;
3,084!
778
    switch (RandomRay::sample_method_) {
1,542!
779
    case RandomRaySampleMethod::PRNG:
1,520✔
780
      sample_method = "PRNG";
1,520✔
781
      break;
782
    case RandomRaySampleMethod::HALTON:
11✔
783
      sample_method = "Halton";
11✔
784
      break;
785
    case RandomRaySampleMethod::S2:
11✔
786
      sample_method = "PRNG S2";
11✔
787
      break;
788
    }
789
    fmt::print(" Sample Method                     = {}\n", sample_method);
1,542✔
790

791
    if (domain_->is_transport_stabilization_needed_) {
1,542✔
792
      fmt::print(" Transport XS Stabilization Used   = YES (rho = {:.3f})\n",
165✔
793
        FlatSourceDomain::diagonal_stabilization_rho_);
794
    } else {
795
      fmt::print(" Transport XS Stabilization Used   = NO\n");
1,377✔
796
    }
797
    if (settings::kinetic_simulation && !simulation::is_initial_condition) {
1,542✔
798
      std::string time_method =
660✔
799
        (RandomRay::time_method_ == RandomRayTimeMethod::ISOTROPIC)
660✔
800
          ? "ISOTROPIC"
801
          : "PROPAGATION";
880✔
802
      fmt::print(" Time Method                       = {}\n", time_method);
660✔
803
      fmt::print(
660✔
804
        " Backwards Difference Order        = {}\n", RandomRay::bd_order_);
805
    }
660✔
806

807
    header("Timing Statistics", 4);
1,542✔
808
    show_time("Total time for initialization", time_initialize.elapsed());
1,542✔
809
    show_time("Reading cross sections", time_read_xs.elapsed(), 1);
1,542✔
810
    show_time("Total simulation time", time_total.elapsed());
1,542✔
811
    show_time("Transport sweep only", time_transport.elapsed(), 1);
1,542✔
812
    show_time("Source update only", time_update_src.elapsed(), 1);
1,542✔
813
    if (settings::kinetic_simulation && settings::create_delayed_neutrons) {
1,542!
814
      show_time(
924✔
815
        "Precursor computation only", time_compute_precursors.elapsed(), 1);
816
      misc_time -= time_compute_precursors.elapsed();
924✔
817
    }
818
    show_time("Tally conversion only", time_tallies.elapsed(), 1);
1,542✔
819
    show_time("MPI source reductions only", time_bank_sendrecv.elapsed(), 1);
1,542✔
820
    show_time("Other iteration routines", misc_time, 1);
1,542✔
821
    if (settings::run_mode == RunMode::EIGENVALUE) {
1,542✔
822
      show_time("Time in inactive batches", time_inactive.elapsed());
1,199✔
823
    }
824
    show_time("Time in active batches", time_active.elapsed());
1,542✔
825
    show_time("Time writing statepoints", time_statepoint.elapsed());
1,542✔
826
    show_time("Total time for finalization", time_finalize.elapsed());
1,542✔
827
    show_time("Time per integration", time_per_integration);
1,542✔
828
  }
1,542✔
829

830
  if (settings::verbosity >= 4 && settings::run_mode == RunMode::EIGENVALUE) {
1,542!
831
    header("Results", 4);
1,199✔
832
    fmt::print(" k-effective                       = {:.5f} +/- {:.5f}\n",
1,199✔
833
      simulation::keff, simulation::keff_std);
834
  }
835
}
1,542✔
836

837
void openmc_finalize_random_ray()
8,654✔
838
{
839
  FlatSourceDomain::volume_estimator_ = RandomRayVolumeEstimator::HYBRID;
8,654✔
840
  FlatSourceDomain::volume_normalized_flux_tallies_ = false;
8,654✔
841
  FlatSourceDomain::adjoint_ = false;
8,654✔
842
  FlatSourceDomain::mesh_domain_map_.clear();
8,654✔
843
  RandomRay::ray_source_.reset();
8,654✔
844
  RandomRay::source_shape_ = RandomRaySourceShape::FLAT;
8,654✔
845
  RandomRay::sample_method_ = RandomRaySampleMethod::PRNG;
8,654✔
846
}
8,654✔
847

848
} // namespace openmc
849

850
//==============================================================================
851
// C API functions
852
//==============================================================================
853

854
void openmc_run_random_ray()
942✔
855
{
856
  //////////////////////////////////////////////////////////
857
  // Run forward simulation
858
  //////////////////////////////////////////////////////////
859
  if (openmc::mpi::master) {
942✔
860
    if (openmc::FlatSourceDomain::adjoint_) {
694✔
861
      openmc::FlatSourceDomain::adjoint_ = false;
56✔
862
      openmc::print_adjoint_header();
56✔
863
      openmc::FlatSourceDomain::adjoint_ = true;
56✔
864
    }
865
  }
866

867
  // Initialize OpenMC general data structures
868
  openmc_simulation_init();
942✔
869

870
  // Validate that inputs meet requirements for random ray mode
871
  if (openmc::mpi::master)
942✔
872
    openmc::validate_random_ray_inputs();
694✔
873

874
  // Initialize Random Ray Simulation Object
875
  openmc::RandomRaySimulation sim;
942✔
876

877
  // Initialize fixed sources, if present
878
  sim.apply_fixed_sources_and_mesh_domains();
942✔
879

880
  // Simulate single random ray simulation (static case)
881
  // OR get an initial estimate for scattering and fission
882
  // distributions (if fissile material exist),
883
  // and k-eff (if kinetic eigenvalue simulation)
884
  sim.simulate();
942✔
885

886
  if (openmc::settings::kinetic_simulation) {
942✔
887
    // Toggle initial condition source correction
888
    openmc::simulation::k_eff_correction = true;
180✔
889
    int i_start = -1;
180✔
890
    // Timestepping loop,
891
    for (int i = i_start; i < openmc::settings::n_timesteps; i++) {
1,260✔
892
      sim.initialize_time_step(i);
1,080✔
893
      sim.simulate();
1,080✔
894
      sim.finalize_time_step();
1,080✔
895
    }
896
  }
897

898
  //////////////////////////////////////////////////////////
899
  // Run adjoint simulation (if enabled)
900
  //////////////////////////////////////////////////////////
901

902
  if (sim.adjoint_needed_) {
942✔
903
    // Setup for adjoint simulation
904
    sim.prepare_adjoint_simulation();
76✔
905

906
    // Run adjoint simulation
907
    sim.simulate();
76✔
908
  }
909
}
942✔
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