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

openmc-dev / openmc / 23157030594

16 Mar 2026 05:27PM UTC coverage: 80.798%. First build
23157030594

Pull #3717

github

web-flow
Merge 0927541b0 into 157869812
Pull Request #3717: Local adjoint source for Random Ray

16780 of 24097 branches covered (69.64%)

Branch coverage included in aggregate %.

378 of 416 new or added lines in 9 files covered. (90.87%)

56876 of 67064 relevant lines covered (84.81%)

28093112.72 hits per line

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

80.28
/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()
265✔
31
{
32
  // Validate tallies
33
  ///////////////////////////////////////////////////////////////////
34
  for (auto& tally : model::tallies) {
755✔
35

36
    // Validate score types
37
    for (auto score_bin : tally->scores_) {
1,090✔
38
      switch (score_bin) {
600!
39
      case SCORE_FLUX:
600✔
40
      case SCORE_TOTAL:
600✔
41
      case SCORE_FISSION:
600✔
42
      case SCORE_NU_FISSION:
600✔
43
      case SCORE_EVENTS:
600✔
44
      case SCORE_KAPPA_FISSION:
600✔
45
        break;
600✔
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()) {
1,065✔
55
      auto& filter = *model::tally_filters[f];
575✔
56

57
      switch (filter.type()) {
575!
58
      case FilterType::CELL:
575✔
59
      case FilterType::CELL_INSTANCE:
575✔
60
      case FilterType::DISTRIBCELL:
575✔
61
      case FilterType::ENERGY:
575✔
62
      case FilterType::MATERIAL:
575✔
63
      case FilterType::MESH:
575✔
64
      case FilterType::UNIVERSE:
575✔
65
      case FilterType::PARTICLE:
575✔
66
        break;
575✔
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_) {
985✔
78
    if (!material.is_isotropic) {
720!
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++) {
3,690✔
83
      if (material.exists_in_model) {
2,970✔
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;
2,950✔
89
        const int a = 0;
2,950✔
90
        double sigma_t =
2,950✔
91
          material.get_xs(MgxsType::TOTAL, g, NULL, NULL, NULL, t, a);
2,950✔
92
        if (sigma_t <= 0.0) {
2,950!
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 =
265!
106
    dynamic_cast<IndependentSource*>(RandomRay::ray_source_.get());
265!
107
  if (!is) {
265!
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();
265!
114
  SpatialBox* sb = dynamic_cast<SpatialBox*>(space_dist);
265!
115
  if (!sb) {
265!
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()) {
265!
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();
265!
129
  Isotropic* id = dynamic_cast<Isotropic*>(angle_dist);
265!
130
  if (!id) {
265!
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) {
265✔
138
    if (model::external_sources.size() < 1) {
145!
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++) {
290✔
144
      Source* s = model::external_sources[i].get();
145!
145

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

149
      if (!is) {
145!
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();
145!
157
      Isotropic* id = dynamic_cast<Isotropic*>(angle_dist);
145!
158
      if (!id) {
145!
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());
145!
166
      if (is->domain_ids().size() == 0 && !sp) {
145!
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) {
145✔
171
        // If both a domain constraint and a non-default point source location
172
        // are specified, notify user that domain constraint takes precedence.
173
        if (sp->r().x == 0.0 && sp->r().y == 0.0 && sp->r().z == 0.0) {
125!
174
          warning("Fixed source has both a domain constraint and a point "
225✔
175
                  "type spatial distribution. The domain constraint takes "
176
                  "precedence in random ray mode -- point source coordinate "
177
                  "will be ignored.");
178
        }
179
      }
180

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

192
  // Validate adjoint sources
193
  ///////////////////////////////////////////////////////////////////
194
  if (FlatSourceDomain::adjoint_ && !model::adjoint_sources.empty()) {
265!
195
    for (int i = 0; i < model::adjoint_sources.size(); i++) {
10✔
196
      Source* s = model::adjoint_sources[i].get();
5!
197

198
      // Check for independent source
199
      IndependentSource* is = dynamic_cast<IndependentSource*>(s);
5!
200

201
      if (!is) {
5!
NEW
202
        fatal_error(
×
203
          "Only IndependentSource adjoint source types are allowed in "
204
          "random ray mode");
205
      }
206

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

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

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

244
  // Validate plotting files
245
  ///////////////////////////////////////////////////////////////////
246
  for (int p = 0; p < model::plots.size(); p++) {
265!
247

248
    // Get handle to OpenMC plot object
249
    const auto& openmc_plottable = model::plots[p];
×
250
    Plot* openmc_plot = dynamic_cast<Plot*>(openmc_plottable.get());
×
251

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

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

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

292
void openmc_finalize_random_ray()
3,565✔
293
{
294
  FlatSourceDomain::volume_estimator_ = RandomRayVolumeEstimator::HYBRID;
3,565✔
295
  FlatSourceDomain::volume_normalized_flux_tallies_ = false;
3,565✔
296
  FlatSourceDomain::adjoint_ = false;
3,565✔
297
  FlatSourceDomain::mesh_domain_map_.clear();
3,565✔
298
  RandomRay::ray_source_.reset();
3,565✔
299
  RandomRay::source_shape_ = RandomRaySourceShape::FLAT;
3,565✔
300
  RandomRay::sample_method_ = RandomRaySampleMethod::PRNG;
3,565✔
301
}
3,565✔
302

303
//==============================================================================
304
// RandomRaySimulation implementation
305
//==============================================================================
306

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

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

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

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

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

345
void RandomRaySimulation::prepare_fw_fixed_sources_adjoint()
36✔
346
{
347
  // Prepare adjoint fixed sources using forward flux
348
  domain_->source_regions_.adjoint_reset();
36✔
349
  if (settings::run_mode == RunMode::FIXED_SOURCE ||
36!
350
      FlatSourceDomain::fw_cadis_local_) {
351
    domain_->set_fw_adjoint_sources();
30✔
352
  }
353
}
36✔
354

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

362
void RandomRaySimulation::simulate()
353✔
363
{
364
  // Random ray power iteration loop
365
  while (simulation::current_batch < settings::n_batches) {
8,853✔
366
    // Initialize the current batch
367
    initialize_batch();
8,500✔
368
    initialize_generation();
8,500✔
369

370
    // MPI not supported in random ray solver, so all work is done by rank 0
371
    // TODO: Implement domain decomposition for MPI parallelism
372
    if (mpi::master) {
8,500✔
373

374
      // Reset total starting particle weight used for normalizing tallies
375
      simulation::total_weight = 1.0;
7,250✔
376

377
      // Update source term (scattering + fission)
378
      domain_->update_all_neutron_sources();
7,250✔
379

380
      // Reset scalar fluxes, iteration volume tallies, and region hit flags
381
      // to zero
382
      domain_->batch_reset();
7,250✔
383

384
      // At the beginning of the simulation, if mesh subdivision is in use, we
385
      // need to swap the main source region container into the base container,
386
      // as the main source region container will be used to hold the true
387
      // subdivided source regions. The base container will therefore only
388
      // contain the external source region information, the mesh indices,
389
      // material properties, and initial guess values for the flux/source.
390

391
      // Start timer for transport
392
      simulation::time_transport.start();
7,250✔
393

394
// Transport sweep over all random rays for the iteration
395
#pragma omp parallel for schedule(dynamic)                                     \
396
  reduction(+ : total_geometric_intersections_)
397
      for (int i = 0; i < settings::n_particles; i++) {
1,060,250✔
398
        RandomRay ray(i, domain_.get());
1,053,000✔
399
        total_geometric_intersections_ +=
2,106,000✔
400
          ray.transport_history_based_single_ray();
1,053,000✔
401
      }
1,053,000✔
402

403
      simulation::time_transport.stop();
7,250✔
404

405
      // Add any newly discovered source regions to the main source region
406
      // container.
407
      domain_->finalize_discovered_source_regions();
7,250✔
408

409
      // Normalize scalar flux and update volumes
410
      domain_->normalize_scalar_flux_and_volumes(
7,250✔
411
        settings::n_particles * RandomRay::distance_active_);
412

413
      // Add source to scalar flux, compute number of FSR hits
414
      int64_t n_hits = domain_->add_source_to_scalar_flux();
7,250✔
415

416
      // Apply transport stabilization factors
417
      domain_->apply_transport_stabilization();
7,250✔
418

419
      if (settings::run_mode == RunMode::EIGENVALUE) {
7,250✔
420
        // Compute random ray k-eff
421
        domain_->compute_k_eff();
2,550✔
422

423
        // Store random ray k-eff into OpenMC's native k-eff variable
424
        global_tally_tracklength = domain_->k_eff_;
2,550✔
425
      }
426

427
      // Execute all tallying tasks, if this is an active batch
428
      if (simulation::current_batch > settings::n_inactive) {
7,250✔
429

430
        // Add this iteration's scalar flux estimate to final accumulated
431
        // estimate
432
        domain_->accumulate_iteration_flux();
3,400✔
433

434
        // Use above mapping to contribute FSR flux data to appropriate
435
        // tallies
436
        domain_->random_ray_tally();
3,400✔
437
      }
438

439
      // Set phi_old = phi_new
440
      domain_->flux_swap();
7,250✔
441

442
      // Check for any obvious insabilities/nans/infs
443
      instability_check(n_hits, domain_->k_eff_, avg_miss_rate_);
7,250✔
444
    } // End MPI master work
445

446
    // Finalize the current batch
447
    finalize_generation();
8,500✔
448
    finalize_batch();
8,500✔
449
  } // End random ray power iteration loop
450

451
  domain_->count_external_source_regions();
353✔
452
}
353✔
453

454
void RandomRaySimulation::output_simulation_results() const
353✔
455
{
456
  // Print random ray results
457
  if (mpi::master) {
353✔
458
    print_results_random_ray(total_geometric_intersections_,
295✔
459
      avg_miss_rate_ / settings::n_batches, negroups_,
295✔
460
      domain_->n_source_regions(), domain_->n_external_source_regions_);
295✔
461
    if (model::plots.size() > 0) {
295!
462
      domain_->output_to_vtk();
×
463
    }
464
  }
465
}
353✔
466

467
// Apply a few sanity checks to catch obvious cases of numerical instability.
468
// Instability typically only occurs if ray density is extremely low.
469
void RandomRaySimulation::instability_check(
7,250✔
470
  int64_t n_hits, double k_eff, double& avg_miss_rate) const
471
{
472
  double percent_missed = ((domain_->n_source_regions() - n_hits) /
7,250!
473
                            static_cast<double>(domain_->n_source_regions())) *
7,250✔
474
                          100.0;
7,250✔
475
  avg_miss_rate += percent_missed;
7,250✔
476

477
  if (mpi::master) {
7,250!
478
    if (percent_missed > 10.0) {
7,250✔
479
      warning(fmt::format(
870✔
480
        "Very high FSR miss rate detected ({:.3f}%). Instability may occur. "
481
        "Increase ray density by adding more rays and/or active distance.",
482
        percent_missed));
483
    } else if (percent_missed > 1.0) {
6,815!
484
      warning(
×
485
        fmt::format("Elevated FSR miss rate detected ({:.3f}%). Increasing "
×
486
                    "ray density by adding more rays and/or active "
487
                    "distance may improve simulation efficiency.",
488
          percent_missed));
489
    }
490

491
    if (k_eff > 10.0 || k_eff < 0.01 || !(std::isfinite(k_eff))) {
7,250!
492
      fatal_error(fmt::format("Instability detected: k-eff = {:.5f}", k_eff));
×
493
    }
494
  }
495
}
7,250✔
496

497
// Print random ray simulation results
498
void RandomRaySimulation::print_results_random_ray(
295✔
499
  uint64_t total_geometric_intersections, double avg_miss_rate, int negroups,
500
  int64_t n_source_regions, int64_t n_external_source_regions) const
501
{
502
  using namespace simulation;
295✔
503

504
  if (settings::verbosity >= 6) {
295!
505
    double total_integrations = total_geometric_intersections * negroups;
295✔
506
    double time_per_integration =
295✔
507
      simulation::time_transport.elapsed() / total_integrations;
295✔
508
    double misc_time = time_total.elapsed() - time_update_src.elapsed() -
295✔
509
                       time_transport.elapsed() - time_tallies.elapsed() -
295✔
510
                       time_bank_sendrecv.elapsed();
295✔
511

512
    header("Simulation Statistics", 4);
295✔
513
    fmt::print(
295✔
514
      " Total Iterations                  = {}\n", settings::n_batches);
515
    fmt::print(
295✔
516
      " Number of Rays per Iteration      = {}\n", settings::n_particles);
517
    fmt::print(" Inactive Distance                 = {} cm\n",
295✔
518
      RandomRay::distance_inactive_);
519
    fmt::print(" Active Distance                   = {} cm\n",
295✔
520
      RandomRay::distance_active_);
521
    fmt::print(" Source Regions (SRs)              = {}\n", n_source_regions);
295✔
522
    fmt::print(
295✔
523
      " SRs Containing External Sources   = {}\n", n_external_source_regions);
524
    fmt::print(" Total Geometric Intersections     = {:.4e}\n",
590✔
525
      static_cast<double>(total_geometric_intersections));
295✔
526
    fmt::print("   Avg per Iteration               = {:.4e}\n",
590✔
527
      static_cast<double>(total_geometric_intersections) / settings::n_batches);
295✔
528
    fmt::print("   Avg per Iteration per SR        = {:.2f}\n",
590✔
529
      static_cast<double>(total_geometric_intersections) /
295✔
530
        static_cast<double>(settings::n_batches) / n_source_regions);
295✔
531
    fmt::print(" Avg SR Miss Rate per Iteration    = {:.4f}%\n", avg_miss_rate);
295✔
532
    fmt::print(" Energy Groups                     = {}\n", negroups);
295✔
533
    fmt::print(
295✔
534
      " Total Integrations                = {:.4e}\n", total_integrations);
535
    fmt::print("   Avg per Iteration               = {:.4e}\n",
590✔
536
      total_integrations / settings::n_batches);
295✔
537

538
    std::string estimator;
295!
539
    switch (domain_->volume_estimator_) {
295!
540
    case RandomRayVolumeEstimator::SIMULATION_AVERAGED:
10✔
541
      estimator = "Simulation Averaged";
10✔
542
      break;
543
    case RandomRayVolumeEstimator::NAIVE:
45✔
544
      estimator = "Naive";
45✔
545
      break;
546
    case RandomRayVolumeEstimator::HYBRID:
240✔
547
      estimator = "Hybrid";
240✔
548
      break;
549
    default:
×
550
      fatal_error("Invalid volume estimator type");
×
551
    }
552
    fmt::print(" Volume Estimator Type             = {}\n", estimator);
295✔
553

554
    std::string adjoint_true = (FlatSourceDomain::adjoint_) ? "ON" : "OFF";
850✔
555
    fmt::print(" Adjoint Flux Mode                 = {}\n", adjoint_true);
295✔
556

557
    std::string shape;
590!
558
    switch (RandomRay::source_shape_) {
295!
559
    case RandomRaySourceShape::FLAT:
165✔
560
      shape = "Flat";
165✔
561
      break;
562
    case RandomRaySourceShape::LINEAR:
115✔
563
      shape = "Linear";
115✔
564
      break;
565
    case RandomRaySourceShape::LINEAR_XY:
15✔
566
      shape = "Linear XY";
15✔
567
      break;
568
    default:
×
569
      fatal_error("Invalid random ray source shape");
×
570
    }
571
    fmt::print(" Source Shape                      = {}\n", shape);
295✔
572
    std::string sample_method;
590!
573
    switch (RandomRay::sample_method_) {
295!
574
    case RandomRaySampleMethod::PRNG:
285✔
575
      sample_method = "PRNG";
285✔
576
      break;
577
    case RandomRaySampleMethod::HALTON:
5✔
578
      sample_method = "Halton";
5✔
579
      break;
580
    case RandomRaySampleMethod::S2:
5✔
581
      sample_method = "PRNG S2";
5✔
582
      break;
583
    }
584
    fmt::print(" Sample Method                     = {}\n", sample_method);
295✔
585

586
    if (domain_->is_transport_stabilization_needed_) {
295✔
587
      fmt::print(" Transport XS Stabilization Used   = YES (rho = {:.3f})\n",
5✔
588
        FlatSourceDomain::diagonal_stabilization_rho_);
589
    } else {
590
      fmt::print(" Transport XS Stabilization Used   = NO\n");
290✔
591
    }
592

593
    header("Timing Statistics", 4);
295✔
594
    show_time("Total time for initialization", time_initialize.elapsed());
295✔
595
    show_time("Reading cross sections", time_read_xs.elapsed(), 1);
295✔
596
    show_time("Total simulation time", time_total.elapsed());
295✔
597
    show_time("Transport sweep only", time_transport.elapsed(), 1);
295✔
598
    show_time("Source update only", time_update_src.elapsed(), 1);
295✔
599
    show_time("Tally conversion only", time_tallies.elapsed(), 1);
295✔
600
    show_time("MPI source reductions only", time_bank_sendrecv.elapsed(), 1);
295✔
601
    show_time("Other iteration routines", misc_time, 1);
295✔
602
    if (settings::run_mode == RunMode::EIGENVALUE) {
295✔
603
      show_time("Time in inactive batches", time_inactive.elapsed());
125✔
604
    }
605
    show_time("Time in active batches", time_active.elapsed());
295✔
606
    show_time("Time writing statepoints", time_statepoint.elapsed());
295✔
607
    show_time("Total time for finalization", time_finalize.elapsed());
295✔
608
    show_time("Time per integration", time_per_integration);
295✔
609
  }
295✔
610

611
  if (settings::verbosity >= 4 && settings::run_mode == RunMode::EIGENVALUE) {
295!
612
    header("Results", 4);
125✔
613
    fmt::print(" k-effective                       = {:.5f} +/- {:.5f}\n",
125✔
614
      simulation::keff, simulation::keff_std);
615
  }
616
}
295✔
617

618
} // namespace openmc
619

620
//==============================================================================
621
// C API functions
622
//==============================================================================
623

624
void openmc_run_random_ray()
317✔
625
{
626
  //////////////////////////////////////////////////////////
627
  // Run forward simulation
628
  //////////////////////////////////////////////////////////
629

630
  // Check if adjoint calculation is needed, and if local adjoint source(s)
631
  // are present. If an adjoint calculation is needed and no sources are
632
  // specified, we will run a forward calculation first to calculate adjoint
633
  // sources for global variance reduction, then perform an adjoint
634
  // calculation later.
635
  bool adjoint_needed = openmc::FlatSourceDomain::adjoint_;
317✔
636
  bool fw_adjoint = openmc::model::adjoint_sources.empty() && adjoint_needed;
317✔
637

638
  // If we're going to do an adjoint simulation with forward-weighted adjoint
639
  // sources afterwards, report that this is the initial forward flux solve.
640
  if (!adjoint_needed || fw_adjoint) {
317✔
641
    // Configure the domain for forward simulation
642
    openmc::FlatSourceDomain::adjoint_ = false;
311✔
643

644
    if (adjoint_needed && openmc::mpi::master)
311✔
645
      openmc::header("FORWARD FLUX SOLVE", 3);
30✔
646
  } else {
647
    // Configure domain for adjoint simulation (later)
648
    openmc::FlatSourceDomain::adjoint_ = true;
6✔
649
  }
650

651
  // Initialize OpenMC general data structures
652
  openmc_simulation_init();
317✔
653

654
  // Validate that inputs meet requirements for random ray mode
655
  if (openmc::mpi::master)
317✔
656
    openmc::validate_random_ray_inputs();
265✔
657

658
  // Initialize Random Ray Simulation Object
659
  openmc::RandomRaySimulation sim;
317✔
660

661
  if (!adjoint_needed || fw_adjoint) {
317✔
662
    // Initialize fixed sources, if present
663
    sim.apply_fixed_sources_and_mesh_domains();
311✔
664

665
    // Begin main simulation timer
666
    openmc::simulation::time_total.start();
311✔
667

668
    // Execute random ray simulation
669
    sim.simulate();
311✔
670

671
    // End main simulation timer
672
    openmc::simulation::time_total.stop();
311✔
673

674
    // Normalize and save the final forward flux
675
    double source_normalization_factor =
311✔
676
      sim.domain()->compute_fixed_source_normalization_factor() /
311✔
677
      (openmc::settings::n_batches - openmc::settings::n_inactive);
311✔
678

679
#pragma omp parallel for
680
    for (uint64_t se = 0; se < sim.domain()->n_source_elements(); se++) {
1,198,276✔
681
      sim.domain()->source_regions_.scalar_flux_final(se) *=
1,197,965✔
682
        source_normalization_factor;
683
    }
684

685
    // Finalize OpenMC
686
    openmc_simulation_finalize();
311✔
687

688
    // Output all simulation results
689
    sim.output_simulation_results();
311✔
690
  }
691

692
  //////////////////////////////////////////////////////////
693
  // Run adjoint simulation (if enabled)
694
  //////////////////////////////////////////////////////////
695

696
  if (!adjoint_needed) {
317✔
697
    return;
275✔
698
  }
699

700
  openmc::reset_timers();
42✔
701

702
  if (openmc::mpi::master)
42✔
703
    openmc::header("ADJOINT FLUX SOLVE", 3);
35✔
704

705
  if (fw_adjoint) {
42✔
706
    // Forward simulation has already been run;
707
    // Configure the domain for adjoint simulation and
708
    // re-initialize OpenMC general data structures
709
    openmc::FlatSourceDomain::adjoint_ = true;
36✔
710

711
    openmc_simulation_init();
36✔
712

713
    sim.prepare_fw_fixed_sources_adjoint();
36✔
714
  } else {
715
    // Initialize adjoint fixed sources
716
    sim.prepare_local_fixed_sources_adjoint();
6✔
717
  }
718

719
  sim.domain()->k_eff_ = 1.0;
42✔
720

721
  // Transpose scattering matrix
722
  sim.domain()->transpose_scattering_matrix();
42✔
723

724
  // Swap nu_sigma_f and chi
725
  sim.domain()->nu_sigma_f_.swap(sim.domain()->chi_);
42✔
726

727
  // Begin main simulation timer
728
  openmc::simulation::time_total.start();
42✔
729

730
  // Execute random ray simulation
731
  sim.simulate();
42✔
732

733
  // End main simulation timer
734
  openmc::simulation::time_total.stop();
42✔
735

736
  // Finalize OpenMC
737
  openmc_simulation_finalize();
42✔
738

739
  // Output all simulation results
740
  sim.output_simulation_results();
42✔
741
}
317✔
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