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

openmc-dev / openmc / 21008733875

14 Jan 2026 08:26PM UTC coverage: 82.003% (-0.05%) from 82.057%
21008733875

Pull #3702

github

web-flow
Merge 349679067 into 7861adf53
Pull Request #3702: Random Ray Kinetic Simulation Mode

17687 of 24565 branches covered (72.0%)

Branch coverage included in aggregate %.

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

79 existing lines in 9 files now uncovered.

56468 of 65865 relevant lines covered (85.73%)

53062658.42 hits per line

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

85.46
/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()
865✔
26
{
27
  //////////////////////////////////////////////////////////
28
  // Run forward simulation
29
  //////////////////////////////////////////////////////////
30

31
  if (mpi::master) {
865✔
32
    if (FlatSourceDomain::adjoint_) {
595✔
33
      FlatSourceDomain::adjoint_ = false;
56✔
34
      openmc::print_adjoint_header();
56✔
35
      FlatSourceDomain::adjoint_ = true;
56✔
36
    }
37
  }
38

39
  // Initialize OpenMC general data structures
40
  openmc_simulation_init();
865✔
41

42
  // Validate that inputs meet requirements for random ray mode
43
  if (mpi::master)
865✔
44
    validate_random_ray_inputs();
595✔
45

46
  // Initialize Random Ray Simulation Object
47
  RandomRaySimulation sim;
865✔
48

49
  // Initialize fixed sources, if present
50
  sim.apply_fixed_sources_and_mesh_domains();
865✔
51

52
  // Run initial random ray simulation
53
  sim.run_single_simulation();
865✔
54

55
  if (settings::kinetic_simulation) {
865✔
56
    // Second steady state simulation to correct the batchwise k-eff
57
    sim.kinetic_initial_condition();
192✔
58

59
    // Timestepping loop
60
    for (int i = 0; i < settings::n_timesteps; i++)
1,152✔
61
      sim.kinetic_single_time_step(i);
960✔
62
  }
63

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

68
  sim.random_ray_adjoint();
865✔
69
}
865✔
70

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

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

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

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

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

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

171
  // Validate ray source
172
  ///////////////////////////////////////////////////////////////////
173

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

389
void print_adjoint_header()
112✔
390
{
391
  if (!FlatSourceDomain::adjoint_)
112✔
392
    // If we're going to do an adjoint simulation afterwards, report that this
393
    // is the initial forward flux solve.
394
    header("FORWARD FLUX SOLVE", 3);
56✔
395
  else
396
    // Otherwise report that we are doing the adjoint simulation
397
    header("ADJOINT FLUX SOLVE", 3);
56✔
398
}
112✔
399

400
//-----------------------------------------------------------------------------
401
// Non-member functions for kinetic simulations
402

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

412
  const char* old_fname = old_filename_.c_str();
2,304✔
413
  const char* new_fname = new_filename_.c_str();
2,304✔
414
  std::rename(old_fname, new_fname);
2,304✔
415
}
2,304✔
416

417
//==============================================================================
418
// RandomRaySimulation implementation
419
//==============================================================================
420

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

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

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

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

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

453
  // Adjoint is always false for the forward calculation
454
  FlatSourceDomain::adjoint_ = false;
865✔
455

456
  // The first simulation is run after initialization
457
  is_first_simulation_ = true;
865✔
458

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

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

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

487
void RandomRaySimulation::run_single_simulation()
2,098✔
488
{
489
  if (!is_first_simulation_) {
2,098✔
490
    if (mpi::master && adjoint_needed_)
1,233✔
491
      openmc::print_adjoint_header();
56✔
492

493
    // Reset the timers and reinitialize the general OpenMC datastructures if
494
    // this is after the first simulation
495
    reset_timers();
1,233✔
496

497
    // Initialize OpenMC general data structures
498
    openmc_simulation_init();
1,233✔
499
  }
500

501
  // Begin main simulation timer
502
  simulation::time_total.start();
2,098✔
503

504
  // Execute random ray simulation
505
  simulate();
2,098✔
506

507
  // End main simulation timer
508
  simulation::time_total.stop();
2,098✔
509

510
  // Normalize and save the final forward quantities
511
  domain_->normalize_final_quantities();
2,098✔
512

513
  // Finalize OpenMC
514
  openmc_simulation_finalize();
2,098✔
515

516
  // Output all simulation results
517
  output_simulation_results();
2,098✔
518

519
  // Toggle that the simulation object has been initialized after the first
520
  // simulation
521
  if (is_first_simulation_)
2,098✔
522
    is_first_simulation_ = false;
865✔
523
}
2,098✔
524

525
void RandomRaySimulation::random_ray_adjoint()
865✔
526
{
527
  if (!adjoint_needed_) {
865✔
528
    return;
784✔
529
  }
530

531
  // Configure the domain for adjoint simulation
532
  FlatSourceDomain::adjoint_ = true;
81✔
533

534
  // Reset k-eff
535
  domain_->k_eff_ = 1.0;
81✔
536

537
  // Initialize adjoint fixed sources, if present
538
  prepare_fixed_sources_adjoint();
81✔
539

540
  // Transpose scattering matrix
541
  domain_->transpose_scattering_matrix();
81✔
542

543
  // Swap nu_sigma_f and chi
544
  domain_->nu_sigma_f_.swap(domain_->chi_);
81✔
545

546
  // Run a single simulation
547
  run_single_simulation();
81✔
548
}
549

550
void RandomRaySimulation::kinetic_initial_condition()
192✔
551
{
552
  // Set flag for k_eff correction
553
  simulation::k_eff_correction = true;
192✔
554

555
  static_avg_k_eff_ = simulation::keff;
192✔
556
  domain_->k_eff_ = static_avg_k_eff_;
192✔
557
  domain_->source_regions_.adjoint_reset();
192✔
558
  domain_->propagate_final_quantities();
192✔
559
  domain_->source_regions_.time_step_reset();
192✔
560

561
  // Run the initial condition
562
  run_single_simulation();
192✔
563

564
  // Initialize the BD arrays
565
  domain_->store_time_step_quantities(false);
192✔
566

567
  // Store k-eff corrected initial condition statepoints
568
  rename_time_step_file(
540✔
569
    fmt::format("statepoint.{0}", settings::n_batches), ".h5", 0);
348✔
570
  if (settings::output_tallies) {
192!
571
    rename_time_step_file("tallies", ".out", 0);
192✔
572
  }
573

574
  // Set flags for kinetic simulation
575
  simulation::is_initial_condition = false;
192✔
576
  simulation::k_eff_correction = false;
192✔
577

578
  // Set starting time as zero
579
  simulation::current_time = 0;
192✔
580
}
192✔
581

582
// TODO: Add support for time-dependent restart
583
void RandomRaySimulation::kinetic_single_time_step(int i)
960✔
584
{
585
  // Increment time step
586
  simulation::current_timestep = i + 1;
960✔
587
  simulation::current_time += settings::dt;
960✔
588

589
  // Propogate results of previous simulation
590
  domain_->k_eff_ = static_avg_k_eff_;
960✔
591
  domain_->source_regions_.adjoint_reset();
960✔
592
  domain_->propagate_final_quantities();
960✔
593
  domain_->source_regions_.time_step_reset();
960✔
594

595
  // Compute RHS backward differences
596
  domain_->compute_rhs_bd_quantities();
960✔
597

598
  // Update time dependent cross section based on the density
599
  domain_->update_material_density(i);
960✔
600

601
  // Run the simulation for the current time step
602
  run_single_simulation();
960✔
603

604
  // Rename statepoint and tallies file for the current time step
605
  rename_time_step_file(fmt::format("statepoint.{0}", settings::n_batches),
1,920✔
606
    ".h5", simulation::current_timestep);
607
  if (settings::output_tallies) {
960!
608
    rename_time_step_file("tallies", ".out", simulation::current_timestep);
960✔
609
  }
610

611
  // Store final quantities for the current time step
612
  domain_->store_time_step_quantities();
960✔
613
}
960✔
614

615
void RandomRaySimulation::simulate()
2,098✔
616
{
617
  // Random ray power iteration loop
618
  while (simulation::current_batch < settings::n_batches) {
210,430✔
619
    // Initialize the current batch
620
    initialize_batch();
208,332✔
621
    initialize_generation();
208,332✔
622

623
    // MPI not supported in random ray solver, so all work is done by rank 0
624
    // TODO: Implement domain decomposition for MPI parallelism
625
    if (mpi::master) {
208,332✔
626

627
      // Reset total starting particle weight used for normalizing tallies
628
      simulation::total_weight = 1.0;
143,232✔
629

630
      // Update source term (scattering + fission (+ delayed if kinetic))
631
      domain_->update_all_neutron_sources();
143,232✔
632

633
      // Reset scalar fluxes, iteration volume tallies, and region hit flags
634
      // to zero
635
      domain_->batch_reset();
143,232✔
636

637
      // At the beginning of the simulation, if mesh subdivision is in use, we
638
      // need to swap the main source region container into the base container,
639
      // as the main source region container will be used to hold the true
640
      // subdivided source regions. The base container will therefore only
641
      // contain the external source region information, the mesh indices,
642
      // material properties, and initial guess values for the flux/source.
643

644
      // Start timer for transport
645
      simulation::time_transport.start();
143,232✔
646

647
// Transport sweep over all random rays for the iteration
648
#pragma omp parallel for schedule(dynamic)                                     \
78,132✔
649
  reduction(+ : total_geometric_intersections_)
78,132✔
650
      for (int i = 0; i < settings::n_particles; i++) {
6,893,100✔
651
        RandomRay ray(i, domain_.get());
6,828,000✔
652
        total_geometric_intersections_ +=
6,828,000✔
653
          ray.transport_history_based_single_ray();
6,828,000✔
654
      }
6,828,000✔
655

656
      simulation::time_transport.stop();
143,232✔
657

658
      // Add any newly discovered source regions to the main source region
659
      // container.
660
      domain_->finalize_discovered_source_regions();
143,232✔
661

662
      // Normalize scalar flux and update volumes
663
      domain_->normalize_scalar_flux_and_volumes(
143,232✔
664
        settings::n_particles * RandomRay::distance_active_);
665

666
      // Add source to scalar flux, compute number of FSR hits
667
      int64_t n_hits = domain_->add_source_to_scalar_flux();
143,232✔
668

669
      // Apply transport stabilization factors
670
      domain_->apply_transport_stabilization();
143,232✔
671

672
      if (settings::run_mode == RunMode::EIGENVALUE) {
143,232✔
673
        // Compute random ray k-eff
674
        if (!settings::kinetic_simulation ||
133,540!
675
            settings::kinetic_simulation && simulation::is_initial_condition) {
130,900✔
676
          domain_->compute_k_eff();
40,040✔
677
          if (simulation::k_eff_correction) {
40,040✔
678
            static_fission_rate_.push_back(domain_->fission_rate_);
18,700✔
679
            static_k_eff_.push_back(domain_->k_eff_);
18,700✔
680
          }
681
        } else {
682
          domain_->k_eff_ = static_k_eff_[simulation::current_batch - 1];
93,500✔
683
          domain_->fission_rate_ =
93,500✔
684
            static_fission_rate_[simulation::current_batch - 1];
93,500✔
685
        }
686

687
        // Store random ray k-eff into OpenMC's native k-eff variable
688
        global_tally_tracklength = domain_->k_eff_;
133,540✔
689
      }
690

691
      // Compute precursors if delayed neutrons are turned on
692
      if (settings::kinetic_simulation && settings::create_delayed_neutrons)
143,232!
693
        domain_->compute_all_precursors();
130,900✔
694

695
      // Execute all tallying tasks, if this is an active batch
696
      if (simulation::current_batch > settings::n_inactive) {
143,232✔
697

698
        // Add this iteration's scalar flux estimate to final accumulated
699
        // estimate
700
        domain_->accumulate_iteration_quantities();
69,746✔
701

702
        // Use above mapping to contribute FSR flux data to appropriate
703
        // tallies
704
        domain_->random_ray_tally();
69,746✔
705
      }
706

707
      // Set phi_old = phi_new
708
      domain_->flux_swap();
143,232✔
709
      if (settings::kinetic_simulation && settings::create_delayed_neutrons) {
143,232!
710
        domain_->precursors_swap();
130,900✔
711
      }
712

713
      // Check for any obvious insabilities/nans/infs
714
      instability_check(n_hits, domain_->k_eff_, avg_miss_rate_);
143,232✔
715
    } // End MPI master work
716

717
    // Store simulation metrics
718
    RandomRay::avg_miss_rate_ = avg_miss_rate_ / settings::n_batches;
208,332✔
719
    RandomRay::total_geometric_intersections_ = total_geometric_intersections_;
208,332✔
720
    RandomRay::n_external_source_regions_ = domain_->n_external_source_regions_;
208,332✔
721
    RandomRay::n_source_regions_ = domain_->n_source_regions();
208,332✔
722

723
    // Finalize the current batch
724
    finalize_generation();
208,332✔
725
    finalize_batch();
208,332✔
726
  } // End random ray power iteration loop
727

728
  domain_->count_external_source_regions();
2,098✔
729
}
2,098✔
730

731
void RandomRaySimulation::output_simulation_results() const
2,098✔
732
{
733
  // Print random ray results
734
  if (mpi::master) {
2,098✔
735
    print_results_random_ray();
1,443✔
736
    if (model::plots.size() > 0) {
1,443!
737
      domain_->output_to_vtk();
×
738
    }
739
  }
740
}
2,098✔
741

742
// Apply a few sanity checks to catch obvious cases of numerical instability.
743
// Instability typically only occurs if ray density is extremely low.
744
void RandomRaySimulation::instability_check(
143,232✔
745
  int64_t n_hits, double k_eff, double& avg_miss_rate) const
746
{
747
  double percent_missed = ((domain_->n_source_regions() - n_hits) /
143,232✔
748
                            static_cast<double>(domain_->n_source_regions())) *
143,232✔
749
                          100.0;
143,232✔
750
  avg_miss_rate += percent_missed;
143,232✔
751

752
  if (mpi::master) {
143,232!
753
    if (percent_missed > 10.0) {
143,232✔
754
      warning(fmt::format(
957✔
755
        "Very high FSR miss rate detected ({:.3f}%). Instability may occur. "
756
        "Increase ray density by adding more rays and/or active distance.",
757
        percent_missed));
758
    } else if (percent_missed > 1.0) {
142,275✔
759
      warning(
2!
760
        fmt::format("Elevated FSR miss rate detected ({:.3f}%). Increasing "
4!
761
                    "ray density by adding more rays and/or active "
762
                    "distance may improve simulation efficiency.",
763
          percent_missed));
764
    }
765

766
    if (k_eff > 10.0 || k_eff < 0.01 || !(std::isfinite(k_eff))) {
143,232!
767
      fatal_error(fmt::format("Instability detected: k-eff = {:.5f}", k_eff));
×
768
    }
769
  }
770
}
143,232✔
771

772
// Print random ray simulation results
773
void RandomRaySimulation::print_results_random_ray() const
1,443✔
774
{
775
  using namespace simulation;
776

777
  if (settings::verbosity >= 6) {
1,443!
778
    double total_integrations =
1,443✔
779
      RandomRay::total_geometric_intersections_ * negroups_;
1,443✔
780
    double time_per_integration =
781
      simulation::time_transport.elapsed() / total_integrations;
1,443✔
782
    double misc_time = time_total.elapsed() - time_update_src.elapsed() -
1,443✔
783
                       time_transport.elapsed() - time_tallies.elapsed() -
1,443✔
784
                       time_bank_sendrecv.elapsed();
1,443✔
785

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

821
    std::string estimator;
1,443✔
822
    switch (domain_->volume_estimator_) {
1,443!
823
    case RandomRayVolumeEstimator::SIMULATION_AVERAGED:
22✔
824
      estimator = "Simulation Averaged";
22✔
825
      break;
22✔
826
    case RandomRayVolumeEstimator::NAIVE:
68✔
827
      estimator = "Naive";
68✔
828
      break;
68✔
829
    case RandomRayVolumeEstimator::HYBRID:
1,353✔
830
      estimator = "Hybrid";
1,353✔
831
      break;
1,353✔
832
    default:
×
833
      fatal_error("Invalid volume estimator type");
×
834
    }
835
    fmt::print(" Volume Estimator Type             = {}\n", estimator);
1,181✔
836

837
    std::string adjoint_true = (FlatSourceDomain::adjoint_) ? "ON" : "OFF";
4,329✔
838
    fmt::print(" Adjoint Flux Mode                 = {}\n", adjoint_true);
1,181✔
839

840
    std::string shape;
2,886✔
841
    switch (RandomRay::source_shape_) {
1,443!
842
    case RandomRaySourceShape::FLAT:
1,223✔
843
      shape = "Flat";
1,223✔
844
      break;
1,223✔
845
    case RandomRaySourceShape::LINEAR:
187✔
846
      shape = "Linear";
187✔
847
      break;
187✔
848
    case RandomRaySourceShape::LINEAR_XY:
33✔
849
      shape = "Linear XY";
33✔
850
      break;
33✔
851
    default:
×
852
      fatal_error("Invalid random ray source shape");
×
853
    }
854
    fmt::print(" Source Shape                      = {}\n", shape);
1,181✔
855
    std::string sample_method =
856
      (RandomRay::sample_method_ == RandomRaySampleMethod::PRNG) ? "PRNG"
1,443✔
857
                                                                 : "Halton";
4,329✔
858
    fmt::print(" Sample Method                     = {}\n", sample_method);
1,181✔
859

860
    if (domain_->is_transport_stabilization_needed_) {
1,443✔
861
      fmt::print(" Transport XS Stabilization Used   = YES (rho = {:.3f})\n",
165✔
862
        FlatSourceDomain::diagonal_stabilization_rho_);
863
    } else {
864
      fmt::print(" Transport XS Stabilization Used   = NO\n");
1,278✔
865
    }
866
    if (settings::kinetic_simulation && !simulation::is_initial_condition) {
1,443✔
867
      std::string time_method =
868
        (RandomRay::time_method_ == RandomRayTimeMethod::ISOTROPIC)
660✔
869
          ? "ISOTROPIC"
870
          : "PROPAGATION";
1,320✔
871
      fmt::print(" Time Method                       = {}\n", time_method);
660✔
872
      fmt::print(
540✔
873
        " Backwards Difference Order        = {}\n", RandomRay::bd_order_);
874
    }
660✔
875

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

899
  if (settings::verbosity >= 4 && settings::run_mode == RunMode::EIGENVALUE) {
1,443!
900
    header("Results", 4);
1,111✔
901
    fmt::print(" k-effective                       = {:.5f} +/- {:.5f}\n",
1,111✔
902
      simulation::keff, simulation::keff_std);
903
  }
904
}
1,443✔
905

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