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

openmc-dev / openmc / 21296750350

23 Jan 2026 06:25PM UTC coverage: 81.966% (-0.01%) from 81.977%
21296750350

Pull #3702

github

web-flow
Merge 8d3c7acd6 into 3e2f1f521
Pull Request #3702: Random Ray Kinetic Simulation Mode

17712 of 24657 branches covered (71.83%)

Branch coverage included in aggregate %.

902 of 1043 new or added lines in 20 files covered. (86.48%)

83 existing lines in 10 files now uncovered.

56501 of 65884 relevant lines covered (85.76%)

52138177.9 hits per line

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

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

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

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

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

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

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

52
  // Run initial random ray simulation
53
  sim.simulate();
799✔
54

55
  if (settings::kinetic_simulation) {
799✔
56
    // Timestepping loop, including k-eff correction initial
57
    // condition (i = -1)
58
    for (int i = -1; i < settings::n_timesteps; i++)
1,176✔
59
      sim.kinetic_single_time_step(i);
1,008✔
60
  }
61

62
  //////////////////////////////////////////////////////////
63
  // Run adjoint simulation (if enabled)
64
  //////////////////////////////////////////////////////////
65

66
  if (sim.adjoint_needed_) {
799✔
67
    // Setup for adjoint simulation
68
    sim.prepare_adjoint_simulation();
71✔
69

70
    // Run adjoint simulation
71
    sim.simulate();
71✔
72
  }
73
}
799✔
74

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

85
    // Validate score types
86
    for (auto score_bin : tally->scores_) {
2,162✔
87
      switch (score_bin) {
1,211!
88
      case SCORE_FLUX:
1,171✔
89
      case SCORE_TOTAL:
90
      case SCORE_FISSION:
91
      case SCORE_NU_FISSION:
92
      case SCORE_EVENTS:
93
      case SCORE_KAPPA_FISSION:
94
        break;
1,171✔
95

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

114
    // Validate filter types
115
    for (auto f : tally->filters()) {
2,074✔
116
      auto& filter = *model::tally_filters[f];
1,123✔
117

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

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

176
  // Validate ray source
177
  ///////////////////////////////////////////////////////////////////
178

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

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

195
  // Check that box source is not restricted to fissionable areas
196
  if (sb->only_fissionable()) {
571!
197
    fatal_error(
×
198
      "Invalid ray source definition -- fissionable spatial distribution "
199
      "not allowed.");
200
  }
201

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

210
  // Validate external sources
211
  ///////////////////////////////////////////////////////////////////
212
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
571✔
213
    if (model::external_sources.size() < 1) {
261!
214
      fatal_error("Must provide a particle source (in addition to ray source) "
×
215
                  "in fixed source random ray mode.");
216
    }
217

218
    for (int i = 0; i < model::external_sources.size(); i++) {
522✔
219
      Source* s = model::external_sources[i].get();
261✔
220

221
      // Check for independent source
222
      IndependentSource* is = dynamic_cast<IndependentSource*>(s);
261!
223

224
      if (!is) {
261!
225
        fatal_error(
×
226
          "Only IndependentSource external source types are allowed in "
227
          "random ray mode");
228
      }
229

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

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

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

267
  // Validate plotting files
268
  ///////////////////////////////////////////////////////////////////
269
  for (int p = 0; p < model::plots.size(); p++) {
571!
270

271
    // Get handle to OpenMC plot object
272
    const auto& openmc_plottable = model::plots[p];
×
273
    Plot* openmc_plot = dynamic_cast<Plot*>(openmc_plottable.get());
×
274

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

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

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

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

328
void write_random_ray_hdf5(hid_t group)
1,342✔
329
{
330
  hid_t random_ray_group = create_group(group, "random_ray");
1,342✔
331
  switch (RandomRay::source_shape_) {
1,342!
332
  case RandomRaySourceShape::FLAT:
1,112✔
333
    write_dataset(random_ray_group, "source_shape", "flat");
1,112✔
334
    break;
1,112✔
335
  case RandomRaySourceShape::LINEAR:
200✔
336
    write_dataset(random_ray_group, "source_shape", "linear");
200✔
337
    break;
200✔
338
  case RandomRaySourceShape::LINEAR_XY:
30✔
339
    write_dataset(random_ray_group, "source_shape", "linear xy");
30✔
340
    break;
30✔
NEW
341
  default:
×
NEW
342
    break;
×
343
  }
344

345
  switch (FlatSourceDomain::volume_estimator_) {
1,342!
346
  case RandomRayVolumeEstimator::SIMULATION_AVERAGED:
20✔
347
    write_dataset(random_ray_group, "volume_estimator", "simulation averaged");
20✔
348
    break;
20✔
349
  case RandomRayVolumeEstimator::NAIVE:
62✔
350
    write_dataset(random_ray_group, "volume_estimator", "naive");
62✔
351
    break;
62✔
352
  case RandomRayVolumeEstimator::HYBRID:
1,260✔
353
    write_dataset(random_ray_group, "volume_estimator", "hybrid");
1,260✔
354
    break;
1,260✔
NEW
355
  default:
×
NEW
356
    break;
×
357
  }
358

359
  write_dataset(
1,342✔
360
    random_ray_group, "distance_active", RandomRay::distance_active_);
361
  write_dataset(
1,342✔
362
    random_ray_group, "distance_inactive", RandomRay::distance_inactive_);
363
  write_dataset(random_ray_group, "volume_normalized_flux_tallies",
1,342✔
364
    FlatSourceDomain::volume_normalized_flux_tallies_);
365
  write_dataset(random_ray_group, "adjoint_mode", FlatSourceDomain::adjoint_);
1,342✔
366

367
  write_dataset(random_ray_group, "avg_miss_rate", RandomRay::avg_miss_rate_);
1,342✔
368
  write_dataset(
1,342✔
369
    random_ray_group, "n_source_regions", RandomRay::n_source_regions_);
370
  write_dataset(random_ray_group, "n_external_source_regions",
1,342✔
371
    RandomRay::n_external_source_regions_);
372
  write_dataset(random_ray_group, "n_geometric_intersections",
1,342✔
373
    RandomRay::total_geometric_intersections_);
374
  int64_t n_integrations =
1,342✔
375
    RandomRay::total_geometric_intersections_ * data::mg.num_energy_groups_;
1,342✔
376
  write_dataset(random_ray_group, "n_integrations", n_integrations);
1,342✔
377

378
  if (settings::kinetic_simulation && !simulation::is_initial_condition) {
1,342✔
379
    write_dataset(random_ray_group, "bd_order", RandomRay::bd_order_);
600✔
380
    switch (RandomRay::time_method_) {
600!
381
    case RandomRayTimeMethod::ISOTROPIC:
400✔
382
      write_dataset(random_ray_group, "time_method", "isotropic");
400✔
383
      break;
400✔
384
    case RandomRayTimeMethod::PROPAGATION:
200✔
385
      write_dataset(random_ray_group, "time_method", "propogation");
200✔
386
      break;
200✔
NEW
387
    default:
×
NEW
388
      break;
×
389
    }
390
  }
391
  close_group(random_ray_group);
1,342✔
392
}
1,342✔
393

394
void print_adjoint_header()
102✔
395
{
396
  if (!FlatSourceDomain::adjoint_)
102✔
397
    // If we're going to do an adjoint simulation afterwards, report that this
398
    // is the initial forward flux solve.
399
    header("FORWARD FLUX SOLVE", 3);
51✔
400
  else
401
    // Otherwise report that we are doing the adjoint simulation
402
    header("ADJOINT FLUX SOLVE", 3);
51✔
403
}
102✔
404

405
//-----------------------------------------------------------------------------
406
// Non-member functions for kinetic simulations
407

408
void rename_time_step_file(
2,016✔
409
  std::string base_filename, std::string extension, int i)
410
{
411
  // Rename file
412
  std::string old_filename_ =
413
    fmt::format("{0}{1}{2}", settings::path_output, base_filename, extension);
2,016✔
414
  std::string new_filename_ = fmt::format(
415
    "{0}{1}_{2}{3}", settings::path_output, base_filename, i, extension);
1,584✔
416

417
  const char* old_fname = old_filename_.c_str();
2,016✔
418
  const char* new_fname = new_filename_.c_str();
2,016✔
419
  std::rename(old_fname, new_fname);
2,016✔
420
}
2,016✔
421

422
//==============================================================================
423
// RandomRaySimulation implementation
424
//==============================================================================
425

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

434
  // Random ray mode does not have an inner loop over generations within a
435
  // batch, so set the current gen to 1
436
  simulation::current_gen = 1;
799✔
437

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

450
  // Convert OpenMC native MGXS into a more efficient format
451
  // internal to the random ray solver
452
  domain_->flatten_xs();
799✔
453

454
  // Check if adjoint calculation is needed. If it is, we will run the forward
455
  // calculation first and then the adjoint calculation later.
456
  adjoint_needed_ = FlatSourceDomain::adjoint_;
799✔
457

458
  // Adjoint is always false for the forward calculation
459
  FlatSourceDomain::adjoint_ = false;
799✔
460

461
  // The first simulation is run after initialization
462
  is_first_simulation_ = true;
799✔
463

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

474
void RandomRaySimulation::apply_fixed_sources_and_mesh_domains()
799✔
475
{
476
  domain_->apply_meshes();
799✔
477
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
799✔
478
    // Transfer external source user inputs onto random ray source regions
479
    domain_->convert_external_sources();
365✔
480
    domain_->count_external_source_regions();
365✔
481
  }
482
}
799✔
483

484
void RandomRaySimulation::prepare_fixed_sources_adjoint()
71✔
485
{
486
  domain_->source_regions_.adjoint_reset();
71✔
487
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
71✔
488
    domain_->set_adjoint_sources();
57✔
489
  }
490
}
71✔
491

492
void RandomRaySimulation::prepare_adjoint_simulation()
71✔
493
{
494
  // Configure the domain for adjoint simulation
495
  FlatSourceDomain::adjoint_ = true;
71✔
496

497
  // Reset k-eff
498
  domain_->k_eff_ = 1.0;
71✔
499

500
  // Initialize adjoint fixed sources, if present
501
  prepare_fixed_sources_adjoint();
71✔
502

503
  // Transpose scattering matrix
504
  domain_->transpose_scattering_matrix();
71✔
505

506
  // Swap nu_sigma_f and chi
507
  domain_->nu_sigma_f_.swap(domain_->chi_);
71✔
508
}
71✔
509
  
510
// TODO: Add support for time-dependent restart
511
void RandomRaySimulation::kinetic_single_time_step(int i)
1,008✔
512
{
513
  if (i == -1) {
1,008✔
514
    // Set flag for k_eff correction if initial condition
515
    simulation::k_eff_correction = true;
168✔
516
    
517
    // Store average keff from initial simulation
518
    static_avg_k_eff_ = simulation::keff;
168✔
519
  }
520
  
521
  // Increment time step
522
  simulation::current_timestep = i + 1;
1,008✔
523
  if (i == -1)
1,008✔
524
    // Current time is zero for initial condition
525
    simulation::current_time = 0;
168✔
526
  else
527
    // Else, increment the current time
528
    simulation::current_time += settings::dt;
840✔
529

530
  domain_->k_eff_ = static_avg_k_eff_;
1,008✔
531
  domain_->source_regions_.adjoint_reset();
1,008✔
532
  domain_->propagate_final_quantities();
1,008✔
533
  domain_->source_regions_.time_step_reset();
1,008✔
534
  
535
  if (i >= 0) {
1,008✔
536
    // Compute RHS backward differences
537
    domain_->compute_rhs_bd_quantities();
840✔
538

539
    // Update time dependent cross section based on the density
540
    domain_->update_material_density(i);
840✔
541
  }
542

543
  // Run the initial condition
544
  simulate();
1,008✔
545

546

547
  if (i == -1)
1,008✔
548
    // Initialize the BD arrays if initial condition
549
    domain_->store_time_step_quantities(false);
168✔
550
  else
551
    // Else, store final quantities for the current time step
552
    domain_->store_time_step_quantities();
840✔
553

554
  // Rename statepoint and tallies file for the current time step
555
  rename_time_step_file(fmt::format("statepoint.{0}", settings::n_batches),
2,016✔
556
    ".h5", simulation::current_timestep);
557
  if (settings::output_tallies) {
1,008!
558
    rename_time_step_file("tallies", ".out", simulation::current_timestep);
1,008✔
559
  }
560

561
  if (i == -1) {
1,008✔
562
    // Reset flags for kinetic simulation if initial condition
563
    simulation::is_initial_condition = false;
168✔
564
    simulation::k_eff_correction = false;
168✔
565
  }
566
}
1,008✔
567

568
void RandomRaySimulation::simulate()
1,878✔
569
{
570
  if (!is_first_simulation_) {
1,878✔
571
    if (mpi::master && adjoint_needed_)
1,079✔
572
      openmc::print_adjoint_header();
51✔
573

574
    // Reset the timers and reinitialize the general OpenMC datastructures if
575
    // this is after the first simulation
576
    reset_timers();
1,079✔
577

578
    // Initialize OpenMC general data structures
579
    openmc_simulation_init();
1,079✔
580
  }
581

582
  // Begin main simulation timer
583
  simulation::time_total.start();
1,878✔
584

585
  // Random ray power iteration loop
586
  while (simulation::current_batch < settings::n_batches) {
184,590✔
587
    // Initialize the current batch
588
    initialize_batch();
182,712✔
589
    initialize_generation();
182,712✔
590

591
    // MPI not supported in random ray solver, so all work is done by rank 0
592
    // TODO: Implement domain decomposition for MPI parallelism
593
    if (mpi::master) {
182,712✔
594

595
      // Reset total starting particle weight used for normalizing tallies
596
      simulation::total_weight = 1.0;
130,512✔
597

598
      // Update source term (scattering + fission (+ delayed if kinetic))
599
      domain_->update_all_neutron_sources();
130,512✔
600

601
      // Reset scalar fluxes, iteration volume tallies, and region hit flags
602
      // to zero
603
      domain_->batch_reset();
130,512✔
604

605
      // At the beginning of the simulation, if mesh subdivision is in use, we
606
      // need to swap the main source region container into the base container,
607
      // as the main source region container will be used to hold the true
608
      // subdivided source regions. The base container will therefore only
609
      // contain the external source region information, the mesh indices,
610
      // material properties, and initial guess values for the flux/source.
611

612
      // Start timer for transport
613
      simulation::time_transport.start();
130,512✔
614

615
// Transport sweep over all random rays for the iteration
616
#pragma omp parallel for schedule(dynamic)                                     \
65,262✔
617
  reduction(+ : total_geometric_intersections_)
65,262✔
618
      for (int i = 0; i < settings::n_particles; i++) {
6,908,250✔
619
        RandomRay ray(i, domain_.get());
6,843,000✔
620
        total_geometric_intersections_ +=
6,843,000✔
621
          ray.transport_history_based_single_ray();
6,843,000✔
622
      }
6,843,000✔
623

624
      simulation::time_transport.stop();
130,512✔
625

626
      // Add any newly discovered source regions to the main source region
627
      // container.
628
      domain_->finalize_discovered_source_regions();
130,512✔
629

630
      // Normalize scalar flux and update volumes
631
      domain_->normalize_scalar_flux_and_volumes(
130,512✔
632
        settings::n_particles * RandomRay::distance_active_);
633

634
      // Add source to scalar flux, compute number of FSR hits
635
      int64_t n_hits = domain_->add_source_to_scalar_flux();
130,512✔
636

637
      // Apply transport stabilization factors
638
      domain_->apply_transport_stabilization();
130,512✔
639

640
      if (settings::run_mode == RunMode::EIGENVALUE) {
130,512✔
641
        // Compute random ray k-eff
642
        if (!settings::kinetic_simulation ||
121,700!
643
            settings::kinetic_simulation && simulation::is_initial_condition) {
119,000✔
644
          domain_->compute_k_eff();
36,700✔
645
          if (simulation::k_eff_correction) {
36,700✔
646
            static_fission_rate_.push_back(domain_->fission_rate_);
17,000✔
647
            static_k_eff_.push_back(domain_->k_eff_);
17,000✔
648
          }
649
        } else {
650
          domain_->k_eff_ = static_k_eff_[simulation::current_batch - 1];
85,000✔
651
          domain_->fission_rate_ =
85,000✔
652
            static_fission_rate_[simulation::current_batch - 1];
85,000✔
653
        }
654

655
        // Store random ray k-eff into OpenMC's native k-eff variable
656
        global_tally_tracklength = domain_->k_eff_;
121,700✔
657
      }
658

659
      // Compute precursors if delayed neutrons are turned on
660
      if (settings::kinetic_simulation && settings::create_delayed_neutrons)
130,512!
661
        domain_->compute_all_precursors();
119,000✔
662

663
      // Execute all tallying tasks, if this is an active batch
664
      if (simulation::current_batch > settings::n_inactive) {
130,512✔
665

666
        // Add this iteration's scalar flux estimate to final accumulated
667
        // estimate
668
        domain_->accumulate_iteration_quantities();
63,556✔
669

670
        // Use above mapping to contribute FSR flux data to appropriate
671
        // tallies
672
        domain_->random_ray_tally();
63,556✔
673
      }
674

675
      // Set phi_old = phi_new
676
      domain_->flux_swap();
130,512✔
677
      if (settings::kinetic_simulation && settings::create_delayed_neutrons) {
130,512!
678
        domain_->precursors_swap();
119,000✔
679
      }
680

681
      // Check for any obvious insabilities/nans/infs
682
      instability_check(n_hits, domain_->k_eff_, avg_miss_rate_);
130,512✔
683
    } // End MPI master work
684

685
    // Store simulation metrics
686
    RandomRay::avg_miss_rate_ = avg_miss_rate_ / settings::n_batches;
182,712✔
687
    RandomRay::total_geometric_intersections_ = total_geometric_intersections_;
182,712✔
688
    RandomRay::n_external_source_regions_ = domain_->n_external_source_regions_;
182,712✔
689
    RandomRay::n_source_regions_ = domain_->n_source_regions();
182,712✔
690

691
    // Finalize the current batch
692
    finalize_generation();
182,712✔
693
    finalize_batch();
182,712✔
694
  } // End random ray power iteration loop
695

696
  domain_->count_external_source_regions();
1,878✔
697

698
  // End main simulation timer
699
  simulation::time_total.stop();
1,878✔
700

701
  // Normalize and save the final forward quantities
702
  domain_->normalize_final_quantities();
1,878✔
703

704
  // Finalize OpenMC
705
  openmc_simulation_finalize();
1,878✔
706

707
  // Output all simulation results
708
  output_simulation_results();
1,878✔
709

710
  // Toggle that the simulation object has been initialized after the first
711
  // simulation
712
  if (is_first_simulation_)
1,878✔
713
    is_first_simulation_ = false;
799✔
714
}
1,878✔
715

716
void RandomRaySimulation::output_simulation_results() const
1,878✔
717
{
718
  // Print random ray results
719
  if (mpi::master) {
1,878✔
720
    print_results_random_ray();
1,342✔
721
    if (model::plots.size() > 0) {
1,342!
UNCOV
722
      domain_->output_to_vtk();
×
723
    }
724
  }
725
}
1,878✔
726

727
// Apply a few sanity checks to catch obvious cases of numerical instability.
728
// Instability typically only occurs if ray density is extremely low.
729
void RandomRaySimulation::instability_check(
130,512✔
730
  int64_t n_hits, double k_eff, double& avg_miss_rate) const
731
{
732
  double percent_missed = ((domain_->n_source_regions() - n_hits) /
130,512✔
733
                            static_cast<double>(domain_->n_source_regions())) *
130,512✔
734
                          100.0;
130,512✔
735
  avg_miss_rate += percent_missed;
130,512✔
736

737
  if (mpi::master) {
130,512!
738
    if (percent_missed > 10.0) {
130,512✔
739
      warning(fmt::format(
870✔
740
        "Very high FSR miss rate detected ({:.3f}%). Instability may occur. "
741
        "Increase ray density by adding more rays and/or active distance.",
742
        percent_missed));
743
    } else if (percent_missed > 1.0) {
129,642✔
744
      warning(
2!
745
        fmt::format("Elevated FSR miss rate detected ({:.3f}%). Increasing "
4!
746
                    "ray density by adding more rays and/or active "
747
                    "distance may improve simulation efficiency.",
748
          percent_missed));
749
    }
750

751
    if (k_eff > 10.0 || k_eff < 0.01 || !(std::isfinite(k_eff))) {
130,512!
UNCOV
752
      fatal_error(fmt::format("Instability detected: k-eff = {:.5f}", k_eff));
×
753
    }
754
  }
755
}
130,512✔
756

757
// Print random ray simulation results
758
void RandomRaySimulation::print_results_random_ray() const
1,342✔
759
{
760
  using namespace simulation;
761

762
  if (settings::verbosity >= 6) {
1,342!
763
    double total_integrations =
1,342✔
764
      RandomRay::total_geometric_intersections_ * negroups_;
1,342✔
765
    double time_per_integration =
766
      simulation::time_transport.elapsed() / total_integrations;
1,342✔
767
    double misc_time = time_total.elapsed() - time_update_src.elapsed() -
1,342✔
768
                       time_transport.elapsed() - time_tallies.elapsed() -
1,342✔
769
                       time_bank_sendrecv.elapsed();
1,342✔
770

771
    if (settings::kinetic_simulation && !simulation::is_initial_condition) {
1,342✔
772
      misc_time -= time_update_bd_vectors.elapsed();
600✔
773
    }
774
    header("Simulation Statistics", 4);
1,342✔
775
    fmt::print(
1,342✔
776
      " Total Iterations                  = {}\n", settings::n_batches);
777
    fmt::print(
1,342✔
778
      " Number of Rays per Iteration      = {}\n", settings::n_particles);
779
    fmt::print(" Inactive Distance                 = {} cm\n",
1,342✔
780
      RandomRay::distance_inactive_);
781
    fmt::print(" Active Distance                   = {} cm\n",
1,342✔
782
      RandomRay::distance_active_);
783
    fmt::print(" Source Regions (SRs)              = {}\n",
1,342✔
784
      RandomRay::n_source_regions_);
785
    fmt::print(" SRs Containing External Sources   = {}\n",
1,074✔
786
      RandomRay::n_external_source_regions_);
787
    fmt::print(" Total Geometric Intersections     = {:.4e}\n",
1,074✔
788
      static_cast<double>(RandomRay::total_geometric_intersections_));
1,342✔
789
    fmt::print("   Avg per Iteration               = {:.4e}\n",
1,074✔
790
      static_cast<double>(RandomRay::total_geometric_intersections_) /
1,342✔
791
        settings::n_batches);
792
    fmt::print("   Avg per Iteration per SR        = {:.2f}\n",
1,074✔
793
      static_cast<double>(RandomRay::total_geometric_intersections_) /
1,342✔
794
        static_cast<double>(settings::n_batches) /
2,684✔
795
        RandomRay::n_source_regions_);
796
    fmt::print(" Avg SR Miss Rate per Iteration    = {:.4f}%\n",
1,074✔
797
      RandomRay::avg_miss_rate_);
798
    fmt::print(" Energy Groups                     = {}\n", negroups_);
2,416✔
799
    if (settings::kinetic_simulation)
1,342✔
800
      fmt::print(" Delay Groups                      = {}\n", ndgroups_);
1,680✔
801
    fmt::print(
1,074✔
802
      " Total Integrations                = {:.4e}\n", total_integrations);
803
    fmt::print("   Avg per Iteration               = {:.4e}\n",
1,074✔
804
      total_integrations / settings::n_batches);
1,342✔
805

806
    std::string estimator;
1,342✔
807
    switch (domain_->volume_estimator_) {
1,342!
808
    case RandomRayVolumeEstimator::SIMULATION_AVERAGED:
20✔
809
      estimator = "Simulation Averaged";
20✔
810
      break;
20✔
811
    case RandomRayVolumeEstimator::NAIVE:
62✔
812
      estimator = "Naive";
62✔
813
      break;
62✔
814
    case RandomRayVolumeEstimator::HYBRID:
1,260✔
815
      estimator = "Hybrid";
1,260✔
816
      break;
1,260✔
817
    default:
×
UNCOV
818
      fatal_error("Invalid volume estimator type");
×
819
    }
820
    fmt::print(" Volume Estimator Type             = {}\n", estimator);
1,074✔
821

822
    std::string adjoint_true = (FlatSourceDomain::adjoint_) ? "ON" : "OFF";
4,026✔
823
    fmt::print(" Adjoint Flux Mode                 = {}\n", adjoint_true);
1,074✔
824

825
    std::string shape;
2,684✔
826
    switch (RandomRay::source_shape_) {
1,342!
827
    case RandomRaySourceShape::FLAT:
1,112✔
828
      shape = "Flat";
1,112✔
829
      break;
1,112✔
830
    case RandomRaySourceShape::LINEAR:
200✔
831
      shape = "Linear";
200✔
832
      break;
200✔
833
    case RandomRaySourceShape::LINEAR_XY:
30✔
834
      shape = "Linear XY";
30✔
835
      break;
30✔
836
    default:
×
UNCOV
837
      fatal_error("Invalid random ray source shape");
×
838
    }
839
    fmt::print(" Source Shape                      = {}\n", shape);
1,074✔
840
    std::string sample_method =
841
      (RandomRay::sample_method_ == RandomRaySampleMethod::PRNG) ? "PRNG"
1,342✔
842
                                                                 : "Halton";
4,026✔
843
    fmt::print(" Sample Method                     = {}\n", sample_method);
1,074✔
844

845
    if (domain_->is_transport_stabilization_needed_) {
1,342✔
846
      fmt::print(" Transport XS Stabilization Used   = YES (rho = {:.3f})\n",
150✔
847
        FlatSourceDomain::diagonal_stabilization_rho_);
848
    } else {
849
      fmt::print(" Transport XS Stabilization Used   = NO\n");
1,192✔
850
    }
851
    if (settings::kinetic_simulation && !simulation::is_initial_condition) {
1,342✔
852
      std::string time_method =
853
        (RandomRay::time_method_ == RandomRayTimeMethod::ISOTROPIC)
600✔
854
          ? "ISOTROPIC"
855
          : "PROPAGATION";
1,200✔
856
      fmt::print(" Time Method                       = {}\n", time_method);
600✔
857
      fmt::print(
480✔
858
        " Backwards Difference Order        = {}\n", RandomRay::bd_order_);
859
    }
600✔
860

861
    header("Timing Statistics", 4);
1,342✔
862
    show_time("Total time for initialization", time_initialize.elapsed());
1,342✔
863
    show_time("Reading cross sections", time_read_xs.elapsed(), 1);
1,342✔
864
    show_time("Total simulation time", time_total.elapsed());
1,342✔
865
    show_time("Transport sweep only", time_transport.elapsed(), 1);
1,342✔
866
    show_time("Source update only", time_update_src.elapsed(), 1);
1,342✔
867
    if (settings::kinetic_simulation && settings::create_delayed_neutrons) {
1,342!
868
      show_time(
840✔
869
        "Precursor computation only", time_compute_precursors.elapsed(), 1);
870
      misc_time -= time_compute_precursors.elapsed();
840✔
871
    }
872
    show_time("Tally conversion only", time_tallies.elapsed(), 1);
1,342✔
873
    show_time("MPI source reductions only", time_bank_sendrecv.elapsed(), 1);
1,342✔
874
    show_time("Other iteration routines", misc_time, 1);
1,342✔
875
    if (settings::run_mode == RunMode::EIGENVALUE) {
1,342✔
876
      show_time("Time in inactive batches", time_inactive.elapsed());
1,040✔
877
    }
878
    show_time("Time in active batches", time_active.elapsed());
1,342✔
879
    show_time("Time writing statepoints", time_statepoint.elapsed());
1,342✔
880
    show_time("Total time for finalization", time_finalize.elapsed());
1,342✔
881
    show_time("Time per integration", time_per_integration);
1,342✔
882
  }
1,342✔
883

884
  if (settings::verbosity >= 4 && settings::run_mode == RunMode::EIGENVALUE) {
1,342!
885
    header("Results", 4);
1,040✔
886
    fmt::print(" k-effective                       = {:.5f} +/- {:.5f}\n",
1,040✔
887
      simulation::keff, simulation::keff_std);
888
  }
889
}
1,342✔
890

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