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

openmc-dev / openmc / 20733102133

06 Jan 2026 12:03AM UTC coverage: 81.932% (-0.2%) from 82.174%
20733102133

Pull #3702

github

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

17551 of 24380 branches covered (71.99%)

Branch coverage included in aggregate %.

869 of 1117 new or added lines in 21 files covered. (77.8%)

483 existing lines in 19 files now uncovered.

55915 of 65287 relevant lines covered (85.64%)

50695599.69 hits per line

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

84.68
/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
  // Check if adjoint calculation is needed. If it is, we will run the forward
32
  // calculation first and then the adjoint calculation later.
33
  bool adjoint_needed = FlatSourceDomain::adjoint_;
753✔
34

35
  // Check if this simulation is to establish an initial condition
36
  if (settings::kinetic_simulation) {
753✔
37
    simulation::is_initial_condition = true;
112✔
38
  }
39

40
  // Configure the domain for forward simulation
41
  FlatSourceDomain::adjoint_ = false;
753✔
42

43
  // If we're going to do a kinetic simulation, report that this is
44
  // the initial condition.
45
  if (settings::kinetic_simulation && mpi::master)
753✔
46
    header("KINETIC SIMULATION INITIAL CONDITION", 3);
77✔
47

48
  // If we're going to do an adjoint simulation afterwards, report that this is
49
  // the initial forward flux solve.
50
  if (adjoint_needed && mpi::master)
753✔
51
    header("FORWARD FLUX SOLVE", 3);
56✔
52

53
  // Initialize OpenMC general data structures
54
  openmc_simulation_init();
753✔
55

56
  // Validate that inputs meet requirements for random ray mode
57
  if (mpi::master)
753✔
58
    validate_random_ray_inputs();
518✔
59

60
  // Initialize Random Ray Simulation Object
61
  RandomRaySimulation sim;
753✔
62

63
  // Initialize fixed sources, if present
64
  sim.apply_fixed_sources_and_mesh_domains();
753✔
65

66
  // Begin main simulation timer
67
  simulation::time_total.start();
753✔
68

69
  // Execute random ray simulation
70
  sim.simulate();
753✔
71

72
  // End main simulation timer
73
  simulation::time_total.stop();
753✔
74

75
  // Normalize and save the final forward quantities
76
  sim.domain()->normalize_final_quantities();
753✔
77

78
  // Finalize OpenMC
79
  openmc_simulation_finalize();
753✔
80

81
  // Output all simulation results
82
  sim.output_simulation_results();
753✔
83

84
  if (settings::kinetic_simulation) {
753✔
85
    // Now do a second steady state simulation to correct the batch wise k-eff
86
    // estimates
87
    simulation::k_eff_correction = true;
112✔
88

89
    if (settings::kinetic_simulation && mpi::master)
112!
90
      header("KINETIC SIMULATION INITIAL CONDITION (K-EFF CORRECTION)", 3);
77✔
91

92
    // If we're going to do an adjoint simulation afterwards, report that this
93
    // is the initial forward flux solve.
94
    if (adjoint_needed && mpi::master)
112!
NEW
95
      header("FORWARD FLUX SOLVE", 3);
×
96

97
    // Initialize OpenMC general data structures
98
    openmc_simulation_init();
112✔
99

100
    double initial_k_eff = simulation::keff;
112✔
101

102
    sim.domain()->k_eff_ = initial_k_eff;
112✔
103
    sim.domain()->source_regions_.adjoint_reset();
112✔
104
    sim.domain()->propagate_final_quantities();
112✔
105
    sim.domain()->source_regions_.time_step_reset();
112✔
106

107
    // Begin main simulation timer
108
    simulation::time_total.start();
112✔
109

110
    // Execute random ray simulation
111
    sim.simulate();
112✔
112

113
    // End main simulation timer
114
    simulation::time_total.stop();
112✔
115

116
    // Normalize and save the final forward quantities
117
    sim.domain()->normalize_final_quantities();
112✔
118

119
    // Finalize OpenMC
120
    openmc_simulation_finalize();
112✔
121

122
    // Output all simulation results
123
    sim.output_simulation_results();
112✔
124

125
    //-------------------------------------------------------------------------
126
    // KINETIC SIMULATION
127

128
    warning(
112✔
129
      "Time-dependent explicit void treatment has not yet been "
130
      "implemented. Use caution when interpreting results from models with "
131
      "voids, as they may contain large inaccuracies.");
132
    sim.domain()->store_time_step_quantities(false);
112✔
133
    rename_statepoint_file(0);
112✔
134
    if (settings::output_tallies) {
112!
135
      rename_tallies_file(0);
112✔
136
    }
137
    set_time_dependent_settings();
112✔
138

139
    // Timestepping loop
140
    // TODO: Add support for time-dependent restart
141
    for (int i = 0; i < settings::n_timesteps; i++) {
672✔
142
      simulation::current_timestep = i + 1;
560✔
143

144
      // Print simulation information
145
      if (mpi::master) {
560✔
146
        std::string message = fmt::format(
147
          "KINETIC SIMULATION TIME STEP {0}", simulation::current_timestep);
315✔
148
        const char* msg = message.c_str();
385✔
149
        header(msg, 3);
385✔
150
      }
385✔
151

152
      // If we're going to do an adjoint simulation afterwards, report that this
153
      // is the initial forward flux solve.
154
      if (adjoint_needed && mpi::master)
560!
UNCOV
155
        header("FORWARD FLUX SOLVE", 3);
×
156

157
      reset_timers();
560✔
158

159
      // Initialize OpenMC general data structures
160
      openmc_simulation_init();
560✔
161

162
      sim.domain()->k_eff_ = initial_k_eff;
560✔
163
      sim.domain()->source_regions_.adjoint_reset();
560✔
164
      sim.domain()->propagate_final_quantities();
560✔
165
      sim.domain()->source_regions_.time_step_reset();
560✔
166

167
      // Compute RHS backward differences to be used later
168
      sim.domain()->compute_rhs_bd_quantities();
560✔
169

170
      // Update time dependent cross section based on the density
171
      sim.domain()->update_material_density(i);
560✔
172

173
      // Begin main simulation timer
174
      simulation::time_total.start();
560✔
175

176
      // Execute random ray simulation
177
      sim.simulate();
560✔
178

179
      // End main simulation timer
180
      simulation::time_total.stop();
560✔
181

182
      // Finalize OpenMC
183
      openmc_simulation_finalize();
560✔
184

185
      // Output all simulation results
186
      sim.output_simulation_results();
560✔
187

188
      // Rename statepoint and tallies file
189
      rename_statepoint_file(simulation::current_timestep);
560✔
190
      if (settings::output_tallies) {
560!
191
        rename_tallies_file(simulation::current_timestep);
560✔
192
      }
193

194
      // Normalize and store final quantities for next time step
195
      sim.domain()->normalize_final_quantities();
560✔
196
      sim.domain()->store_time_step_quantities();
560✔
197

198
      // Advance time
199
      simulation::current_time += settings::dt;
560✔
200
    }
201
  }
202

203
  //////////////////////////////////////////////////////////
204
  // Run adjoint simulation (if enabled)
205
  //////////////////////////////////////////////////////////
206

207
  if (!adjoint_needed) {
753✔
208
    return;
672✔
209
  }
210

211
  reset_timers();
81✔
212

213
  // Configure the domain for adjoint simulation
214
  FlatSourceDomain::adjoint_ = true;
81✔
215

216
  if (mpi::master)
81✔
217
    header("ADJOINT FLUX SOLVE", 3);
56✔
218

219
  sim.domain()->k_eff_ = 1.0;
81✔
220

221
  // Initialize adjoint fixed sources, if present
222
  sim.prepare_fixed_sources_adjoint();
81✔
223

224
  // Transpose scattering matrix
225
  sim.domain()->transpose_scattering_matrix();
81✔
226

227
  // Swap nu_sigma_f and chi
228
  sim.domain()->nu_sigma_f_.swap(sim.domain()->chi_);
81✔
229

230
  // Initialize OpenMC general data structures
231
  openmc_simulation_init();
81✔
232

233
  // Begin main simulation timer
234
  simulation::time_total.start();
81✔
235

236
  // Execute random ray simulation
237
  sim.simulate();
81✔
238

239
  // End main simulation timer
240
  simulation::time_total.stop();
81✔
241

242
  // Finalize OpenMC
243
  openmc_simulation_finalize();
81✔
244

245
  // Output all simulation results
246
  sim.output_simulation_results();
81✔
247
}
753✔
248

249
// Enforces restrictions on inputs in random ray mode.  While there are
250
// many features that don't make sense in random ray mode, and are therefore
251
// unsupported, we limit our testing/enforcement operations only to inputs
252
// that may cause erroneous/misleading output or crashes from the solver.
253
void validate_random_ray_inputs()
518✔
254
{
255
  // Validate tallies
256
  ///////////////////////////////////////////////////////////////////
257
  for (auto& tally : model::tallies) {
1,465✔
258

259
    // Validate score types
260
    for (auto score_bin : tally->scores_) {
2,136✔
261
      switch (score_bin) {
1,189!
262
      case SCORE_FLUX:
1,156✔
263
      case SCORE_TOTAL:
264
      case SCORE_FISSION:
265
      case SCORE_NU_FISSION:
266
      case SCORE_EVENTS:
267
        break;
1,156✔
268

269
      // TODO: add support for prompt and delayed fission
270
      case SCORE_PRECURSORS: {
33✔
271
        if (settings::kinetic_simulation && settings::create_delayed_neutrons) {
33!
272
          break;
273
        } else {
UNCOV
274
          fatal_error("Invalid score specified in tallies.xml. Precursors can "
×
275
                      "only be scored for kinetic simulations.");
276
        }
277
      }
278
      default:
279
        fatal_error(
×
280
          "Invalid score specified. Only flux, total, fission, nu-fission, and "
281
          "event scores are supported in random ray mode. (precursors are "
282
          "supported for "
283
          "kinetic simulations when delayed neutrons are turned on).");
284
      }
285
    }
286

287
    // Validate filter types
288
    for (auto f : tally->filters()) {
2,050✔
289
      auto& filter = *model::tally_filters[f];
1,103✔
290

291
      switch (filter.type()) {
1,103!
292
      case FilterType::CELL:
1,070✔
293
      case FilterType::CELL_INSTANCE:
294
      case FilterType::DISTRIBCELL:
295
      case FilterType::ENERGY:
296
      case FilterType::MATERIAL:
297
      case FilterType::MESH:
298
      case FilterType::UNIVERSE:
299
      case FilterType::PARTICLE:
300
        break;
1,070✔
301
      case FilterType::DELAYED_GROUP:
33✔
302
        if (settings::kinetic_simulation) {
33!
303
          break;
33✔
304
        } else {
UNCOV
305
          fatal_error("Invalid filter specified in tallies.xml. Kinetic "
×
306
                      "simulations is required "
307
                      "to tally with a delayed_group filter.");
308
        }
UNCOV
309
      default:
×
UNCOV
310
        fatal_error("Invalid filter specified. Only cell, cell_instance, "
×
311
                    "distribcell, energy, material, mesh, and universe filters "
312
                    "are supported in random ray mode (delayed_group is "
313
                    "supported for kinetic simulations).");
314
      }
315
    }
316
  }
317

318
  // TODO: validate kinetic data is present
319
  //  Validate MGXS data
320
  ///////////////////////////////////////////////////////////////////
321
  for (auto& material : data::mg.macro_xs_) {
1,938✔
322
    if (!material.is_isotropic) {
1,420!
NEW
323
      fatal_error("Anisotropic MGXS detected. Only isotropic XS data sets "
×
324
                  "supported in random ray mode.");
325
    }
326
    if (material.get_xsdata().size() > 1) {
1,420!
NEW
327
      warning("Non-isothermal MGXS detected. Only isothermal XS data sets "
×
328
              "supported in random ray mode. Using lowest temperature.");
329
    }
330
    for (int g = 0; g < data::mg.num_energy_groups_; g++) {
10,035✔
331
      if (material.exists_in_model) {
8,615✔
332
        // Temperature and angle indices, if using multiple temperature
333
        // data sets and/or anisotropic data sets.
334
        // TODO: Currently assumes we are only using single temp/single angle
335
        // data.
336
        const int t = 0;
8,571✔
337
        const int a = 0;
8,571✔
338
        double sigma_t =
339
          material.get_xs(MgxsType::TOTAL, g, NULL, NULL, NULL, t, a);
8,571✔
340
        if (sigma_t <= 0.0) {
8,571!
NEW
341
          fatal_error("No zero or negative total macroscopic cross sections "
×
342
                      "allowed in random ray mode. If the intention is to make "
343
                      "a void material, use a cell fill of 'None' instead.");
344
        }
345
      }
346
    }
347
  }
348

349
  // Validate ray source
350
  ///////////////////////////////////////////////////////////////////
351

352
  // Check for independent source
353
  IndependentSource* is =
354
    dynamic_cast<IndependentSource*>(RandomRay::ray_source_.get());
518!
355
  if (!is) {
518!
NEW
356
    fatal_error("Invalid ray source definition. Ray source must provided and "
×
357
                "be of type IndependentSource.");
358
  }
359

360
  // Check for box source
361
  SpatialDistribution* space_dist = is->space();
518✔
362
  SpatialBox* sb = dynamic_cast<SpatialBox*>(space_dist);
518!
363
  if (!sb) {
518!
NEW
364
    fatal_error(
×
365
      "Invalid ray source definition -- only box sources are allowed.");
366
  }
367

368
  // Check that box source is not restricted to fissionable areas
369
  if (sb->only_fissionable()) {
518!
NEW
370
    fatal_error(
×
371
      "Invalid ray source definition -- fissionable spatial distribution "
372
      "not allowed.");
373
  }
374

375
  // Check for isotropic source
376
  UnitSphereDistribution* angle_dist = is->angle();
518✔
377
  Isotropic* id = dynamic_cast<Isotropic*>(angle_dist);
518!
378
  if (!id) {
518!
NEW
379
    fatal_error("Invalid ray source definition -- only isotropic sources are "
×
380
                "allowed.");
381
  }
382

383
  // Validate external sources
384
  ///////////////////////////////////////////////////////////////////
385
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
518✔
386
    if (model::external_sources.size() < 1) {
276!
NEW
387
      fatal_error("Must provide a particle source (in addition to ray source) "
×
388
                  "in fixed source random ray mode.");
389
    }
390

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

394
      // Check for independent source
395
      IndependentSource* is = dynamic_cast<IndependentSource*>(s);
276!
396

397
      if (!is) {
276!
NEW
398
        fatal_error(
×
399
          "Only IndependentSource external source types are allowed in "
400
          "random ray mode");
401
      }
402

403
      // Check for isotropic source
404
      UnitSphereDistribution* angle_dist = is->angle();
276✔
405
      Isotropic* id = dynamic_cast<Isotropic*>(angle_dist);
276!
406
      if (!id) {
276!
NEW
407
        fatal_error(
×
408
          "Invalid source definition -- only isotropic external sources are "
409
          "allowed in random ray mode.");
410
      }
411

412
      // Validate that a domain ID was specified OR that it is a point source
413
      auto sp = dynamic_cast<SpatialPoint*>(is->space());
276!
414
      if (is->domain_ids().size() == 0 && !sp) {
276!
NEW
415
        fatal_error("Fixed sources must be point source or spatially "
×
416
                    "constrained by domain id (cell, material, or universe) in "
417
                    "random ray mode.");
418
      } else if (is->domain_ids().size() > 0 && sp) {
276✔
419
        // If both a domain constraint and a non-default point source location
420
        // are specified, notify user that domain constraint takes precedence.
421
        if (sp->r().x == 0.0 && sp->r().y == 0.0 && sp->r().z == 0.0) {
242!
422
          warning("Fixed source has both a domain constraint and a point "
242✔
423
                  "type spatial distribution. The domain constraint takes "
424
                  "precedence in random ray mode -- point source coordinate "
425
                  "will be ignored.");
426
        }
427
      }
428

429
      // Check that a discrete energy distribution was used
430
      Distribution* d = is->energy();
276✔
431
      Discrete* dd = dynamic_cast<Discrete*>(d);
276!
432
      if (!dd) {
276!
UNCOV
433
        fatal_error(
×
434
          "Only discrete (multigroup) energy distributions are allowed for "
435
          "external sources in random ray mode.");
436
      }
437
    }
438
  }
439

440
  // Validate plotting files
441
  ///////////////////////////////////////////////////////////////////
442
  for (int p = 0; p < model::plots.size(); p++) {
518!
443

444
    // Get handle to OpenMC plot object
UNCOV
445
    const auto& openmc_plottable = model::plots[p];
×
UNCOV
446
    Plot* openmc_plot = dynamic_cast<Plot*>(openmc_plottable.get());
×
447

448
    // Random ray plots only support voxel plots
UNCOV
449
    if (!openmc_plot) {
×
NEW
450
      warning(fmt::format(
×
451
        "Plot {} will not be used for end of simulation data plotting -- only "
452
        "voxel plotting is allowed in random ray mode.",
NEW
453
        openmc_plottable->id()));
×
NEW
454
      continue;
×
NEW
455
    } else if (openmc_plot->type_ != Plot::PlotType::voxel) {
×
NEW
456
      warning(fmt::format(
×
457
        "Plot {} will not be used for end of simulation data plotting -- only "
458
        "voxel plotting is allowed in random ray mode.",
NEW
459
        openmc_plottable->id()));
×
NEW
460
      continue;
×
461
    }
462
  }
463

464
  // Warn about slow MPI domain replication, if detected
465
  ///////////////////////////////////////////////////////////////////
466
#ifdef OPENMC_MPI
467
  if (mpi::n_procs > 1) {
236✔
468
    warning(
235✔
469
      "MPI parallelism is not supported by the random ray solver. All work "
470
      "will be performed by rank 0. Domain decomposition may be implemented in "
471
      "the future to provide efficient MPI scaling.");
472
  }
473
#endif
474

475
  // Warn about instability resulting from linear sources in small regions
476
  // when generating weight windows with FW-CADIS and an overlaid mesh.
477
  ///////////////////////////////////////////////////////////////////
478
  if (RandomRay::source_shape_ == RandomRaySourceShape::LINEAR &&
694✔
479
      variance_reduction::weight_windows.size() > 0) {
176✔
480
    warning(
11✔
481
      "Linear sources may result in negative fluxes in small source regions "
482
      "generated by mesh subdivision. Negative sources may result in low "
483
      "quality FW-CADIS weight windows. We recommend you use flat source mode "
484
      "when generating weight windows with an overlaid mesh tally.");
485
  }
486
}
518✔
487

488
void openmc_reset_random_ray()
7,901✔
489
{
490
  FlatSourceDomain::volume_estimator_ = RandomRayVolumeEstimator::HYBRID;
7,901✔
491
  FlatSourceDomain::volume_normalized_flux_tallies_ = false;
7,901✔
492
  FlatSourceDomain::adjoint_ = false;
7,901✔
493
  FlatSourceDomain::mesh_domain_map_.clear();
7,901✔
494
  RandomRay::ray_source_.reset();
7,901✔
495
  RandomRay::source_shape_ = RandomRaySourceShape::FLAT;
7,901✔
496
  RandomRay::sample_method_ = RandomRaySampleMethod::PRNG;
7,901✔
497
}
7,901✔
498

499
void write_random_ray_hdf5(hid_t group)
1,036✔
500
{
501
  hid_t random_ray_group = create_group(group, "random_ray");
1,036✔
502
  switch (RandomRay::source_shape_) {
1,036!
503
  case RandomRaySourceShape::FLAT:
816✔
504
    write_dataset(random_ray_group, "source_shape", "flat");
816✔
505
    break;
816✔
506
  case RandomRaySourceShape::LINEAR:
187✔
507
    write_dataset(random_ray_group, "source_shape", "linear");
187✔
508
    break;
187✔
509
  case RandomRaySourceShape::LINEAR_XY:
33✔
510
    write_dataset(random_ray_group, "source_shape", "linear xy");
33✔
511
    break;
33✔
NEW
512
  default:
×
NEW
513
    break;
×
514
  }
515

516
  switch (FlatSourceDomain::volume_estimator_) {
1,036!
517
  case RandomRayVolumeEstimator::SIMULATION_AVERAGED:
22✔
518
    write_dataset(random_ray_group, "volume_estimator", "simulation averaged");
22✔
519
    break;
22✔
520
  case RandomRayVolumeEstimator::NAIVE:
68✔
521
    write_dataset(random_ray_group, "volume_estimator", "naive");
68✔
522
    break;
68✔
523
  case RandomRayVolumeEstimator::HYBRID:
946✔
524
    write_dataset(random_ray_group, "volume_estimator", "hybrid");
946✔
525
    break;
946✔
NEW
526
  default:
×
NEW
527
    break;
×
528
  }
529

530
  write_dataset(
1,036✔
531
    random_ray_group, "distance_active", RandomRay::distance_active_);
532
  write_dataset(
1,036✔
533
    random_ray_group, "distance_inactive", RandomRay::distance_inactive_);
534
  write_dataset(random_ray_group, "volume_normalized_flux_tallies",
1,036✔
535
    FlatSourceDomain::volume_normalized_flux_tallies_);
536
  write_dataset(random_ray_group, "adjoint_mode", FlatSourceDomain::adjoint_);
1,036✔
537

538
  write_dataset(random_ray_group, "avg_miss_rate", RandomRay::avg_miss_rate_);
1,036✔
539
  write_dataset(
1,036✔
540
    random_ray_group, "n_source_regions", RandomRay::n_source_regions_);
541
  write_dataset(random_ray_group, "n_external_source_regions",
1,036✔
542
    RandomRay::n_external_source_regions_);
543
  write_dataset(random_ray_group, "n_geometric_intersections",
1,036✔
544
    RandomRay::total_geometric_intersections_);
545
  int64_t n_integrations =
1,036✔
546
    RandomRay::total_geometric_intersections_ * data::mg.num_energy_groups_;
1,036✔
547
  write_dataset(random_ray_group, "n_integrations", n_integrations);
1,036✔
548

549
  if (settings::kinetic_simulation && !simulation::is_initial_condition) {
1,036✔
550
    write_dataset(random_ray_group, "bd_order", RandomRay::bd_order_);
385✔
551
    switch (RandomRay::time_method_) {
385!
552
    case RandomRayTimeMethod::ISOTROPIC:
385✔
553
      write_dataset(random_ray_group, "time_method", "isotropic");
385✔
554
      break;
385✔
NEW
555
    case RandomRayTimeMethod::PROPAGATION:
×
NEW
556
      write_dataset(random_ray_group, "time_method", "propogation");
×
NEW
557
      break;
×
NEW
558
    default:
×
NEW
559
      break;
×
560
    }
561
  }
562
  close_group(random_ray_group);
1,036✔
563
}
1,036✔
564

565
//-----------------------------------------------------------------------------
566
// Non-member functions for kinetic simulations
567

568
void set_time_dependent_settings()
112✔
569
{
570
  // Reset flags
571
  simulation::is_initial_condition = false;
112✔
572
  simulation::k_eff_correction = false;
112✔
573

574
  // Set current time
575
  simulation::current_time = settings::dt;
112✔
576
}
112✔
577

578
// TODO: condense this into one function with rename_tallies_file and use char
579
// or string arguments
580
void rename_statepoint_file(int i)
672✔
581
{
582
  // Rename statepoint file
583
  std::string old_filename_ = fmt::format(
584
    "{0}statepoint.{1}.h5", settings::path_output, settings::n_batches);
672✔
585
  std::string new_filename_ =
586
    fmt::format("{0}openmc_td_simulation_{1}.h5", settings::path_output, i);
546✔
587

588
  const char* old_fname = old_filename_.c_str();
672✔
589
  const char* new_fname = new_filename_.c_str();
672✔
590
  std::rename(old_fname, new_fname);
672✔
591
}
672✔
592

593
void rename_tallies_file(int i)
672✔
594
{
595
  // Rename tallies file
596
  std::string old_filename_ =
597
    fmt::format("{0}tallies.out", settings::path_output);
672✔
598
  std::string new_filename_ =
599
    fmt::format("{0}tallies_{1}.out", settings::path_output, i);
546✔
600

601
  const char* old_fname = old_filename_.c_str();
672✔
602
  const char* new_fname = new_filename_.c_str();
672✔
603
  std::rename(old_fname, new_fname);
672✔
604
}
672✔
605

606
//==============================================================================
607
// RandomRaySimulation implementation
608
//==============================================================================
609

610
RandomRaySimulation::RandomRaySimulation()
753✔
611
  : negroups_(data::mg.num_energy_groups_),
753✔
612
    ndgroups_(data::mg.num_delayed_groups_)
753✔
613
{
614
  // There are no source sites in random ray mode, so be sure to disable to
615
  // ensure we don't attempt to write source sites to statepoint
616
  settings::source_write = false;
753✔
617

618
  // Random ray mode does not have an inner loop over generations within a
619
  // batch, so set the current gen to 1
620
  simulation::current_gen = 1;
753✔
621

622
  switch (RandomRay::source_shape_) {
753!
623
  case RandomRaySourceShape::FLAT:
449✔
624
    domain_ = make_unique<FlatSourceDomain>();
449✔
625
    break;
449✔
626
  case RandomRaySourceShape::LINEAR:
304✔
627
  case RandomRaySourceShape::LINEAR_XY:
628
    domain_ = make_unique<LinearSourceDomain>();
304✔
629
    break;
304✔
UNCOV
630
  default:
×
UNCOV
631
    fatal_error("Unknown random ray source shape");
×
632
  }
633

634
  // Convert OpenMC native MGXS into a more efficient format
635
  // internal to the random ray solver
636
  domain_->flatten_xs();
753✔
637

638
  // Initialize vectors used for the steady state
639
  if (settings::kinetic_simulation) {
753✔
640
    static_k_eff_;
641
    static_fission_rate_;
642
  }
643
}
753✔
644

645
void RandomRaySimulation::apply_fixed_sources_and_mesh_domains()
753✔
646
{
647
  domain_->apply_meshes();
753✔
648
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
753✔
649
    // Transfer external source user inputs onto random ray source regions
650
    domain_->convert_external_sources();
401✔
651
    domain_->count_external_source_regions();
401✔
652
  }
653
}
753✔
654

655
void RandomRaySimulation::prepare_fixed_sources_adjoint()
81✔
656
{
657
  domain_->source_regions_.adjoint_reset();
81✔
658
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
81✔
659
    domain_->set_adjoint_sources();
65✔
660
  }
661
}
81✔
662

663
void RandomRaySimulation::simulate()
1,506✔
664
{
665
  // Random ray power iteration loop
666
  while (simulation::current_batch < settings::n_batches) {
159,118✔
667
    // Initialize the current batch
668
    initialize_batch();
157,612✔
669
    initialize_generation();
157,612✔
670

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

675
      // Reset total starting particle weight used for normalizing tallies
676
      simulation::total_weight = 1.0;
108,362✔
677

678
      // Update source term (scattering + fission (+ delayed if kinetic))
679
      domain_->update_all_neutron_sources();
108,362✔
680

681
      // Reset scalar fluxes, iteration volume tallies, and region hit flags
682
      // to zero
683
      domain_->batch_reset();
108,362✔
684

685
      // At the beginning of the simulation, if mesh subdivision is in use, we
686
      // need to swap the main source region container into the base container,
687
      // as the main source region container will be used to hold the true
688
      // subdivided source regions. The base container will therefore only
689
      // contain the external source region information, the mesh indices,
690
      // material properties, and initial guess values for the flux/source.
691

692
      // Start timer for transport
693
      simulation::time_transport.start();
108,362✔
694

695
// Transport sweep over all random rays for the iteration
696
#pragma omp parallel for schedule(dynamic)                                     \
59,112✔
697
  reduction(+ : total_geometric_intersections_)
59,112✔
698
      for (int i = 0; i < settings::n_particles; i++) {
5,292,750✔
699
        RandomRay ray(i, domain_.get());
5,243,500✔
700
        total_geometric_intersections_ +=
5,243,500✔
701
          ray.transport_history_based_single_ray();
5,243,500✔
702
      }
5,243,500✔
703

704
      simulation::time_transport.stop();
108,362✔
705

706
      // Add any newly discovered source regions to the main source region
707
      // container.
708
      domain_->finalize_discovered_source_regions();
108,362✔
709

710
      // Normalize scalar flux and update volumes
711
      domain_->normalize_scalar_flux_and_volumes(
108,362✔
712
        settings::n_particles * RandomRay::distance_active_);
713

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

717
      // Apply transport stabilization factors
718
      domain_->apply_transport_stabilization();
108,362✔
719

720
      if (settings::run_mode == RunMode::EIGENVALUE) {
108,362✔
721
        // Compute random ray k-eff
722
        if (!settings::kinetic_simulation ||
98,780!
723
            settings::kinetic_simulation && simulation::is_initial_condition) {
96,250✔
724
          domain_->compute_k_eff();
30,030✔
725
          if (simulation::k_eff_correction) {
30,030✔
726
            static_fission_rate_.push_back(domain_->fission_rate_);
13,750✔
727
            static_k_eff_.push_back(domain_->k_eff_);
13,750✔
728
          }
729
        } else {
730
          domain_->k_eff_ = static_k_eff_[simulation::current_batch - 1];
68,750✔
731
          domain_->fission_rate_ =
68,750✔
732
            static_fission_rate_[simulation::current_batch - 1];
68,750✔
733
        }
734

735
        // Store random ray k-eff into OpenMC's native k-eff variable
736
        global_tally_tracklength = domain_->k_eff_;
98,780✔
737
      }
738

739
      // Compute precursors if delayed neutrons are turned on
740
      if (settings::kinetic_simulation && settings::create_delayed_neutrons)
108,362!
741
        domain_->compute_all_precursors();
96,250✔
742

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

746
        // Add this iteration's scalar flux estimate to final accumulated
747
        // estimate
748
        domain_->accumulate_iteration_quantities();
52,696✔
749

750
        // Use above mapping to contribute FSR flux data to appropriate
751
        // tallies
752
        domain_->random_ray_tally();
52,696✔
753
      }
754

755
      // Set phi_old = phi_new
756
      domain_->flux_swap();
108,362✔
757
      if (settings::kinetic_simulation && settings::create_delayed_neutrons) {
108,362!
758
        domain_->precursors_swap();
96,250✔
759
      }
760

761
      // Check for any obvious insabilities/nans/infs
762
      instability_check(n_hits, domain_->k_eff_, avg_miss_rate_);
108,362✔
763
    } // End MPI master work
764

765
    // Store simulation metrics
766
    RandomRay::avg_miss_rate_ = avg_miss_rate_ / settings::n_batches;
157,612✔
767
    RandomRay::total_geometric_intersections_ = total_geometric_intersections_;
157,612✔
768
    RandomRay::n_external_source_regions_ = domain_->n_external_source_regions_;
157,612✔
769
    RandomRay::n_source_regions_ = domain_->n_source_regions();
157,612✔
770

771
    // Finalize the current batch
772
    finalize_generation();
157,612✔
773
    finalize_batch();
157,612✔
774
  } // End random ray power iteration loop
775

776
  domain_->count_external_source_regions();
1,506✔
777
}
1,506✔
778

779
void RandomRaySimulation::output_simulation_results() const
1,506✔
780
{
781
  // Print random ray results
782
  if (mpi::master) {
1,506✔
783
    print_results_random_ray();
1,036✔
784
    if (model::plots.size() > 0) {
1,036!
NEW
785
      domain_->output_to_vtk();
×
786
    }
787
  }
788
}
1,506✔
789

790
// Apply a few sanity checks to catch obvious cases of numerical instability.
791
// Instability typically only occurs if ray density is extremely low.
792
void RandomRaySimulation::instability_check(
108,362✔
793
  int64_t n_hits, double k_eff, double& avg_miss_rate) const
794
{
795
  double percent_missed = ((domain_->n_source_regions() - n_hits) /
108,362✔
796
                            static_cast<double>(domain_->n_source_regions())) *
108,362✔
797
                          100.0;
108,362✔
798
  avg_miss_rate += percent_missed;
108,362✔
799

800
  if (mpi::master) {
108,362!
801
    if (percent_missed > 10.0) {
108,362✔
802
      warning(fmt::format(
957✔
803
        "Very high FSR miss rate detected ({:.3f}%). Instability may occur. "
804
        "Increase ray density by adding more rays and/or active distance.",
805
        percent_missed));
806
    } else if (percent_missed > 1.0) {
107,405✔
807
      warning(
2!
808
        fmt::format("Elevated FSR miss rate detected ({:.3f}%). Increasing "
4!
809
                    "ray density by adding more rays and/or active "
810
                    "distance may improve simulation efficiency.",
811
          percent_missed));
812
    }
813

814
    if (k_eff > 10.0 || k_eff < 0.01 || !(std::isfinite(k_eff))) {
108,362!
NEW
815
      fatal_error(fmt::format("Instability detected: k-eff = {:.5f}", k_eff));
×
816
    }
817
  }
818
}
108,362✔
819

820
// Print random ray simulation results
821
void RandomRaySimulation::print_results_random_ray() const
1,036✔
822
{
823
  using namespace simulation;
824

825
  if (settings::verbosity >= 6) {
1,036!
826
    double total_integrations =
1,036✔
827
      RandomRay::total_geometric_intersections_ * negroups_;
1,036✔
828
    double time_per_integration =
829
      simulation::time_transport.elapsed() / total_integrations;
1,036✔
830
    double misc_time = time_total.elapsed() - time_update_src.elapsed() -
1,036✔
831
                       time_transport.elapsed() - time_tallies.elapsed() -
1,036✔
832
                       time_bank_sendrecv.elapsed();
1,036✔
833

834
    if (settings::kinetic_simulation && !simulation::is_initial_condition) {
1,036✔
835
      misc_time -= time_update_bd_vectors.elapsed();
385✔
836
    }
837
    header("Simulation Statistics", 4);
1,036✔
838
    fmt::print(
1,036✔
839
      " Total Iterations                  = {}\n", settings::n_batches);
840
    fmt::print(
1,036✔
841
      " Number of Rays per Iteration      = {}\n", settings::n_particles);
842
    fmt::print(" Inactive Distance                 = {} cm\n",
1,036✔
843
      RandomRay::distance_inactive_);
844
    fmt::print(" Active Distance                   = {} cm\n",
1,036✔
845
      RandomRay::distance_active_);
846
    fmt::print(" Source Regions (SRs)              = {}\n",
1,036✔
847
      RandomRay::n_source_regions_);
848
    fmt::print(" SRs Containing External Sources   = {}\n",
848✔
849
      RandomRay::n_external_source_regions_);
850
    fmt::print(" Total Geometric Intersections     = {:.4e}\n",
848✔
851
      static_cast<double>(RandomRay::total_geometric_intersections_));
1,036✔
852
    fmt::print("   Avg per Iteration               = {:.4e}\n",
848✔
853
      static_cast<double>(RandomRay::total_geometric_intersections_) /
1,036✔
854
        settings::n_batches);
855
    fmt::print("   Avg per Iteration per SR        = {:.2f}\n",
848✔
856
      static_cast<double>(RandomRay::total_geometric_intersections_) /
1,036✔
857
        static_cast<double>(settings::n_batches) /
2,072✔
858
        RandomRay::n_source_regions_);
859
    fmt::print(" Avg SR Miss Rate per Iteration    = {:.4f}%\n",
848✔
860
      RandomRay::avg_miss_rate_);
861
    fmt::print(" Energy Groups                     = {}\n", negroups_);
1,884✔
862
    if (settings::kinetic_simulation)
1,036✔
863
      fmt::print(" Delay Groups                      = {}\n", ndgroups_);
1,078✔
864
    fmt::print(
848✔
865
      " Total Integrations                = {:.4e}\n", total_integrations);
866
    fmt::print("   Avg per Iteration               = {:.4e}\n",
848✔
867
      total_integrations / settings::n_batches);
1,036✔
868

869
    std::string estimator;
1,036✔
870
    switch (domain_->volume_estimator_) {
1,036!
871
    case RandomRayVolumeEstimator::SIMULATION_AVERAGED:
22✔
872
      estimator = "Simulation Averaged";
22✔
873
      break;
22✔
874
    case RandomRayVolumeEstimator::NAIVE:
68✔
875
      estimator = "Naive";
68✔
876
      break;
68✔
877
    case RandomRayVolumeEstimator::HYBRID:
946✔
878
      estimator = "Hybrid";
946✔
879
      break;
946✔
NEW
880
    default:
×
NEW
881
      fatal_error("Invalid volume estimator type");
×
882
    }
883
    fmt::print(" Volume Estimator Type             = {}\n", estimator);
848✔
884

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

888
    std::string shape;
2,072✔
889
    switch (RandomRay::source_shape_) {
1,036!
890
    case RandomRaySourceShape::FLAT:
816✔
891
      shape = "Flat";
816✔
892
      break;
816✔
893
    case RandomRaySourceShape::LINEAR:
187✔
894
      shape = "Linear";
187✔
895
      break;
187✔
896
    case RandomRaySourceShape::LINEAR_XY:
33✔
897
      shape = "Linear XY";
33✔
898
      break;
33✔
UNCOV
899
    default:
×
UNCOV
900
      fatal_error("Invalid random ray source shape");
×
901
    }
902
    fmt::print(" Source Shape                      = {}\n", shape);
848✔
903
    std::string sample_method =
904
      (RandomRay::sample_method_ == RandomRaySampleMethod::PRNG) ? "PRNG"
1,036✔
905
                                                                 : "Halton";
3,108✔
906
    fmt::print(" Sample Method                     = {}\n", sample_method);
848✔
907

908
    if (domain_->is_transport_stabilization_needed_) {
1,036✔
909
      fmt::print(" Transport XS Stabilization Used   = YES (rho = {:.3f})\n",
88✔
910
        FlatSourceDomain::diagonal_stabilization_rho_);
911
    } else {
912
      fmt::print(" Transport XS Stabilization Used   = NO\n");
948✔
913
    }
914
    if (settings::kinetic_simulation && !simulation::is_initial_condition) {
1,036✔
915
      std::string time_method =
916
        (RandomRay::time_method_ == RandomRayTimeMethod::ISOTROPIC)
385✔
917
          ? "ISOTROPIC"
918
          : "PROPAGATION";
770!
919
      fmt::print(" Time Method                       = {}\n", time_method);
385✔
920
      fmt::print(
315✔
921
        " Backwards Difference Order        = {}\n", RandomRay::bd_order_);
922
    }
385✔
923

924
    header("Timing Statistics", 4);
1,036✔
925
    show_time("Total time for initialization", time_initialize.elapsed());
1,036✔
926
    show_time("Reading cross sections", time_read_xs.elapsed(), 1);
1,036✔
927
    show_time("Total simulation time", time_total.elapsed());
1,036✔
928
    show_time("Transport sweep only", time_transport.elapsed(), 1);
1,036✔
929
    show_time("Source update only", time_update_src.elapsed(), 1);
1,036✔
930
    if (settings::kinetic_simulation && settings::create_delayed_neutrons) {
1,036!
931
      show_time(
539✔
932
        "Precursor computation only", time_compute_precursors.elapsed(), 1);
933
      misc_time -= time_compute_precursors.elapsed();
539✔
934
    }
935
    show_time("Tally conversion only", time_tallies.elapsed(), 1);
1,036✔
936
    show_time("MPI source reductions only", time_bank_sendrecv.elapsed(), 1);
1,036✔
937
    show_time("Other iteration routines", misc_time, 1);
1,036✔
938
    if (settings::run_mode == RunMode::EIGENVALUE) {
1,036✔
939
      show_time("Time in inactive batches", time_inactive.elapsed());
715✔
940
    }
941
    show_time("Time in active batches", time_active.elapsed());
1,036✔
942
    show_time("Time writing statepoints", time_statepoint.elapsed());
1,036✔
943
    show_time("Total time for finalization", time_finalize.elapsed());
1,036✔
944
    show_time("Time per integration", time_per_integration);
1,036✔
945
  }
1,036✔
946

947
  if (settings::verbosity >= 4 && settings::run_mode == RunMode::EIGENVALUE) {
1,036!
948
    header("Results", 4);
715✔
949
    fmt::print(" k-effective                       = {:.5f} +/- {:.5f}\n",
715✔
950
      simulation::keff, simulation::keff_std);
951
  }
952
}
1,036✔
953

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