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

openmc-dev / openmc / 29958310732

22 Jul 2026 09:12PM UTC coverage: 81.403% (+0.05%) from 81.356%
29958310732

Pull #3956

github

web-flow
Merge 6ca1ce7ba into 54b661d39
Pull Request #3956: Add description attribute to Model

18380 of 26608 branches covered (69.08%)

Branch coverage included in aggregate %.

11 of 11 new or added lines in 1 file covered. (100.0%)

1077 existing lines in 35 files now uncovered.

59961 of 69630 relevant lines covered (86.11%)

49150910.1 hits per line

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

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

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

20
namespace openmc {
21

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

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

36
    // Validate score types
37
    for (auto score_bin : tally->scores_) {
2,444✔
38
      switch (score_bin) {
1,343!
39
      case SCORE_FLUX:
1,343✔
40
      case SCORE_TOTAL:
1,343✔
41
      case SCORE_FISSION:
1,343✔
42
      case SCORE_NU_FISSION:
1,343✔
43
      case SCORE_EVENTS:
1,343✔
44
      case SCORE_KAPPA_FISSION:
1,343✔
45
        break;
1,343✔
46
      default:
×
47
        fatal_error(
×
48
          "Invalid score specified. Only flux, total, fission, nu-fission, "
49
          "kappa-fission, and event scores are supported in random ray mode.");
50
      }
51
    }
52

53
    // Validate filter types
54
    for (auto f : tally->filters()) {
2,435✔
55
      auto& filter = *model::tally_filters[f];
1,334✔
56

57
      switch (filter.type()) {
1,334!
58
      case FilterType::CELL:
1,334✔
59
      case FilterType::CELL_INSTANCE:
1,334✔
60
      case FilterType::DISTRIBCELL:
1,334✔
61
      case FilterType::ENERGY:
1,334✔
62
      case FilterType::MATERIAL:
1,334✔
63
      case FilterType::MESH:
1,334✔
64
      case FilterType::UNIVERSE:
1,334✔
65
      case FilterType::PARTICLE:
1,334✔
66
        break;
1,334✔
67
      default:
×
68
        fatal_error("Invalid filter specified. Only cell, cell_instance, "
×
69
                    "distribcell, energy, material, mesh, and universe filters "
70
                    "are supported in random ray mode.");
71
      }
72
    }
73
  }
74

75
  // Validate MGXS data
76
  ///////////////////////////////////////////////////////////////////
77
  for (auto& material : data::mg.macro_xs_) {
2,257✔
78
    if (!material.is_isotropic) {
1,651!
79
      fatal_error("Anisotropic MGXS detected. Only isotropic XS data sets "
×
80
                  "supported in random ray mode.");
81
    }
82
    for (int g = 0; g < data::mg.num_energy_groups_; g++) {
8,451✔
83
      if (material.exists_in_model) {
6,800✔
84
        // Temperature and angle indices, if using multiple temperature
85
        // data sets and/or anisotropic data sets.
86
        // TODO: Currently assumes we are only using single temp/single angle
87
        // data.
88
        const int t = 0;
6,756✔
89
        const int a = 0;
6,756✔
90
        double sigma_t =
6,756✔
91
          material.get_xs(MgxsType::TOTAL, g, NULL, NULL, NULL, t, a);
6,756✔
92
        if (sigma_t <= 0.0) {
6,756!
93
          fatal_error("No zero or negative total macroscopic cross sections "
×
94
                      "allowed in random ray mode. If the intention is to make "
95
                      "a void material, use a cell fill of 'None' instead.");
96
        }
97
      }
98
    }
99
  }
100

101
  // Validate ray source
102
  ///////////////////////////////////////////////////////////////////
103

104
  // Check for independent source
105
  IndependentSource* is =
606!
106
    dynamic_cast<IndependentSource*>(RandomRay::ray_source_.get());
606!
107
  if (!is) {
606!
108
    fatal_error("Invalid ray source definition. Ray source must provided and "
×
109
                "be of type IndependentSource.");
110
  }
111

112
  // Check for box source
113
  SpatialDistribution* space_dist = is->space();
606!
114
  SpatialBox* sb = dynamic_cast<SpatialBox*>(space_dist);
606!
115
  if (!sb) {
606!
116
    fatal_error(
×
117
      "Invalid ray source definition -- only box sources are allowed.");
118
  }
119

120
  // Check that box source is not restricted to fissionable areas
121
  if (sb->only_fissionable()) {
606!
122
    fatal_error(
×
123
      "Invalid ray source definition -- fissionable spatial distribution "
124
      "not allowed.");
125
  }
126

127
  // Check for isotropic source
128
  UnitSphereDistribution* angle_dist = is->angle();
606!
129
  Isotropic* id = dynamic_cast<Isotropic*>(angle_dist);
606!
130
  if (!id) {
606!
131
    fatal_error("Invalid ray source definition -- only isotropic sources are "
×
132
                "allowed.");
133
  }
134

135
  // Validate external sources
136
  ///////////////////////////////////////////////////////////////////
137
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
606✔
138
    if (model::external_sources.size() < 1) {
342!
139
      fatal_error("Must provide a particle source (in addition to ray source) "
×
140
                  "in fixed source random ray mode.");
141
    }
142

143
    for (int i = 0; i < model::external_sources.size(); i++) {
684✔
144
      Source* s = model::external_sources[i].get();
342!
145

146
      // Check for independent source
147
      IndependentSource* is = dynamic_cast<IndependentSource*>(s);
342!
148

149
      if (!is) {
342!
150
        fatal_error(
×
151
          "Only IndependentSource external source types are allowed in "
152
          "random ray mode");
153
      }
154

155
      // Check for isotropic source
156
      UnitSphereDistribution* angle_dist = is->angle();
342!
157
      Isotropic* id = dynamic_cast<Isotropic*>(angle_dist);
342!
158
      if (!id) {
342!
159
        fatal_error(
×
160
          "Invalid source definition -- only isotropic external sources are "
161
          "allowed in random ray mode.");
162
      }
163

164
      // Validate that a domain ID was specified OR that it is a point source
165
      auto sp = dynamic_cast<SpatialPoint*>(is->space());
342!
166
      if (is->domain_ids().size() == 0 && !sp) {
342!
167
        fatal_error("Fixed sources must be point source or spatially "
×
168
                    "constrained by domain id (cell, material, or universe) in "
169
                    "random ray mode.");
170
      } else if (is->domain_ids().size() > 0 && sp) {
342✔
171
        // If both a domain constraint and a point source location are
172
        // specified, notify user that domain constraint takes precedence.
173
        warning("Fixed source has both a domain constraint and a point "
550✔
174
                "type spatial distribution. The domain constraint takes "
175
                "precedence in random ray mode -- point source coordinate "
176
                "will be ignored.");
177
      }
178

179
      // Check that a discrete energy distribution was used
180
      Distribution* d = is->energy();
342!
181
      Discrete* dd = dynamic_cast<Discrete*>(d);
342!
182
      if (!dd) {
342!
183
        fatal_error(
×
184
          "Only discrete (multigroup) energy distributions are allowed for "
185
          "external sources in random ray mode.");
186
      }
187
    }
188
  }
189

190
  // Validate adjoint sources
191
  ///////////////////////////////////////////////////////////////////
192
  if (FlatSourceDomain::adjoint_requested_ && !model::adjoint_sources.empty()) {
606✔
193
    for (int i = 0; i < model::adjoint_sources.size(); i++) {
22✔
194
      Source* s = model::adjoint_sources[i].get();
11!
195

196
      // Check for independent source
197
      IndependentSource* is = dynamic_cast<IndependentSource*>(s);
11!
198

199
      if (!is) {
11!
200
        fatal_error(
×
201
          "Only IndependentSource adjoint source types are allowed in "
202
          "random ray mode");
203
      }
204

205
      // Check for isotropic source
206
      UnitSphereDistribution* angle_dist = is->angle();
11!
207
      Isotropic* id = dynamic_cast<Isotropic*>(angle_dist);
11!
208
      if (!id) {
11!
209
        fatal_error(
×
210
          "Invalid source definition -- only isotropic adjoint sources are "
211
          "allowed in random ray mode.");
212
      }
213

214
      // Validate that a domain ID was specified OR that it is a point source
215
      auto sp = dynamic_cast<SpatialPoint*>(is->space());
11!
216
      if (is->domain_ids().size() == 0 && !sp) {
11!
217
        fatal_error("Adjoint sources must be point source or spatially "
×
218
                    "constrained by domain id (cell, material, or universe) in "
219
                    "random ray mode.");
220
      } else if (is->domain_ids().size() > 0 && sp) {
11!
221
        // If both a domain constraint and a point source location are
222
        // specified, notify user that domain constraint takes precedence.
223
        warning("Adjoint source has both a domain constraint and a point "
22✔
224
                "type spatial distribution. The domain constraint takes "
225
                "precedence in random ray mode -- point source coordinate "
226
                "will be ignored.");
227
      }
228

229
      // Check that a discrete energy distribution was used
230
      Distribution* d = is->energy();
11!
231
      Discrete* dd = dynamic_cast<Discrete*>(d);
11!
232
      if (!dd) {
11!
233
        fatal_error(
×
234
          "Only discrete (multigroup) energy distributions are allowed for "
235
          "adjoint sources in random ray mode.");
236
      }
237
    }
238
  }
239

240
  // Validate plotting files
241
  ///////////////////////////////////////////////////////////////////
242
  for (int p = 0; p < model::plots.size(); p++) {
606!
243

244
    // Get handle to OpenMC plot object
245
    const auto& openmc_plottable = model::plots[p];
×
246
    Plot* openmc_plot = dynamic_cast<Plot*>(openmc_plottable.get());
×
247

248
    // Random ray plots only support voxel plots
249
    if (!openmc_plot) {
×
250
      warning(fmt::format(
×
251
        "Plot {} will not be used for end of simulation data plotting -- only "
252
        "voxel plotting is allowed in random ray mode.",
253
        openmc_plottable->id()));
×
254
      continue;
×
255
    } else if (openmc_plot->type_ != Plot::PlotType::voxel) {
×
256
      warning(fmt::format(
×
257
        "Plot {} will not be used for end of simulation data plotting -- only "
258
        "voxel plotting is allowed in random ray mode.",
259
        openmc_plottable->id()));
×
260
      continue;
×
261
    }
262
  }
263

264
  // Warn about slow MPI domain replication, if detected
265
  ///////////////////////////////////////////////////////////////////
266
#ifdef OPENMC_MPI
267
  if (mpi::n_procs > 1) {
221✔
268
    warning(
416✔
269
      "MPI parallelism is not supported by the random ray solver. All work "
270
      "will be performed by rank 0. Domain decomposition may be implemented in "
271
      "the future to provide efficient MPI scaling.");
272
  }
273
#endif
274

275
  // Warn about instability resulting from linear sources in small regions
276
  // when generating weight windows with FW-CADIS and an overlaid mesh.
277
  ///////////////////////////////////////////////////////////////////
278
  if (RandomRay::source_shape_ == RandomRaySourceShape::LINEAR &&
606✔
279
      variance_reduction::weight_windows.size() > 0) {
242✔
280
    warning(
22✔
281
      "Linear sources may result in negative fluxes in small source regions "
282
      "generated by mesh subdivision. Negative sources may result in low "
283
      "quality FW-CADIS weight windows. We recommend you use flat source "
284
      "mode when generating weight windows with an overlaid mesh tally.");
285
  }
286
}
606✔
287

288
void openmc_finalize_random_ray()
9,128✔
289
{
290
  FlatSourceDomain::volume_estimator_ = RandomRayVolumeEstimator::HYBRID;
9,128✔
291
  FlatSourceDomain::volume_normalized_flux_tallies_ = false;
9,128✔
292
  FlatSourceDomain::adjoint_requested_ = false;
9,128✔
293
  FlatSourceDomain::solve_ = RandomRaySolve::FORWARD;
9,128✔
294
  FlatSourceDomain::fw_cadis_local_ = false;
9,128✔
295
  FlatSourceDomain::fw_cadis_local_targets_.clear();
9,128✔
296
  FlatSourceDomain::mesh_domain_map_.clear();
9,128✔
297
  RandomRay::ray_source_.reset();
9,128✔
298
  RandomRay::source_shape_ = RandomRaySourceShape::FLAT;
9,128✔
299
  RandomRay::sample_method_ = RandomRaySampleMethod::PRNG;
9,128✔
300
}
9,128✔
301

302
//==============================================================================
303
// RandomRaySimulation implementation
304
//==============================================================================
305

306
RandomRaySimulation::RandomRaySimulation()
814✔
307
  : negroups_(data::mg.num_energy_groups_)
814!
308
{
309
  // There are no source sites in random ray mode, so be sure to disable to
310
  // ensure we don't attempt to write source sites to statepoint
311
  settings::source_write = false;
814✔
312

313
  // Random ray mode does not have an inner loop over generations within a
314
  // batch, so set the current gen to 1
315
  simulation::current_gen = 1;
814✔
316

317
  switch (RandomRay::source_shape_) {
814!
318
  case RandomRaySourceShape::FLAT:
439✔
319
    domain_ = make_unique<FlatSourceDomain>();
439✔
320
    break;
439✔
321
  case RandomRaySourceShape::LINEAR:
375✔
322
  case RandomRaySourceShape::LINEAR_XY:
375✔
323
    domain_ = make_unique<LinearSourceDomain>();
375✔
324
    break;
375✔
325
  default:
×
UNCOV
326
    fatal_error("Unknown random ray source shape");
×
327
  }
328

329
  // Convert OpenMC native MGXS into a more efficient format
330
  // internal to the random ray solver
331
  domain_->flatten_xs();
814✔
332
}
814✔
333

334
void RandomRaySimulation::apply_fixed_sources_and_mesh_domains()
799✔
335
{
336
  domain_->apply_meshes();
799✔
337
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
799✔
338
    // Transfer external source user inputs onto random ray source regions
339
    domain_->convert_external_sources(false);
443✔
340
    domain_->count_external_source_regions();
443✔
341
  }
342
}
799✔
343

344
void RandomRaySimulation::prepare_fw_fixed_sources_adjoint()
113✔
345
{
346
  // Prepare adjoint fixed sources using forward flux
347
  domain_->source_regions_.adjoint_reset();
113✔
348
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
113✔
349
    domain_->set_fw_adjoint_sources();
98✔
350
  }
351
}
113✔
352

353
void RandomRaySimulation::prepare_local_fixed_sources_adjoint()
15✔
354
{
355
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
15!
356
    domain_->set_local_adjoint_sources();
15✔
357
  }
358
}
15✔
359

360
void RandomRaySimulation::prepare_adjoint_simulation(bool from_forward)
128✔
361
{
362
  reset_timers();
128✔
363

364
  if (mpi::master)
128✔
365
    header("ADJOINT FLUX SOLVE", 3);
100✔
366

367
  if (from_forward) {
128✔
368
    // The forward solve has already run. Re-initialize OpenMC's general data
369
    // structures for the adjoint solve and derive the adjoint source from the
370
    // forward flux.
371
    openmc_simulation_init();
113✔
372

373
    prepare_fw_fixed_sources_adjoint();
113✔
374
  } else {
375
    // Initialize adjoint fixed sources
376
    domain_->apply_meshes();
15✔
377
    prepare_local_fixed_sources_adjoint();
15✔
378
    domain_->count_external_source_regions();
15✔
379
  }
380

381
  domain_->k_eff_ = 1.0;
128✔
382

383
  // Transpose scattering matrix
384
  domain_->transpose_scattering_matrix();
128✔
385

386
  // Swap nu_sigma_f and chi
387
  domain_->nu_sigma_f_.swap(domain_->chi_);
128✔
388
}
128✔
389

390
void RandomRaySimulation::simulate()
927✔
391
{
392
  // Begin main simulation timer
393
  simulation::time_total.start();
927✔
394

395
  // Reset per-solve accumulators, as simulate() may run more than once on the
396
  // same object (e.g. forward then adjoint when generating weight windows)
397
  avg_miss_rate_ = 0.0;
927✔
398
  total_geometric_intersections_ = 0;
927✔
399

400
  // Random ray power iteration loop
401
  while (simulation::current_batch < settings::n_batches) {
22,769✔
402
    // Initialize the current batch
403
    initialize_batch();
21,842✔
404
    initialize_generation();
21,842✔
405

406
    // MPI not supported in random ray solver, so all work is done by rank 0
407
    // TODO: Implement domain decomposition for MPI parallelism
408
    if (mpi::master) {
21,842✔
409

410
      // Reset total starting particle weight used for normalizing tallies
411
      simulation::total_weight = 1.0;
16,842✔
412

413
      // Update source term (scattering + fission)
414
      domain_->update_all_neutron_sources();
16,842✔
415

416
      // Reset scalar fluxes, iteration volume tallies, and region hit flags
417
      // to zero
418
      domain_->batch_reset();
16,842✔
419

420
      // At the beginning of the simulation, if mesh subdivision is in use, we
421
      // need to swap the main source region container into the base container,
422
      // as the main source region container will be used to hold the true
423
      // subdivided source regions. The base container will therefore only
424
      // contain the external source region information, the mesh indices,
425
      // material properties, and initial guess values for the flux/source.
426

427
      // Start timer for transport
428
      simulation::time_transport.start();
16,842✔
429

430
// Transport sweep over all random rays for the iteration
431
#pragma omp parallel for schedule(dynamic)                                     \
9,192✔
432
  reduction(+ : total_geometric_intersections_)
9,192✔
433
      for (int i = 0; i < settings::n_particles; i++) {
1,460,650✔
434
        RandomRay ray(i, domain_.get());
1,453,000✔
435
        total_geometric_intersections_ +=
2,906,000✔
436
          ray.transport_history_based_single_ray();
1,453,000✔
437
      }
1,453,000✔
438

439
      simulation::time_transport.stop();
16,842✔
440

441
      // Add any newly discovered source regions to the main source region
442
      // container.
443
      domain_->finalize_discovered_source_regions();
16,842✔
444

445
      // Normalize scalar flux and update volumes
446
      domain_->normalize_scalar_flux_and_volumes(
16,842✔
447
        settings::n_particles * RandomRay::distance_active_);
448

449
      // Add source to scalar flux, compute number of FSR hits
450
      int64_t n_hits = domain_->add_source_to_scalar_flux();
16,842✔
451

452
      // Apply transport stabilization factors
453
      domain_->apply_transport_stabilization();
16,842✔
454

455
      if (settings::run_mode == RunMode::EIGENVALUE) {
16,842✔
456
        // Compute random ray k-eff
457
        domain_->compute_k_eff();
5,610✔
458

459
        // Store random ray k-eff into OpenMC's native k-eff variable
460
        global_tally_tracklength = domain_->k_eff_;
5,610✔
461
      }
462

463
      // Execute all tallying tasks, if this is an active batch
464
      if (simulation::current_batch > settings::n_inactive) {
16,842✔
465

466
        // Add this iteration's scalar flux estimate to final accumulated
467
        // estimate
468
        domain_->accumulate_iteration_flux();
7,706✔
469

470
        // Use above mapping to contribute FSR flux data to appropriate
471
        // tallies
472
        domain_->random_ray_tally();
7,706✔
473
      }
474

475
      // Set phi_old = phi_new
476
      domain_->flux_swap();
16,842✔
477

478
      // Check for any obvious insabilities/nans/infs
479
      instability_check(n_hits, domain_->k_eff_, avg_miss_rate_);
16,842✔
480
    } // End MPI master work
481

482
    // Finalize the current batch
483
    finalize_generation();
21,842✔
484
    finalize_batch();
21,842✔
485
  } // End random ray power iteration loop
486

487
  domain_->count_external_source_regions();
927✔
488

489
  // End main simulation timer
490
  simulation::time_total.stop();
927✔
491

492
  // Normalize and save the final forward flux
493
  double source_normalization_factor =
927✔
494
    domain_->compute_fixed_source_normalization_factor() /
927✔
495
    (settings::n_batches - settings::n_inactive);
927✔
496

497
#pragma omp parallel for
554✔
498
  for (uint64_t se = 0; se < domain_->n_source_elements(); se++) {
1,680,343✔
499
    domain_->source_regions_.scalar_flux_final(se) *=
1,679,970✔
500
      source_normalization_factor;
501
  }
502

503
  // Finalize OpenMC
504
  openmc_simulation_finalize();
927✔
505

506
  // Output all simulation results
507
  output_simulation_results();
927✔
508
}
927✔
509

510
void RandomRaySimulation::output_simulation_results() const
927✔
511
{
512
  // Print random ray results
513
  if (mpi::master) {
927✔
514
    print_results_random_ray(total_geometric_intersections_,
695✔
515
      avg_miss_rate_ / settings::n_batches, negroups_,
695✔
516
      domain_->n_source_regions(), domain_->n_external_source_regions_);
695✔
517
    if (model::plots.size() > 0) {
695!
UNCOV
518
      domain_->output_to_vtk();
×
519
    }
520
  }
521
}
927✔
522

523
// Apply a few sanity checks to catch obvious cases of numerical instability.
524
// Instability typically only occurs if ray density is extremely low.
525
void RandomRaySimulation::instability_check(
16,842✔
526
  int64_t n_hits, double k_eff, double& avg_miss_rate) const
527
{
528
  double percent_missed = ((domain_->n_source_regions() - n_hits) /
16,842!
529
                            static_cast<double>(domain_->n_source_regions())) *
16,842✔
530
                          100.0;
16,842✔
531
  avg_miss_rate += percent_missed;
16,842✔
532

533
  if (mpi::master) {
16,842!
534
    if (percent_missed > 10.0) {
16,842✔
535
      warning(fmt::format(
1,914✔
536
        "Very high FSR miss rate detected ({:.3f}%). Instability may occur. "
537
        "Increase ray density by adding more rays and/or active distance.",
538
        percent_missed));
539
    } else if (percent_missed > 1.0) {
15,885✔
540
      warning(
860✔
541
        fmt::format("Elevated FSR miss rate detected ({:.3f}%). Increasing "
1,720✔
542
                    "ray density by adding more rays and/or active "
543
                    "distance may improve simulation efficiency.",
544
          percent_missed));
545
    }
546

547
    if (k_eff > 10.0 || k_eff < 0.01 || !(std::isfinite(k_eff))) {
16,842!
UNCOV
548
      fatal_error(fmt::format("Instability detected: k-eff = {:.5f}", k_eff));
×
549
    }
550
  }
551
}
16,842✔
552

553
// Print random ray simulation results
554
void RandomRaySimulation::print_results_random_ray(
695✔
555
  uint64_t total_geometric_intersections, double avg_miss_rate, int negroups,
556
  int64_t n_source_regions, int64_t n_external_source_regions) const
557
{
558
  using namespace simulation;
695✔
559

560
  if (settings::verbosity >= 6) {
695!
561
    double total_integrations = total_geometric_intersections * negroups;
695✔
562
    double time_per_integration =
695✔
563
      simulation::time_transport.elapsed() / total_integrations;
695✔
564
    double misc_time = time_total.elapsed() - time_update_src.elapsed() -
695✔
565
                       time_transport.elapsed() - time_tallies.elapsed() -
695✔
566
                       time_bank_sendrecv.elapsed();
695✔
567

568
    header("Simulation Statistics", 4);
695✔
569
    fmt::print(
695✔
570
      " Total Iterations                  = {}\n", settings::n_batches);
571
    fmt::print(
695✔
572
      " Number of Rays per Iteration      = {}\n", settings::n_particles);
573
    fmt::print(" Inactive Distance                 = {} cm\n",
695✔
574
      RandomRay::distance_inactive_);
575
    fmt::print(" Active Distance                   = {} cm\n",
695✔
576
      RandomRay::distance_active_);
577
    fmt::print(" Source Regions (SRs)              = {}\n", n_source_regions);
695✔
578
    fmt::print(
695✔
579
      " SRs Containing External Sources   = {}\n", n_external_source_regions);
580
    fmt::print(" Total Geometric Intersections     = {:.4e}\n",
1,390✔
581
      static_cast<double>(total_geometric_intersections));
695✔
582
    fmt::print("   Avg per Iteration               = {:.4e}\n",
1,390✔
583
      static_cast<double>(total_geometric_intersections) / settings::n_batches);
695✔
584
    fmt::print("   Avg per Iteration per SR        = {:.2f}\n",
1,390✔
585
      static_cast<double>(total_geometric_intersections) /
695✔
586
        static_cast<double>(settings::n_batches) / n_source_regions);
695✔
587
    fmt::print(" Avg SR Miss Rate per Iteration    = {:.4f}%\n", avg_miss_rate);
695✔
588
    fmt::print(" Energy Groups                     = {}\n", negroups);
695✔
589
    fmt::print(
695✔
590
      " Total Integrations                = {:.4e}\n", total_integrations);
591
    fmt::print("   Avg per Iteration               = {:.4e}\n",
1,390✔
592
      total_integrations / settings::n_batches);
695✔
593

594
    std::string estimator;
695!
595
    switch (domain_->volume_estimator_) {
695!
596
    case RandomRayVolumeEstimator::SIMULATION_AVERAGED:
22✔
597
      estimator = "Simulation Averaged";
22✔
598
      break;
599
    case RandomRayVolumeEstimator::NAIVE:
101✔
600
      estimator = "Naive";
101✔
601
      break;
602
    case RandomRayVolumeEstimator::HYBRID:
572✔
603
      estimator = "Hybrid";
572✔
604
      break;
UNCOV
605
    default:
×
UNCOV
606
      fatal_error("Invalid volume estimator type");
×
607
    }
608
    fmt::print(" Volume Estimator Type             = {}\n", estimator);
695✔
609

610
    std::string adjoint_true =
695✔
611
      (FlatSourceDomain::solve_ == RandomRaySolve::ADJOINT) ? "ON" : "OFF";
1,985✔
612
    fmt::print(" Adjoint Flux Mode                 = {}\n", adjoint_true);
695✔
613

614
    std::string shape;
1,390!
615
    switch (RandomRay::source_shape_) {
695!
616
    case RandomRaySourceShape::FLAT:
409✔
617
      shape = "Flat";
409✔
618
      break;
619
    case RandomRaySourceShape::LINEAR:
253✔
620
      shape = "Linear";
253✔
621
      break;
622
    case RandomRaySourceShape::LINEAR_XY:
33✔
623
      shape = "Linear XY";
33✔
624
      break;
UNCOV
625
    default:
×
UNCOV
626
      fatal_error("Invalid random ray source shape");
×
627
    }
628
    fmt::print(" Source Shape                      = {}\n", shape);
695✔
629
    std::string sample_method;
1,390!
630
    switch (RandomRay::sample_method_) {
695!
631
    case RandomRaySampleMethod::PRNG:
629✔
632
      sample_method = "PRNG";
629✔
633
      break;
634
    case RandomRaySampleMethod::HALTON:
55✔
635
      sample_method = "Halton";
55✔
636
      break;
637
    case RandomRaySampleMethod::S2:
11✔
638
      sample_method = "PRNG S2";
11✔
639
      break;
640
    }
641
    fmt::print(" Sample Method                     = {}\n", sample_method);
695✔
642

643
    if (domain_->is_transport_stabilization_needed_) {
695✔
644
      fmt::print(" Transport XS Stabilization Used   = YES (rho = {:.3f})\n",
11✔
645
        FlatSourceDomain::diagonal_stabilization_rho_);
646
    } else {
647
      fmt::print(" Transport XS Stabilization Used   = NO\n");
684✔
648
    }
649

650
    header("Timing Statistics", 4);
695✔
651
    show_time("Total time for initialization", time_initialize.elapsed());
695✔
652
    show_time("Reading cross sections", time_read_xs.elapsed(), 1);
695✔
653
    show_time("Total simulation time", time_total.elapsed());
695✔
654
    show_time("Transport sweep only", time_transport.elapsed(), 1);
695✔
655
    show_time("Source update only", time_update_src.elapsed(), 1);
695✔
656
    show_time("Tally conversion only", time_tallies.elapsed(), 1);
695✔
657
    show_time("MPI source reductions only", time_bank_sendrecv.elapsed(), 1);
695✔
658
    show_time("Other iteration routines", misc_time, 1);
695✔
659
    if (settings::run_mode == RunMode::EIGENVALUE) {
695✔
660
      show_time("Time in inactive batches", time_inactive.elapsed());
275✔
661
    }
662
    show_time("Time in active batches", time_active.elapsed());
695✔
663
    show_time("Time writing statepoints", time_statepoint.elapsed());
695✔
664
    show_time("Total time for finalization", time_finalize.elapsed());
695✔
665
    show_time("Time per integration", time_per_integration);
695✔
666
  }
695✔
667

668
  if (settings::verbosity >= 4 && settings::run_mode == RunMode::EIGENVALUE) {
695!
669
    header("Results", 4);
275✔
670
    fmt::print(" k-effective                       = {:.5f} +/- {:.5f}\n",
275✔
671
      simulation::keff, simulation::keff_std);
672
  }
673
}
695✔
674

675
} // namespace openmc
676

677
//==============================================================================
678
// C API functions
679
//==============================================================================
680

681
void openmc_run_random_ray()
814✔
682
{
683
  using namespace openmc;
814✔
684

685
  // Determine which solves to run. If adjoint results are requested and no
686
  // user-defined adjoint source is present, an initial forward solve is needed
687
  // to construct the adjoint source from the forward flux (FW-CADIS). If the
688
  // user has defined an adjoint source, the forward solve is skipped and only
689
  // the adjoint solve is run.
690
  const bool run_adjoint = FlatSourceDomain::adjoint_requested_;
814✔
691
  const bool have_adjoint_source = !model::adjoint_sources.empty();
814✔
692
  const bool run_forward = !(run_adjoint && have_adjoint_source);
814✔
693

694
  // Set the initial solve type
695
  if (!run_forward) {
814✔
696
    FlatSourceDomain::solve_ = RandomRaySolve::ADJOINT;
15✔
697
  } else if (run_adjoint) {
799✔
698
    FlatSourceDomain::solve_ = RandomRaySolve::FORWARD_FOR_ADJOINT;
113✔
699
  } else {
700
    FlatSourceDomain::solve_ = RandomRaySolve::FORWARD;
686✔
701
  }
702

703
  // Initialize OpenMC general data structures
704
  openmc_simulation_init();
814✔
705

706
  // Validate that inputs meet requirements for random ray mode
707
  if (mpi::master)
814✔
708
    validate_random_ray_inputs();
606✔
709

710
  // Initialize Random Ray Simulation Object
711
  RandomRaySimulation sim;
814✔
712

713
  // Run the forward solve
714
  if (run_forward) {
814✔
715
    // When an adjoint solve follows, report this as the initial forward solve
716
    if (run_adjoint && mpi::master)
799✔
717
      header("FORWARD FLUX SOLVE", 3);
89✔
718
    sim.apply_fixed_sources_and_mesh_domains();
799✔
719
    sim.simulate();
799✔
720
  }
721

722
  // Run the adjoint solve
723
  if (run_adjoint) {
814✔
724
    FlatSourceDomain::solve_ = RandomRaySolve::ADJOINT;
128✔
725
    sim.prepare_adjoint_simulation(run_forward);
128✔
726
    sim.simulate();
128✔
727
  }
728
}
814✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc