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

openmc-dev / openmc / 20738425414

06 Jan 2026 04:51AM UTC coverage: 81.961% (-0.2%) from 82.154%
20738425414

Pull #3702

github

web-flow
Merge 8f77d8b3e into 60ddafa9b
Pull Request #3702: Random Ray Kinetic Simulation Mode

17517 of 24332 branches covered (71.99%)

Branch coverage included in aggregate %.

907 of 1117 new or added lines in 20 files covered. (81.2%)

103 existing lines in 8 files now uncovered.

55908 of 65253 relevant lines covered (85.68%)

50718541.16 hits per line

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

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

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

19
namespace openmc {
20

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

25
void openmc_run_random_ray()
753✔
26
{
27
  //////////////////////////////////////////////////////////
28
  // Run forward simulation
29
  //////////////////////////////////////////////////////////
30

31
  if (mpi::master) {
753✔
32
    bool adjoint_needed = FlatSourceDomain::adjoint_;
518✔
33
    print_random_ray_headers(adjoint_needed);
518✔
34
  }
35

36
  // Initialize OpenMC general data structures
37
  openmc_simulation_init();
753✔
38

39
  // Validate that inputs meet requirements for random ray mode
40
  if (mpi::master)
753✔
41
    validate_random_ray_inputs();
518✔
42

43
  // Initialize Random Ray Simulation Object
44
  RandomRaySimulation sim;
753✔
45

46
  // Initialize fixed sources, if present
47
  sim.apply_fixed_sources_and_mesh_domains();
753✔
48

49
  // Run initial random ray simulation
50
  sim.run_single_simulation();
753✔
51

52
  if (settings::kinetic_simulation) {
753✔
53
    // Second steady state simulation to correct the batchwise k-eff
54
    sim.kinetic_initial_condition();
112✔
55

56
    warning(
112✔
57
      "Time-dependent explicit void treatment has not yet been "
58
      "implemented. Use caution when interpreting results from models with "
59
      "voids, as they may contain large inaccuracies.");
60
    // Timestepping loop
61
    for (int i = 0; i < settings::n_timesteps; i++)
672✔
62
      sim.kinetic_single_time_step(i);
560✔
63
  }
64

65
  //////////////////////////////////////////////////////////
66
  // Run adjoint simulation (if enabled)
67
  //////////////////////////////////////////////////////////
68

69
  sim.random_ray_adjoint();
753✔
70
}
753✔
71

72
// Enforces restrictions on inputs in random ray mode.  While there are
73
// many features that don't make sense in random ray mode, and are therefore
74
// unsupported, we limit our testing/enforcement operations only to inputs
75
// that may cause erroneous/misleading output or crashes from the solver.
76
void validate_random_ray_inputs()
518✔
77
{
78
  // Validate tallies
79
  ///////////////////////////////////////////////////////////////////
80
  for (auto& tally : model::tallies) {
1,465✔
81

82
    // Validate score types
83
    for (auto score_bin : tally->scores_) {
2,136✔
84
      switch (score_bin) {
1,189!
85
      case SCORE_FLUX:
1,156✔
86
      case SCORE_TOTAL:
87
      case SCORE_FISSION:
88
      case SCORE_NU_FISSION:
89
      case SCORE_EVENTS:
90
        break;
1,156✔
91

92
      // TODO: add support for prompt and delayed fission
93
      case SCORE_PRECURSORS: {
33✔
94
        if (settings::kinetic_simulation && settings::create_delayed_neutrons) {
33!
95
          break;
96
        } else {
NEW
97
          fatal_error("Invalid score specified in tallies.xml. Precursors can "
×
98
                      "only be scored for kinetic simulations.");
99
        }
100
      }
101
      default:
102
        fatal_error(
×
103
          "Invalid score specified. Only flux, total, fission, nu-fission, and "
104
          "event scores are supported in random ray mode. (precursors are "
105
          "supported for "
106
          "kinetic simulations when delayed neutrons are turned on).");
107
      }
108
    }
109

110
    // Validate filter types
111
    for (auto f : tally->filters()) {
2,050✔
112
      auto& filter = *model::tally_filters[f];
1,103✔
113

114
      switch (filter.type()) {
1,103!
115
      case FilterType::CELL:
1,070✔
116
      case FilterType::CELL_INSTANCE:
117
      case FilterType::DISTRIBCELL:
118
      case FilterType::ENERGY:
119
      case FilterType::MATERIAL:
120
      case FilterType::MESH:
121
      case FilterType::UNIVERSE:
122
      case FilterType::PARTICLE:
123
        break;
1,070✔
124
      case FilterType::DELAYED_GROUP:
33✔
125
        if (settings::kinetic_simulation) {
33!
126
          break;
33✔
127
        } else {
NEW
128
          fatal_error("Invalid filter specified in tallies.xml. Kinetic "
×
129
                      "simulations is required "
130
                      "to tally with a delayed_group filter.");
131
        }
132
      default:
×
133
        fatal_error("Invalid filter specified. Only cell, cell_instance, "
×
134
                    "distribcell, energy, material, mesh, and universe filters "
135
                    "are supported in random ray mode (delayed_group is "
136
                    "supported for kinetic simulations).");
137
      }
138
    }
139
  }
140

141
  // TODO: validate kinetic data is present
142
  //  Validate MGXS data
143
  ///////////////////////////////////////////////////////////////////
144
  for (auto& material : data::mg.macro_xs_) {
1,938✔
145
    if (!material.is_isotropic) {
1,420!
146
      fatal_error("Anisotropic MGXS detected. Only isotropic XS data sets "
×
147
                  "supported in random ray mode.");
148
    }
149
    if (material.get_xsdata().size() > 1) {
1,420!
150
      warning("Non-isothermal MGXS detected. Only isothermal XS data sets "
×
151
              "supported in random ray mode. Using lowest temperature.");
152
    }
153
    for (int g = 0; g < data::mg.num_energy_groups_; g++) {
10,035✔
154
      if (material.exists_in_model) {
8,615✔
155
        // Temperature and angle indices, if using multiple temperature
156
        // data sets and/or anisotropic data sets.
157
        // TODO: Currently assumes we are only using single temp/single angle
158
        // data.
159
        const int t = 0;
8,571✔
160
        const int a = 0;
8,571✔
161
        double sigma_t =
162
          material.get_xs(MgxsType::TOTAL, g, NULL, NULL, NULL, t, a);
8,571✔
163
        if (sigma_t <= 0.0) {
8,571!
164
          fatal_error("No zero or negative total macroscopic cross sections "
×
165
                      "allowed in random ray mode. If the intention is to make "
166
                      "a void material, use a cell fill of 'None' instead.");
167
        }
168
      }
169
    }
170
  }
171

172
  // Validate ray source
173
  ///////////////////////////////////////////////////////////////////
174

175
  // Check for independent source
176
  IndependentSource* is =
177
    dynamic_cast<IndependentSource*>(RandomRay::ray_source_.get());
518!
178
  if (!is) {
518!
179
    fatal_error("Invalid ray source definition. Ray source must provided and "
×
180
                "be of type IndependentSource.");
181
  }
182

183
  // Check for box source
184
  SpatialDistribution* space_dist = is->space();
518✔
185
  SpatialBox* sb = dynamic_cast<SpatialBox*>(space_dist);
518!
186
  if (!sb) {
518!
187
    fatal_error(
×
188
      "Invalid ray source definition -- only box sources are allowed.");
189
  }
190

191
  // Check that box source is not restricted to fissionable areas
192
  if (sb->only_fissionable()) {
518!
193
    fatal_error(
×
194
      "Invalid ray source definition -- fissionable spatial distribution "
195
      "not allowed.");
196
  }
197

198
  // Check for isotropic source
199
  UnitSphereDistribution* angle_dist = is->angle();
518✔
200
  Isotropic* id = dynamic_cast<Isotropic*>(angle_dist);
518!
201
  if (!id) {
518!
202
    fatal_error("Invalid ray source definition -- only isotropic sources are "
×
203
                "allowed.");
204
  }
205

206
  // Validate external sources
207
  ///////////////////////////////////////////////////////////////////
208
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
518✔
209
    if (model::external_sources.size() < 1) {
276!
210
      fatal_error("Must provide a particle source (in addition to ray source) "
×
211
                  "in fixed source random ray mode.");
212
    }
213

214
    for (int i = 0; i < model::external_sources.size(); i++) {
552✔
215
      Source* s = model::external_sources[i].get();
276✔
216

217
      // Check for independent source
218
      IndependentSource* is = dynamic_cast<IndependentSource*>(s);
276!
219

220
      if (!is) {
276!
221
        fatal_error(
×
222
          "Only IndependentSource external source types are allowed in "
223
          "random ray mode");
224
      }
225

226
      // Check for isotropic source
227
      UnitSphereDistribution* angle_dist = is->angle();
276✔
228
      Isotropic* id = dynamic_cast<Isotropic*>(angle_dist);
276!
229
      if (!id) {
276!
230
        fatal_error(
×
231
          "Invalid source definition -- only isotropic external sources are "
232
          "allowed in random ray mode.");
233
      }
234

235
      // Validate that a domain ID was specified OR that it is a point source
236
      auto sp = dynamic_cast<SpatialPoint*>(is->space());
276!
237
      if (is->domain_ids().size() == 0 && !sp) {
276!
238
        fatal_error("Fixed sources must be point source or spatially "
×
239
                    "constrained by domain id (cell, material, or universe) in "
240
                    "random ray mode.");
241
      } else if (is->domain_ids().size() > 0 && sp) {
276✔
242
        // If both a domain constraint and a non-default point source location
243
        // are specified, notify user that domain constraint takes precedence.
244
        if (sp->r().x == 0.0 && sp->r().y == 0.0 && sp->r().z == 0.0) {
242!
245
          warning("Fixed source has both a domain constraint and a point "
242✔
246
                  "type spatial distribution. The domain constraint takes "
247
                  "precedence in random ray mode -- point source coordinate "
248
                  "will be ignored.");
249
        }
250
      }
251

252
      // Check that a discrete energy distribution was used
253
      Distribution* d = is->energy();
276✔
254
      Discrete* dd = dynamic_cast<Discrete*>(d);
276!
255
      if (!dd) {
276!
256
        fatal_error(
×
257
          "Only discrete (multigroup) energy distributions are allowed for "
258
          "external sources in random ray mode.");
259
      }
260
    }
261
  }
262

263
  // Validate plotting files
264
  ///////////////////////////////////////////////////////////////////
265
  for (int p = 0; p < model::plots.size(); p++) {
518!
266

267
    // Get handle to OpenMC plot object
268
    const auto& openmc_plottable = model::plots[p];
×
269
    Plot* openmc_plot = dynamic_cast<Plot*>(openmc_plottable.get());
×
270

271
    // Random ray plots only support voxel plots
272
    if (!openmc_plot) {
×
273
      warning(fmt::format(
×
274
        "Plot {} will not be used for end of simulation data plotting -- only "
275
        "voxel plotting is allowed in random ray mode.",
276
        openmc_plottable->id()));
×
277
      continue;
×
278
    } else if (openmc_plot->type_ != Plot::PlotType::voxel) {
×
279
      warning(fmt::format(
×
280
        "Plot {} will not be used for end of simulation data plotting -- only "
281
        "voxel plotting is allowed in random ray mode.",
282
        openmc_plottable->id()));
×
283
      continue;
×
284
    }
285
  }
286

287
  // Warn about slow MPI domain replication, if detected
288
  ///////////////////////////////////////////////////////////////////
289
#ifdef OPENMC_MPI
290
  if (mpi::n_procs > 1) {
236✔
291
    warning(
235✔
292
      "MPI parallelism is not supported by the random ray solver. All work "
293
      "will be performed by rank 0. Domain decomposition may be implemented in "
294
      "the future to provide efficient MPI scaling.");
295
  }
296
#endif
297

298
  // Warn about instability resulting from linear sources in small regions
299
  // when generating weight windows with FW-CADIS and an overlaid mesh.
300
  ///////////////////////////////////////////////////////////////////
301
  if (RandomRay::source_shape_ == RandomRaySourceShape::LINEAR &&
694✔
302
      variance_reduction::weight_windows.size() > 0) {
176✔
303
    warning(
11✔
304
      "Linear sources may result in negative fluxes in small source regions "
305
      "generated by mesh subdivision. Negative sources may result in low "
306
      "quality FW-CADIS weight windows. We recommend you use flat source mode "
307
      "when generating weight windows with an overlaid mesh tally.");
308
  }
309
}
518✔
310

311
void openmc_reset_random_ray()
7,901✔
312
{
313
  FlatSourceDomain::volume_estimator_ = RandomRayVolumeEstimator::HYBRID;
7,901✔
314
  FlatSourceDomain::volume_normalized_flux_tallies_ = false;
7,901✔
315
  FlatSourceDomain::adjoint_ = false;
7,901✔
316
  FlatSourceDomain::mesh_domain_map_.clear();
7,901✔
317
  RandomRay::ray_source_.reset();
7,901✔
318
  RandomRay::source_shape_ = RandomRaySourceShape::FLAT;
7,901✔
319
  RandomRay::sample_method_ = RandomRaySampleMethod::PRNG;
7,901✔
320
  RandomRay::bd_order_ = 3;
7,901✔
321
  RandomRay::time_method_ = RandomRayTimeMethod::ISOTROPIC;
7,901✔
322
}
7,901✔
323

324
void write_random_ray_hdf5(hid_t group)
1,036✔
325
{
326
  hid_t random_ray_group = create_group(group, "random_ray");
1,036✔
327
  switch (RandomRay::source_shape_) {
1,036!
328
  case RandomRaySourceShape::FLAT:
816✔
329
    write_dataset(random_ray_group, "source_shape", "flat");
816✔
330
    break;
816✔
331
  case RandomRaySourceShape::LINEAR:
187✔
332
    write_dataset(random_ray_group, "source_shape", "linear");
187✔
333
    break;
187✔
334
  case RandomRaySourceShape::LINEAR_XY:
33✔
335
    write_dataset(random_ray_group, "source_shape", "linear xy");
33✔
336
    break;
33✔
NEW
337
  default:
×
NEW
338
    break;
×
339
  }
340

341
  switch (FlatSourceDomain::volume_estimator_) {
1,036!
342
  case RandomRayVolumeEstimator::SIMULATION_AVERAGED:
22✔
343
    write_dataset(random_ray_group, "volume_estimator", "simulation averaged");
22✔
344
    break;
22✔
345
  case RandomRayVolumeEstimator::NAIVE:
68✔
346
    write_dataset(random_ray_group, "volume_estimator", "naive");
68✔
347
    break;
68✔
348
  case RandomRayVolumeEstimator::HYBRID:
946✔
349
    write_dataset(random_ray_group, "volume_estimator", "hybrid");
946✔
350
    break;
946✔
NEW
351
  default:
×
NEW
352
    break;
×
353
  }
354

355
  write_dataset(
1,036✔
356
    random_ray_group, "distance_active", RandomRay::distance_active_);
357
  write_dataset(
1,036✔
358
    random_ray_group, "distance_inactive", RandomRay::distance_inactive_);
359
  write_dataset(random_ray_group, "volume_normalized_flux_tallies",
1,036✔
360
    FlatSourceDomain::volume_normalized_flux_tallies_);
361
  write_dataset(random_ray_group, "adjoint_mode", FlatSourceDomain::adjoint_);
1,036✔
362

363
  write_dataset(random_ray_group, "avg_miss_rate", RandomRay::avg_miss_rate_);
1,036✔
364
  write_dataset(
1,036✔
365
    random_ray_group, "n_source_regions", RandomRay::n_source_regions_);
366
  write_dataset(random_ray_group, "n_external_source_regions",
1,036✔
367
    RandomRay::n_external_source_regions_);
368
  write_dataset(random_ray_group, "n_geometric_intersections",
1,036✔
369
    RandomRay::total_geometric_intersections_);
370
  int64_t n_integrations =
1,036✔
371
    RandomRay::total_geometric_intersections_ * data::mg.num_energy_groups_;
1,036✔
372
  write_dataset(random_ray_group, "n_integrations", n_integrations);
1,036✔
373

374
  if (settings::kinetic_simulation && !simulation::is_initial_condition) {
1,036✔
375
    write_dataset(random_ray_group, "bd_order", RandomRay::bd_order_);
385✔
376
    switch (RandomRay::time_method_) {
385!
377
    case RandomRayTimeMethod::ISOTROPIC:
385✔
378
      write_dataset(random_ray_group, "time_method", "isotropic");
385✔
379
      break;
385✔
NEW
380
    case RandomRayTimeMethod::PROPAGATION:
×
NEW
381
      write_dataset(random_ray_group, "time_method", "propogation");
×
NEW
382
      break;
×
NEW
383
    default:
×
NEW
384
      break;
×
385
    }
386
  }
387
  close_group(random_ray_group);
1,036✔
388
}
1,036✔
389

390
void print_random_ray_headers(bool& adjoint_needed)
1,036✔
391
{
392
  // If we're going to do an adjoint simulation afterwards, report that this is
393
  // the initial forward flux solve.
394
  if (adjoint_needed && !FlatSourceDomain::adjoint_)
1,036!
NEW
395
    header("FORWARD FLUX SOLVE", 3);
×
396

397
  // Otherwise report that we are doing the adjoint simulation
398
  if (adjoint_needed && FlatSourceDomain::adjoint_)
1,036!
399
    header("ADJOINT FLUX SOLVE", 3);
112✔
400
}
1,036✔
401

402
//-----------------------------------------------------------------------------
403
// Non-member functions for kinetic simulations
404

405
void rename_time_step_file(
1,344✔
406
  std::string base_filename, std::string extension, int i)
407
{
408
  // Rename file
409
  std::string old_filename_ =
410
    fmt::format("{0}{1}{2}", settings::path_output, base_filename, extension);
1,344✔
411
  std::string new_filename_ = fmt::format(
412
    "{0}{1}_{2}{3}", settings::path_output, base_filename, i, extension);
1,092✔
413

414
  const char* old_fname = old_filename_.c_str();
1,344✔
415
  const char* new_fname = new_filename_.c_str();
1,344✔
416
  std::rename(old_fname, new_fname);
1,344✔
417
}
1,344✔
418

419
//==============================================================================
420
// RandomRaySimulation implementation
421
//==============================================================================
422

423
RandomRaySimulation::RandomRaySimulation()
753✔
424
  : negroups_(data::mg.num_energy_groups_),
753✔
425
    ndgroups_(data::mg.num_delayed_groups_)
753✔
426
{
427
  // There are no source sites in random ray mode, so be sure to disable to
428
  // ensure we don't attempt to write source sites to statepoint
429
  settings::source_write = false;
753✔
430

431
  // Random ray mode does not have an inner loop over generations within a
432
  // batch, so set the current gen to 1
433
  simulation::current_gen = 1;
753✔
434

435
  switch (RandomRay::source_shape_) {
753!
436
  case RandomRaySourceShape::FLAT:
449✔
437
    domain_ = make_unique<FlatSourceDomain>();
449✔
438
    break;
449✔
439
  case RandomRaySourceShape::LINEAR:
304✔
440
  case RandomRaySourceShape::LINEAR_XY:
441
    domain_ = make_unique<LinearSourceDomain>();
304✔
442
    break;
304✔
443
  default:
×
444
    fatal_error("Unknown random ray source shape");
×
445
  }
446

447
  // Convert OpenMC native MGXS into a more efficient format
448
  // internal to the random ray solver
449
  domain_->flatten_xs();
753✔
450

451
  // Check if adjoint calculation is needed. If it is, we will run the forward
452
  // calculation first and then the adjoint calculation later.
453
  adjoint_needed_ = FlatSourceDomain::adjoint_;
753✔
454

455
  // Adjoint is always false for the forward calculation
456
  FlatSourceDomain::adjoint_ = false;
753✔
457

458
  // The first simulation is run after initialization
459
  is_first_simulation_ = true;
753✔
460

461
  // Initialize vectors used for baking in the initial condition during time
462
  // stepping
463
  if (settings::kinetic_simulation) {
753✔
464
    // Initialize vars used for time-consistent seed approach
465
    static_avg_k_eff_;
466
    static_k_eff_;
467
    static_fission_rate_;
468
  }
469
}
753✔
470

471
void RandomRaySimulation::apply_fixed_sources_and_mesh_domains()
753✔
472
{
473
  domain_->apply_meshes();
753✔
474
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
753✔
475
    // Transfer external source user inputs onto random ray source regions
476
    domain_->convert_external_sources();
401✔
477
    domain_->count_external_source_regions();
401✔
478
  }
479
}
753✔
480

481
void RandomRaySimulation::prepare_fixed_sources_adjoint()
81✔
482
{
483
  domain_->source_regions_.adjoint_reset();
81✔
484
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
81✔
485
    domain_->set_adjoint_sources();
65✔
486
  }
487
}
81✔
488

489
void RandomRaySimulation::print_random_ray_headers()
518✔
490
{
491
  openmc::print_random_ray_headers(adjoint_needed_);
518✔
492
}
518✔
493

494
void RandomRaySimulation::run_single_simulation()
1,506✔
495
{
496
  if (!is_first_simulation_) {
1,506✔
497
    if (mpi::master)
753✔
498
      print_random_ray_headers();
518✔
499

500
    // Reset the timers and reinitialize the general OpenMC datastructures if
501
    // this is after the first simulation
502
    reset_timers();
753✔
503

504
    // Initialize OpenMC general data structures
505
    openmc_simulation_init();
753✔
506
  }
507

508
  // Begin main simulation timer
509
  simulation::time_total.start();
1,506✔
510

511
  // Execute random ray simulation
512
  simulate();
1,506✔
513

514
  // End main simulation timer
515
  simulation::time_total.stop();
1,506✔
516

517
  // Normalize and save the final forward quantities
518
  domain_->normalize_final_quantities();
1,506✔
519

520
  // Finalize OpenMC
521
  openmc_simulation_finalize();
1,506✔
522

523
  // Output all simulation results
524
  output_simulation_results();
1,506✔
525

526
  // Toggle that the simulation object has been initialized after the first
527
  // simulation
528
  if (is_first_simulation_)
1,506✔
529
    is_first_simulation_ = false;
753✔
530
}
1,506✔
531

532
void RandomRaySimulation::random_ray_adjoint()
753✔
533
{
534
  if (!adjoint_needed_) {
753✔
535
    return;
672✔
536
  }
537

538
  // Configure the domain for adjoint simulation
539
  FlatSourceDomain::adjoint_ = true;
81✔
540

541
  // Reset k-eff
542
  domain_->k_eff_ = 1.0;
81✔
543

544
  // Initialize adjoint fixed sources, if present
545
  prepare_fixed_sources_adjoint();
81✔
546

547
  // Transpose scattering matrix
548
  domain_->transpose_scattering_matrix();
81✔
549

550
  // Swap nu_sigma_f and chi
551
  domain_->nu_sigma_f_.swap(domain_->chi_);
81✔
552

553
  // Run a single simulation
554
  run_single_simulation();
81✔
555
}
556

557
void RandomRaySimulation::kinetic_initial_condition()
112✔
558
{
559
  // Set flag for k_eff correction
560
  simulation::k_eff_correction = true;
112✔
561

562
  static_avg_k_eff_ = simulation::keff;
112✔
563
  domain_->k_eff_ = static_avg_k_eff_;
112✔
564
  domain_->source_regions_.adjoint_reset();
112✔
565
  domain_->propagate_final_quantities();
112✔
566
  domain_->source_regions_.time_step_reset();
112✔
567

568
  // Run the initial condition
569
  run_single_simulation();
112✔
570

571
  // Initialize the BD arrays
572
  domain_->store_time_step_quantities(false);
112✔
573

574
  // Store k-eff corrected initial condition statepoints
575
  rename_time_step_file(
315✔
576
    fmt::format("statepoint.{0}", settings::n_batches), ".h5", 0);
203✔
577
  if (settings::output_tallies) {
112!
578
    rename_time_step_file("tallies", ".out", 0);
112✔
579
  }
580

581
  // Set flags for kinetic simulation
582
  simulation::is_initial_condition = false;
112✔
583
  simulation::k_eff_correction = false;
112✔
584

585
  // Set starting time as zero
586
  simulation::current_time = 0;
112✔
587
}
112✔
588

589
// TODO: Add support for time-dependent restart
590
void RandomRaySimulation::kinetic_single_time_step(int i)
560✔
591
{
592
  // Increment time step
593
  simulation::current_timestep = i + 1;
560✔
594
  simulation::current_time += settings::dt;
560✔
595

596
  // Propogate results of previous simulation
597
  domain_->k_eff_ = static_avg_k_eff_;
560✔
598
  domain_->source_regions_.adjoint_reset();
560✔
599
  domain_->propagate_final_quantities();
560✔
600
  domain_->source_regions_.time_step_reset();
560✔
601

602
  // Compute RHS backward differences
603
  domain_->compute_rhs_bd_quantities();
560✔
604

605
  // Update time dependent cross section based on the density
606
  domain_->update_material_density(i);
560✔
607

608
  // Run the simulation for the current time step
609
  run_single_simulation();
560✔
610

611
  // Rename statepoint and tallies file for the current time step
612
  rename_time_step_file(fmt::format("statepoint.{0}", settings::n_batches),
1,120✔
613
    ".h5", simulation::current_timestep);
614
  if (settings::output_tallies) {
560!
615
    rename_time_step_file("tallies", ".out", simulation::current_timestep);
560✔
616
  }
617

618
  // Store final quantities for the current time step
619
  domain_->store_time_step_quantities();
560✔
620
}
560✔
621

622
void RandomRaySimulation::simulate()
1,506✔
623
{
624
  // Random ray power iteration loop
625
  while (simulation::current_batch < settings::n_batches) {
159,118✔
626
    // Initialize the current batch
627
    initialize_batch();
157,612✔
628
    initialize_generation();
157,612✔
629

630
    // MPI not supported in random ray solver, so all work is done by rank 0
631
    // TODO: Implement domain decomposition for MPI parallelism
632
    if (mpi::master) {
157,612✔
633

634
      // Reset total starting particle weight used for normalizing tallies
635
      simulation::total_weight = 1.0;
108,362✔
636

637
      // Update source term (scattering + fission (+ delayed if kinetic))
638
      domain_->update_all_neutron_sources();
108,362✔
639

640
      // Reset scalar fluxes, iteration volume tallies, and region hit flags
641
      // to zero
642
      domain_->batch_reset();
108,362✔
643

644
      // At the beginning of the simulation, if mesh subdivision is in use, we
645
      // need to swap the main source region container into the base container,
646
      // as the main source region container will be used to hold the true
647
      // subdivided source regions. The base container will therefore only
648
      // contain the external source region information, the mesh indices,
649
      // material properties, and initial guess values for the flux/source.
650

651
      // Start timer for transport
652
      simulation::time_transport.start();
108,362✔
653

654
// Transport sweep over all random rays for the iteration
655
#pragma omp parallel for schedule(dynamic)                                     \
59,112✔
656
  reduction(+ : total_geometric_intersections_)
59,112✔
657
      for (int i = 0; i < settings::n_particles; i++) {
5,292,750✔
658
        RandomRay ray(i, domain_.get());
5,243,500✔
659
        total_geometric_intersections_ +=
5,243,500✔
660
          ray.transport_history_based_single_ray();
5,243,500✔
661
      }
5,243,500✔
662

663
      simulation::time_transport.stop();
108,362✔
664

665
      // Add any newly discovered source regions to the main source region
666
      // container.
667
      domain_->finalize_discovered_source_regions();
108,362✔
668

669
      // Normalize scalar flux and update volumes
670
      domain_->normalize_scalar_flux_and_volumes(
108,362✔
671
        settings::n_particles * RandomRay::distance_active_);
672

673
      // Add source to scalar flux, compute number of FSR hits
674
      int64_t n_hits = domain_->add_source_to_scalar_flux();
108,362✔
675

676
      // Apply transport stabilization factors
677
      domain_->apply_transport_stabilization();
108,362✔
678

679
      if (settings::run_mode == RunMode::EIGENVALUE) {
108,362✔
680
        // Compute random ray k-eff
681
        if (!settings::kinetic_simulation ||
98,780!
682
            settings::kinetic_simulation && simulation::is_initial_condition) {
96,250✔
683
          domain_->compute_k_eff();
30,030✔
684
          if (simulation::k_eff_correction) {
30,030✔
685
            static_fission_rate_.push_back(domain_->fission_rate_);
13,750✔
686
            static_k_eff_.push_back(domain_->k_eff_);
13,750✔
687
          }
688
        } else {
689
          domain_->k_eff_ = static_k_eff_[simulation::current_batch - 1];
68,750✔
690
          domain_->fission_rate_ =
68,750✔
691
            static_fission_rate_[simulation::current_batch - 1];
68,750✔
692
        }
693

694
        // Store random ray k-eff into OpenMC's native k-eff variable
695
        global_tally_tracklength = domain_->k_eff_;
98,780✔
696
      }
697

698
      // Compute precursors if delayed neutrons are turned on
699
      if (settings::kinetic_simulation && settings::create_delayed_neutrons)
108,362!
700
        domain_->compute_all_precursors();
96,250✔
701

702
      // Execute all tallying tasks, if this is an active batch
703
      if (simulation::current_batch > settings::n_inactive) {
108,362✔
704

705
        // Add this iteration's scalar flux estimate to final accumulated
706
        // estimate
707
        domain_->accumulate_iteration_quantities();
52,696✔
708

709
        // Use above mapping to contribute FSR flux data to appropriate
710
        // tallies
711
        domain_->random_ray_tally();
52,696✔
712
      }
713

714
      // Set phi_old = phi_new
715
      domain_->flux_swap();
108,362✔
716
      if (settings::kinetic_simulation && settings::create_delayed_neutrons) {
108,362!
717
        domain_->precursors_swap();
96,250✔
718
      }
719

720
      // Check for any obvious insabilities/nans/infs
721
      instability_check(n_hits, domain_->k_eff_, avg_miss_rate_);
108,362✔
722
    } // End MPI master work
723

724
    // Store simulation metrics
725
    RandomRay::avg_miss_rate_ = avg_miss_rate_ / settings::n_batches;
157,612✔
726
    RandomRay::total_geometric_intersections_ = total_geometric_intersections_;
157,612✔
727
    RandomRay::n_external_source_regions_ = domain_->n_external_source_regions_;
157,612✔
728
    RandomRay::n_source_regions_ = domain_->n_source_regions();
157,612✔
729

730
    // Finalize the current batch
731
    finalize_generation();
157,612✔
732
    finalize_batch();
157,612✔
733
  } // End random ray power iteration loop
734

735
  domain_->count_external_source_regions();
1,506✔
736
}
1,506✔
737

738
void RandomRaySimulation::output_simulation_results() const
1,506✔
739
{
740
  // Print random ray results
741
  if (mpi::master) {
1,506✔
742
    print_results_random_ray();
1,036✔
743
    if (model::plots.size() > 0) {
1,036!
744
      domain_->output_to_vtk();
×
745
    }
746
  }
747
}
1,506✔
748

749
// Apply a few sanity checks to catch obvious cases of numerical instability.
750
// Instability typically only occurs if ray density is extremely low.
751
void RandomRaySimulation::instability_check(
108,362✔
752
  int64_t n_hits, double k_eff, double& avg_miss_rate) const
753
{
754
  double percent_missed = ((domain_->n_source_regions() - n_hits) /
108,362✔
755
                            static_cast<double>(domain_->n_source_regions())) *
108,362✔
756
                          100.0;
108,362✔
757
  avg_miss_rate += percent_missed;
108,362✔
758

759
  if (mpi::master) {
108,362!
760
    if (percent_missed > 10.0) {
108,362✔
761
      warning(fmt::format(
957✔
762
        "Very high FSR miss rate detected ({:.3f}%). Instability may occur. "
763
        "Increase ray density by adding more rays and/or active distance.",
764
        percent_missed));
765
    } else if (percent_missed > 1.0) {
107,405✔
766
      warning(
2!
767
        fmt::format("Elevated FSR miss rate detected ({:.3f}%). Increasing "
4!
768
                    "ray density by adding more rays and/or active "
769
                    "distance may improve simulation efficiency.",
770
          percent_missed));
771
    }
772

773
    if (k_eff > 10.0 || k_eff < 0.01 || !(std::isfinite(k_eff))) {
108,362!
774
      fatal_error(fmt::format("Instability detected: k-eff = {:.5f}", k_eff));
×
775
    }
776
  }
777
}
108,362✔
778

779
// Print random ray simulation results
780
void RandomRaySimulation::print_results_random_ray() const
1,036✔
781
{
782
  using namespace simulation;
783

784
  if (settings::verbosity >= 6) {
1,036!
785
    double total_integrations =
1,036✔
786
      RandomRay::total_geometric_intersections_ * negroups_;
1,036✔
787
    double time_per_integration =
788
      simulation::time_transport.elapsed() / total_integrations;
1,036✔
789
    double misc_time = time_total.elapsed() - time_update_src.elapsed() -
1,036✔
790
                       time_transport.elapsed() - time_tallies.elapsed() -
1,036✔
791
                       time_bank_sendrecv.elapsed();
1,036✔
792

793
    if (settings::kinetic_simulation && !simulation::is_initial_condition) {
1,036✔
794
      misc_time -= time_update_bd_vectors.elapsed();
385✔
795
    }
796
    header("Simulation Statistics", 4);
1,036✔
797
    fmt::print(
1,036✔
798
      " Total Iterations                  = {}\n", settings::n_batches);
799
    fmt::print(
1,036✔
800
      " Number of Rays per Iteration      = {}\n", settings::n_particles);
801
    fmt::print(" Inactive Distance                 = {} cm\n",
1,036✔
802
      RandomRay::distance_inactive_);
803
    fmt::print(" Active Distance                   = {} cm\n",
1,036✔
804
      RandomRay::distance_active_);
805
    fmt::print(" Source Regions (SRs)              = {}\n",
1,036✔
806
      RandomRay::n_source_regions_);
807
    fmt::print(" SRs Containing External Sources   = {}\n",
848✔
808
      RandomRay::n_external_source_regions_);
809
    fmt::print(" Total Geometric Intersections     = {:.4e}\n",
848✔
810
      static_cast<double>(RandomRay::total_geometric_intersections_));
1,036✔
811
    fmt::print("   Avg per Iteration               = {:.4e}\n",
848✔
812
      static_cast<double>(RandomRay::total_geometric_intersections_) /
1,036✔
813
        settings::n_batches);
814
    fmt::print("   Avg per Iteration per SR        = {:.2f}\n",
848✔
815
      static_cast<double>(RandomRay::total_geometric_intersections_) /
1,036✔
816
        static_cast<double>(settings::n_batches) /
2,072✔
817
        RandomRay::n_source_regions_);
818
    fmt::print(" Avg SR Miss Rate per Iteration    = {:.4f}%\n",
848✔
819
      RandomRay::avg_miss_rate_);
820
    fmt::print(" Energy Groups                     = {}\n", negroups_);
1,884✔
821
    if (settings::kinetic_simulation)
1,036✔
822
      fmt::print(" Delay Groups                      = {}\n", ndgroups_);
1,078✔
823
    fmt::print(
848✔
824
      " Total Integrations                = {:.4e}\n", total_integrations);
825
    fmt::print("   Avg per Iteration               = {:.4e}\n",
848✔
826
      total_integrations / settings::n_batches);
1,036✔
827

828
    std::string estimator;
1,036✔
829
    switch (domain_->volume_estimator_) {
1,036!
830
    case RandomRayVolumeEstimator::SIMULATION_AVERAGED:
22✔
831
      estimator = "Simulation Averaged";
22✔
832
      break;
22✔
833
    case RandomRayVolumeEstimator::NAIVE:
68✔
834
      estimator = "Naive";
68✔
835
      break;
68✔
836
    case RandomRayVolumeEstimator::HYBRID:
946✔
837
      estimator = "Hybrid";
946✔
838
      break;
946✔
839
    default:
×
840
      fatal_error("Invalid volume estimator type");
×
841
    }
842
    fmt::print(" Volume Estimator Type             = {}\n", estimator);
848✔
843

844
    std::string adjoint_true = (FlatSourceDomain::adjoint_) ? "ON" : "OFF";
3,108✔
845
    fmt::print(" Adjoint Flux Mode                 = {}\n", adjoint_true);
848✔
846

847
    std::string shape;
2,072✔
848
    switch (RandomRay::source_shape_) {
1,036!
849
    case RandomRaySourceShape::FLAT:
816✔
850
      shape = "Flat";
816✔
851
      break;
816✔
852
    case RandomRaySourceShape::LINEAR:
187✔
853
      shape = "Linear";
187✔
854
      break;
187✔
855
    case RandomRaySourceShape::LINEAR_XY:
33✔
856
      shape = "Linear XY";
33✔
857
      break;
33✔
858
    default:
×
859
      fatal_error("Invalid random ray source shape");
×
860
    }
861
    fmt::print(" Source Shape                      = {}\n", shape);
848✔
862
    std::string sample_method =
863
      (RandomRay::sample_method_ == RandomRaySampleMethod::PRNG) ? "PRNG"
1,036✔
864
                                                                 : "Halton";
3,108✔
865
    fmt::print(" Sample Method                     = {}\n", sample_method);
848✔
866

867
    if (domain_->is_transport_stabilization_needed_) {
1,036✔
868
      fmt::print(" Transport XS Stabilization Used   = YES (rho = {:.3f})\n",
88✔
869
        FlatSourceDomain::diagonal_stabilization_rho_);
870
    } else {
871
      fmt::print(" Transport XS Stabilization Used   = NO\n");
948✔
872
    }
873
    if (settings::kinetic_simulation && !simulation::is_initial_condition) {
1,036✔
874
      std::string time_method =
875
        (RandomRay::time_method_ == RandomRayTimeMethod::ISOTROPIC)
385✔
876
          ? "ISOTROPIC"
877
          : "PROPAGATION";
770!
878
      fmt::print(" Time Method                       = {}\n", time_method);
385✔
879
      fmt::print(
315✔
880
        " Backwards Difference Order        = {}\n", RandomRay::bd_order_);
881
    }
385✔
882

883
    header("Timing Statistics", 4);
1,036✔
884
    show_time("Total time for initialization", time_initialize.elapsed());
1,036✔
885
    show_time("Reading cross sections", time_read_xs.elapsed(), 1);
1,036✔
886
    show_time("Total simulation time", time_total.elapsed());
1,036✔
887
    show_time("Transport sweep only", time_transport.elapsed(), 1);
1,036✔
888
    show_time("Source update only", time_update_src.elapsed(), 1);
1,036✔
889
    if (settings::kinetic_simulation && settings::create_delayed_neutrons) {
1,036!
890
      show_time(
539✔
891
        "Precursor computation only", time_compute_precursors.elapsed(), 1);
892
      misc_time -= time_compute_precursors.elapsed();
539✔
893
    }
894
    show_time("Tally conversion only", time_tallies.elapsed(), 1);
1,036✔
895
    show_time("MPI source reductions only", time_bank_sendrecv.elapsed(), 1);
1,036✔
896
    show_time("Other iteration routines", misc_time, 1);
1,036✔
897
    if (settings::run_mode == RunMode::EIGENVALUE) {
1,036✔
898
      show_time("Time in inactive batches", time_inactive.elapsed());
715✔
899
    }
900
    show_time("Time in active batches", time_active.elapsed());
1,036✔
901
    show_time("Time writing statepoints", time_statepoint.elapsed());
1,036✔
902
    show_time("Total time for finalization", time_finalize.elapsed());
1,036✔
903
    show_time("Time per integration", time_per_integration);
1,036✔
904
  }
1,036✔
905

906
  if (settings::verbosity >= 4 && settings::run_mode == RunMode::EIGENVALUE) {
1,036!
907
    header("Results", 4);
715✔
908
    fmt::print(" k-effective                       = {:.5f} +/- {:.5f}\n",
715✔
909
      simulation::keff, simulation::keff_std);
910
  }
911
}
1,036✔
912

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