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

openmc-dev / openmc / 29109839871

10 Jul 2026 05:07PM UTC coverage: 81.355% (+0.06%) from 81.295%
29109839871

Pull #3971

github

web-flow
Merge b03e8c77b into 7256d5046
Pull Request #3971: Delta tracking

18567 of 26880 branches covered (69.07%)

Branch coverage included in aggregate %.

604 of 650 new or added lines in 20 files covered. (92.92%)

59962 of 69646 relevant lines covered (86.1%)

49564145.57 hits per line

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

94.7
/src/simulation.cpp
1
#include "openmc/simulation.h"
2

3
#include "openmc/bank.h"
4
#include "openmc/capi.h"
5
#include "openmc/collision_track.h"
6
#include "openmc/container_util.h"
7
#include "openmc/eigenvalue.h"
8
#include "openmc/error.h"
9
#include "openmc/event.h"
10
#include "openmc/geometry_aux.h"
11
#include "openmc/ifp.h"
12
#include "openmc/majorant.h"
13
#include "openmc/material.h"
14
#include "openmc/message_passing.h"
15
#include "openmc/nuclide.h"
16
#include "openmc/output.h"
17
#include "openmc/particle.h"
18
#include "openmc/photon.h"
19
#include "openmc/random_lcg.h"
20
#include "openmc/random_ray/flat_source_domain.h"
21
#include "openmc/settings.h"
22
#include "openmc/source.h"
23
#include "openmc/state_point.h"
24
#include "openmc/tallies/derivative.h"
25
#include "openmc/tallies/filter.h"
26
#include "openmc/tallies/tally.h"
27
#include "openmc/tallies/trigger.h"
28
#include "openmc/timer.h"
29
#include "openmc/track_output.h"
30
#include "openmc/weight_windows.h"
31

32
#ifdef _OPENMP
33
#include <omp.h>
34
#endif
35
#include "openmc/tensor.h"
36

37
#ifdef OPENMC_MPI
38
#include <mpi.h>
39
#endif
40

41
#include <fmt/format.h>
42

43
#include <algorithm>
44
#include <cmath>
45
#include <string>
46

47
//==============================================================================
48
// C API functions
49
//==============================================================================
50

51
// OPENMC_RUN encompasses all the main logic where iterations are performed
52
// over the batches, generations, and histories in a fixed source or
53
// k-eigenvalue calculation.
54

55
int openmc_run()
6,840✔
56
{
57
  openmc::simulation::time_total.start();
6,840✔
58
  openmc_simulation_init();
6,840✔
59

60
  // Ensure that a batch isn't executed in the case that the maximum number of
61
  // batches has already been run in a restart statepoint file
62
  int status = 0;
6,840✔
63
  if (openmc::simulation::current_batch >= openmc::settings::n_max_batches) {
6,840✔
64
    status = openmc::STATUS_EXIT_MAX_BATCH;
11✔
65
  }
66

67
  int err = 0;
68
  while (status == 0 && err == 0) {
149,580✔
69
    err = openmc_next_batch(&status);
142,753✔
70
  }
71

72
  openmc_simulation_finalize();
6,827✔
73
  openmc::simulation::time_total.stop();
6,827✔
74
  return err;
6,827✔
75
}
76

77
int openmc_simulation_init()
8,012✔
78
{
79
  using namespace openmc;
8,012✔
80

81
  // Skip if simulation has already been initialized
82
  if (simulation::initialized)
8,012✔
83
    return 0;
84

85
  // Initialize nuclear data (energy limits, log grid)
86
  if (settings::run_CE) {
7,990✔
87
    initialize_data();
6,579✔
88
  }
89

90
  // Create the majorant cross sections for delta tracking.
91
  if (settings::delta_tracking) {
7,990✔
92
    create_majorants();
150✔
93
  }
94

95
  // Determine how much work each process should do
96
  calculate_work(settings::n_particles);
7,990✔
97

98
  // Allocate source, fission and surface source banks.
99
  allocate_banks();
7,990✔
100

101
  // Create track file if needed
102
  if (!settings::track_identifiers.empty() || settings::write_all_tracks) {
7,990✔
103
    open_track_file();
90✔
104
  }
105

106
  // If doing an event-based simulation, intialize the particle buffer
107
  // and event queues
108
  if (settings::event_based) {
7,990✔
109
    int64_t event_buffer_length =
260!
110
      std::min(simulation::work_per_rank, settings::max_particles_in_flight);
260✔
111
    init_event_queues(event_buffer_length);
260✔
112
  }
113

114
  // Allocate tally results arrays if they're not allocated yet
115
  for (auto& t : model::tallies) {
35,766✔
116
    t->set_strides();
27,776✔
117
    t->init_results();
27,776✔
118
  }
119

120
  // Set up material nuclide index mapping
121
  for (auto& mat : model::materials) {
28,055✔
122
    mat->init_nuclide_index();
20,065✔
123
  }
124

125
  // Reset global variables -- this is done before loading state point (as that
126
  // will potentially populate k_generation and entropy)
127
  simulation::current_batch = 0;
7,990✔
128
  simulation::ct_current_file = 1;
7,990✔
129
  simulation::ssw_current_file = 1;
7,990✔
130
  simulation::k_generation.clear();
7,990✔
131
  simulation::entropy.clear();
7,990✔
132
  reset_source_rejection_counters();
7,990✔
133
  openmc_reset();
7,990✔
134

135
  // If this is a restart run, load the state point data and binary source
136
  // file
137
  if (settings::restart_run) {
7,990✔
138
    load_state_point();
63✔
139
    write_message("Resuming simulation...", 6);
126✔
140
  } else {
141
    // Only initialize primary source bank for eigenvalue simulations
142
    if (settings::run_mode == RunMode::EIGENVALUE &&
7,927✔
143
        settings::solver_type == SolverType::MONTE_CARLO) {
4,568✔
144
      initialize_source();
4,197✔
145
    }
146
  }
147

148
  // Display header
149
  if (mpi::master) {
7,990✔
150
    if (settings::run_mode == RunMode::FIXED_SOURCE) {
6,938✔
151
      if (settings::solver_type == SolverType::MONTE_CARLO) {
3,041✔
152
        header("FIXED SOURCE TRANSPORT SIMULATION", 3);
2,665✔
153
      } else if (settings::solver_type == SolverType::RANDOM_RAY) {
376!
154
        header("FIXED SOURCE TRANSPORT SIMULATION (RANDOM RAY SOLVER)", 3);
376✔
155
      }
156
    } else if (settings::run_mode == RunMode::EIGENVALUE) {
3,897!
157
      if (settings::solver_type == SolverType::MONTE_CARLO) {
3,897✔
158
        header("K EIGENVALUE SIMULATION", 3);
3,622✔
159
      } else if (settings::solver_type == SolverType::RANDOM_RAY) {
275!
160
        header("K EIGENVALUE SIMULATION (RANDOM RAY SOLVER)", 3);
275✔
161
      }
162
      if (settings::verbosity >= 7)
3,897✔
163
        print_columns();
3,517✔
164
    }
165
  }
166

167
  // load weight windows from file
168
  if (!settings::weight_windows_file.empty()) {
7,990!
169
    openmc_weight_windows_import(settings::weight_windows_file.c_str());
×
170
  }
171

172
  // Set flag indicating initialization is done
173
  simulation::initialized = true;
7,990✔
174
  return 0;
7,990✔
175
}
176

177
int openmc_simulation_finalize()
7,977✔
178
{
179
  using namespace openmc;
7,977✔
180

181
  // Skip if simulation was never run
182
  if (!simulation::initialized)
7,977!
183
    return 0;
184

185
  // Stop active batch timer and start finalization timer
186
  simulation::time_active.stop();
7,977✔
187
  simulation::time_finalize.start();
7,977✔
188

189
  // Clear material nuclide mapping
190
  for (auto& mat : model::materials) {
28,029✔
191
    mat->mat_nuclide_index_.clear();
40,104!
192
  }
193

194
  // Close track file if open
195
  if (!settings::track_identifiers.empty() || settings::write_all_tracks) {
7,977✔
196
    close_track_file();
90✔
197
  }
198

199
  // Increment total number of generations
200
  simulation::total_gen += simulation::current_batch * settings::gen_per_batch;
7,977✔
201

202
#ifdef OPENMC_MPI
203
  broadcast_results();
3,590✔
204
#endif
205

206
  // Write tally results to tallies.out
207
  if (settings::output_tallies && mpi::master)
7,977!
208
    write_tallies();
6,579✔
209

210
  // If weight window generators are present in this simulation, write a
211
  // weight windows file. This is skipped during the forward solve of an
212
  // adjoint (FW-CADIS) run, where only the adjoint-derived weight windows
213
  // are meaningful.
214
  if (variance_reduction::weight_windows_generators.size() > 0 &&
7,977✔
215
      FlatSourceDomain::solve_ != RandomRaySolve::FORWARD_FOR_ADJOINT) {
166✔
216
    openmc_weight_windows_export();
105✔
217
  }
218

219
  // Deactivate all tallies
220
  for (auto& t : model::tallies) {
35,753✔
221
    t->active_ = false;
27,776✔
222
  }
223

224
  // Stop timers and show timing statistics
225
  simulation::time_finalize.stop();
7,977✔
226
  simulation::time_total.stop();
7,977✔
227

228
#ifdef OPENMC_MPI
229
  // Reduce track count across ranks for correct reporting. In shared secondary
230
  // bank mode, all ranks already have the global count; in non-shared mode,
231
  // each rank only has its own count.
232
  if (settings::weight_windows_on && !settings::use_shared_secondary_bank) {
3,590✔
233
    int64_t total_tracks;
92✔
234
    MPI_Reduce(&simulation::simulation_tracks_completed, &total_tracks, 1,
92✔
235
      MPI_INT64_T, MPI_SUM, 0, mpi::intracomm);
236
    if (mpi::master)
92✔
237
      simulation::simulation_tracks_completed = total_tracks;
76✔
238
  }
239
#endif
240

241
  if (mpi::master) {
7,977✔
242
    if (settings::solver_type != SolverType::RANDOM_RAY) {
6,925✔
243
      if (settings::verbosity >= 6)
6,274✔
244
        print_runtime();
5,894✔
245
      if (settings::verbosity >= 4)
6,274✔
246
        print_results();
5,894✔
247
    }
248
  }
249
  if (settings::check_overlaps)
7,977!
250
    print_overlap_check();
×
251

252
  // Clear majorants as they could change if OpenMC is run again.
253
  reset_majorants();
7,977✔
254

255
  // Reset flags
256
  simulation::initialized = false;
7,977✔
257
  return 0;
7,977✔
258
}
259

260
int openmc_next_batch(int* status)
146,878✔
261
{
262
  using namespace openmc;
146,878✔
263
  using openmc::simulation::current_gen;
146,878✔
264

265
  // Make sure simulation has been initialized
266
  if (!simulation::initialized) {
146,878✔
267
    set_errmsg("Simulation has not been initialized yet.");
11✔
268
    return OPENMC_E_ALLOCATE;
11✔
269
  }
270

271
  initialize_batch();
146,867✔
272

273
  // =======================================================================
274
  // LOOP OVER GENERATIONS
275
  for (current_gen = 1; current_gen <= settings::gen_per_batch; ++current_gen) {
293,931✔
276

277
    initialize_generation();
147,077✔
278

279
    // Start timer for transport
280
    simulation::time_transport.start();
147,077✔
281

282
    // Transport loop
283
    if (settings::event_based) {
147,077✔
284
      if (settings::use_shared_secondary_bank) {
3,611✔
285
        transport_event_based_shared_secondary();
11✔
286
      } else {
287
        transport_event_based();
3,600✔
288
      }
289
    } else {
290
      if (settings::use_shared_secondary_bank) {
143,466✔
291
        transport_history_based_shared_secondary();
667✔
292
      } else {
293
        transport_history_based();
142,799✔
294
      }
295
    }
296

297
    // Accumulate time for transport
298
    simulation::time_transport.stop();
147,064✔
299

300
    finalize_generation();
147,064✔
301
  }
302

303
  finalize_batch();
146,854✔
304

305
  // Check simulation ending criteria
306
  if (status) {
146,854!
307
    if (simulation::current_batch >= settings::n_max_batches) {
146,854✔
308
      *status = STATUS_EXIT_MAX_BATCH;
7,020✔
309
    } else if (simulation::satisfy_triggers) {
139,834✔
310
      *status = STATUS_EXIT_ON_TRIGGER;
93✔
311
    } else {
312
      *status = STATUS_EXIT_NORMAL;
139,741✔
313
    }
314
  }
315
  return 0;
316
}
317

318
bool openmc_is_statepoint_batch()
3,135✔
319
{
320
  using namespace openmc;
3,135✔
321
  using openmc::simulation::current_gen;
3,135✔
322

323
  if (!simulation::initialized)
3,135!
324
    return false;
325
  else
326
    return contains(settings::statepoint_batch, simulation::current_batch);
6,270✔
327
}
328

329
namespace openmc {
330

331
//==============================================================================
332
// Global variables
333
//==============================================================================
334

335
namespace simulation {
336

337
int ct_current_file;
338
int current_batch;
339
int current_gen;
340
bool initialized {false};
341
double keff {1.0};
342
double keff_std;
343
double k_col_abs {0.0};
344
double k_col_tra {0.0};
345
double k_abs_tra {0.0};
346
double log_spacing;
347
int n_lost_particles {0};
348
bool need_depletion_rx {false};
349
int restart_batch;
350
bool satisfy_triggers {false};
351
int ssw_current_file;
352
int total_gen {0};
353
double total_weight;
354
int64_t work_per_rank;
355

356
const RegularMesh* entropy_mesh {nullptr};
357
const RegularMesh* ufs_mesh {nullptr};
358

359
vector<double> k_generation;
360
vector<int64_t> work_index;
361

362
int64_t simulation_tracks_completed {0};
363

364
} // namespace simulation
365

366
//==============================================================================
367
// Non-member functions
368
//==============================================================================
369

370
void allocate_banks()
7,990✔
371
{
372
  if (settings::run_mode == RunMode::EIGENVALUE &&
7,990✔
373
      settings::solver_type == SolverType::MONTE_CARLO) {
4,631✔
374
    // Allocate source bank
375
    simulation::source_bank.resize(simulation::work_per_rank);
4,260✔
376

377
    // Allocate fission bank
378
    init_fission_bank(3 * simulation::work_per_rank);
4,260✔
379

380
    // Allocate IFP bank
381
    if (settings::ifp_on) {
4,260✔
382
      resize_simulation_ifp_banks();
74✔
383
    }
384
  }
385

386
  if (settings::surf_source_write) {
7,990✔
387
    // Allocate surface source bank
388
    simulation::surf_source_bank.reserve(settings::ssw_max_particles);
1,154✔
389
  }
390

391
  if (settings::collision_track) {
7,990✔
392
    // Allocate collision track bank
393
    collision_track_reserve_bank();
160✔
394
  }
395
}
7,990✔
396

397
void initialize_batch()
167,829✔
398
{
399
  // Increment current batch
400
  ++simulation::current_batch;
167,829✔
401
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
167,829✔
402
    if (settings::solver_type == SolverType::RANDOM_RAY &&
65,386✔
403
        simulation::current_batch < settings::n_inactive + 1) {
14,112✔
404
      write_message(
16,812✔
405
        6, "Simulating batch {:<4} (inactive)", simulation::current_batch);
406
    } else {
407
      write_message(6, "Simulating batch {}", simulation::current_batch);
113,960✔
408
    }
409
  }
410

411
  // Reset total starting particle weight used for normalizing tallies
412
  simulation::total_weight = 0.0;
167,829✔
413

414
  // Determine if this batch is the first inactive or active batch.
415
  bool first_inactive = false;
167,829✔
416
  bool first_active = false;
167,829✔
417
  if (!settings::restart_run) {
167,829✔
418
    first_inactive = settings::n_inactive > 0 && simulation::current_batch == 1;
167,666✔
419
    first_active = simulation::current_batch == settings::n_inactive + 1;
167,666✔
420
  } else if (simulation::current_batch == simulation::restart_batch + 1) {
163✔
421
    first_inactive = simulation::restart_batch < settings::n_inactive;
52✔
422
    first_active = !first_inactive;
52✔
423
  }
424

425
  // Manage active/inactive timers and activate tallies if necessary.
426
  if (first_inactive) {
167,718✔
427
    simulation::time_inactive.start();
4,015✔
428
  } else if (first_active) {
163,814✔
429
    simulation::time_inactive.stop();
7,943✔
430
    simulation::time_active.start();
7,943✔
431
    for (auto& t : model::tallies) {
35,697✔
432
      t->active_ = true;
27,754✔
433
    }
434
  }
435

436
  // Add user tallies to active tallies list
437
  setup_active_tallies();
167,829✔
438
}
167,829✔
439

440
void finalize_batch()
167,816✔
441
{
442
  // Reduce tallies onto master process and accumulate
443
  simulation::time_tallies.start();
167,816✔
444
  accumulate_tallies();
167,816✔
445
  simulation::time_tallies.stop();
167,816✔
446

447
  // update weight windows if needed
448
  for (const auto& wwg : variance_reduction::weight_windows_generators) {
170,448✔
449
    wwg->update();
2,632✔
450
  }
451

452
  // Reset global tally results
453
  if (simulation::current_batch <= settings::n_inactive) {
167,816✔
454
    simulation::global_tallies.fill(0.0);
33,353✔
455
    simulation::n_realizations = 0;
33,353✔
456
  }
457

458
  // Check_triggers
459
  if (mpi::master)
167,816✔
460
    check_triggers();
148,457✔
461
#ifdef OPENMC_MPI
462
  MPI_Bcast(&simulation::satisfy_triggers, 1, MPI_C_BOOL, 0, mpi::intracomm);
73,668✔
463
#endif
464
  if (simulation::satisfy_triggers ||
167,816✔
465
      (settings::trigger_on &&
2,567✔
466
        simulation::current_batch == settings::n_max_batches)) {
2,567✔
467
    settings::statepoint_batch.insert(simulation::current_batch);
141✔
468
  }
469

470
  // Write out state point if it's been specified for this batch and is not
471
  // a CMFD run instance
472
  if (contains(settings::statepoint_batch, simulation::current_batch) &&
335,632✔
473
      !settings::cmfd_run) {
8,245✔
474
    if (contains(settings::sourcepoint_batch, simulation::current_batch) &&
15,860✔
475
        settings::source_write && !settings::source_separate) {
14,977✔
476
      bool b = (settings::run_mode == RunMode::EIGENVALUE);
6,837✔
477
      openmc_statepoint_write(nullptr, &b);
6,837✔
478
    } else {
479
      bool b = false;
1,232✔
480
      openmc_statepoint_write(nullptr, &b);
1,232✔
481
    }
482
  }
483

484
  if (settings::run_mode == RunMode::EIGENVALUE) {
167,816✔
485
    // Write out a separate source point if it's been specified for this batch
486
    if (contains(settings::sourcepoint_batch, simulation::current_batch) &&
107,100✔
487
        settings::source_write && settings::source_separate) {
106,729✔
488

489
      // Determine width for zero padding
490
      int w = std::to_string(settings::n_max_batches).size();
71✔
491
      std::string source_point_filename = fmt::format("{0}source.{1:0{2}}",
71✔
492
        settings::path_output, simulation::current_batch, w);
71✔
493
      span<SourceSite> bankspan(simulation::source_bank);
71✔
494
      write_source_point(source_point_filename, bankspan,
142✔
495
        simulation::work_index, settings::source_mcpl_write);
496
    }
71✔
497

498
    // Write a continously-overwritten source point if requested.
499
    if (settings::source_latest) {
102,443✔
500
      auto filename = settings::path_output + "source";
150✔
501
      span<SourceSite> bankspan(simulation::source_bank);
150✔
502
      write_source_point(filename, bankspan, simulation::work_index,
300✔
503
        settings::source_mcpl_write);
504
    }
150✔
505
  }
506

507
  // Write out surface source if requested.
508
  if (settings::surf_source_write &&
167,816✔
509
      simulation::ssw_current_file <= settings::ssw_max_files) {
17,669✔
510
    bool last_batch = (simulation::current_batch == settings::n_batches);
1,976✔
511
    if (simulation::surf_source_bank.full() || last_batch) {
1,976✔
512
      // Determine appropriate filename
513
      auto filename = fmt::format("{}surface_source.{}", settings::path_output,
1,187✔
514
        simulation::current_batch);
1,187✔
515
      if (settings::ssw_max_files == 1 ||
1,187✔
516
          (simulation::ssw_current_file == 1 && last_batch)) {
55!
517
        filename = settings::path_output + "surface_source";
1,132✔
518
      }
519

520
      // Get span of source bank and calculate parallel index vector
521
      auto surf_work_index = mpi::calculate_parallel_index_vector(
1,187✔
522
        simulation::surf_source_bank.size());
1,187✔
523
      span<SourceSite> surfbankspan(simulation::surf_source_bank.begin(),
1,187✔
524
        simulation::surf_source_bank.size());
1,187✔
525

526
      // Write surface source file
527
      write_source_point(
1,187✔
528
        filename, surfbankspan, surf_work_index, settings::surf_mcpl_write);
529

530
      // Reset surface source bank and increment counter
531
      simulation::surf_source_bank.clear();
1,187✔
532
      if (!last_batch && settings::ssw_max_files >= 1) {
1,187!
533
        simulation::surf_source_bank.reserve(settings::ssw_max_particles);
1,005✔
534
      }
535
      ++simulation::ssw_current_file;
1,187✔
536
    }
1,187✔
537
  }
538
  // Write collision track file if requested
539
  if (settings::collision_track) {
167,816✔
540
    collision_track_flush_bank();
580✔
541
  }
542
}
167,816✔
543

544
void initialize_generation()
168,039✔
545
{
546
  if (settings::run_mode == RunMode::EIGENVALUE) {
168,039✔
547
    // Clear out the fission bank
548
    simulation::fission_bank.resize(0);
102,653✔
549

550
    // Count source sites if using uniform fission source weighting
551
    if (settings::ufs_on)
102,653✔
552
      ufs_count_sites();
150✔
553

554
    // Store current value of tracklength k
555
    if (settings::delta_tracking) {
102,653✔
556
      simulation::keff_generation = simulation::global_tallies(
1,500✔
557
        GlobalTally::K_COLLISION, TallyResult::VALUE);
558
    } else {
559
      simulation::keff_generation = simulation::global_tallies(
101,153✔
560
        GlobalTally::K_TRACKLENGTH, TallyResult::VALUE);
561
    }
562
  }
563
}
168,039✔
564

565
void finalize_generation()
168,026✔
566
{
567
  auto& gt = simulation::global_tallies;
168,026✔
568

569
  // Update global tallies with the accumulation variables
570
  if (settings::run_mode == RunMode::EIGENVALUE) {
168,026✔
571
    gt(GlobalTally::K_COLLISION, TallyResult::VALUE) += global_tally_collision;
102,653✔
572
    gt(GlobalTally::K_ABSORPTION, TallyResult::VALUE) +=
102,653✔
573
      global_tally_absorption;
574
    gt(GlobalTally::K_TRACKLENGTH, TallyResult::VALUE) +=
102,653✔
575
      global_tally_tracklength;
576
  }
577
  gt(GlobalTally::LEAKAGE, TallyResult::VALUE) += global_tally_leakage;
168,026✔
578

579
  // reset tallies
580
  if (settings::run_mode == RunMode::EIGENVALUE) {
168,026✔
581
    global_tally_collision = 0.0;
102,653✔
582
    global_tally_absorption = 0.0;
102,653✔
583
    global_tally_tracklength = 0.0;
102,653✔
584
  }
585
  global_tally_leakage = 0.0;
168,026✔
586

587
  if (settings::run_mode == RunMode::EIGENVALUE &&
168,026✔
588
      settings::solver_type == SolverType::MONTE_CARLO) {
102,653✔
589
    // If using shared memory, stable sort the fission bank (by parent IDs)
590
    // so as to allow for reproducibility regardless of which order particles
591
    // are run in.
592
    sort_bank(simulation::fission_bank, true);
95,803✔
593

594
    // Distribute fission bank across processors evenly
595
    synchronize_bank();
95,803✔
596
  }
597

598
  if (settings::run_mode == RunMode::EIGENVALUE) {
168,026✔
599

600
    // Calculate shannon entropy
601
    if (settings::entropy_on &&
102,653✔
602
        settings::solver_type == SolverType::MONTE_CARLO)
14,535✔
603
      shannon_entropy();
7,685✔
604

605
    // Collect results and statistics
606
    calculate_generation_keff();
102,653✔
607
    calculate_average_keff();
102,653✔
608

609
    // Write generation output
610
    if (mpi::master && settings::verbosity >= 7) {
102,653✔
611
      print_generation();
77,188✔
612
    }
613
  }
614
}
168,026✔
615

616
void sample_source_particle(Particle& p, int64_t index_source)
179,108,577✔
617
{
618
  // Sample a particle from the source bank
619
  if (settings::run_mode == RunMode::EIGENVALUE) {
179,108,577✔
620
    p.from_source(&simulation::source_bank[index_source - 1]);
151,004,000✔
621
  } else if (settings::run_mode == RunMode::FIXED_SOURCE) {
28,104,577!
622
    // initialize random number seed
623
    int64_t id = compute_transport_seed(compute_particle_id(index_source));
28,104,577✔
624
    uint64_t seed = init_seed(id, STREAM_SOURCE);
28,104,577✔
625
    // sample from external source distribution or custom library then set
626
    auto site = sample_external_source(&seed);
28,104,577✔
627
    p.from_source(&site);
28,104,573✔
628
  }
629
}
179,108,573✔
630

631
void initialize_particle_track(
200,443,086✔
632
  Particle& p, int64_t index_source, bool is_secondary)
633
{
634
  // Note: index_source is 1-based (first particle = 1), but current_work() is
635
  // stored as 0-based for direct use as an array index into
636
  // progeny_per_particle, source_bank, ifp banks, etc.
637
  if (!is_secondary) {
200,443,086✔
638
    sample_source_particle(p, index_source);
179,108,577✔
639
  }
640

641
  p.current_work() = index_source - 1;
200,443,082✔
642

643
  // set identifier for particle
644
  p.id() = compute_particle_id(index_source);
200,443,082✔
645

646
  // set progeny count to zero
647
  p.n_progeny() = 0;
200,443,082✔
648

649
  // Reset particle event counter
650
  p.n_event() = 0;
200,443,082✔
651

652
  // Initialize track counter (1 for this primary/secondary track)
653
  p.n_tracks() = 1;
200,443,082✔
654

655
  // Reset split counter
656
  p.n_split() = 0;
200,443,082✔
657

658
  // Reset weight window ratio
659
  p.ww_factor() = 0.0;
200,443,082✔
660

661
  // set particle history start weight
662
  p.wgt_born() = p.wgt();
200,443,082✔
663

664
  // Reset pulse_height_storage
665
  std::fill(p.pht_storage().begin(), p.pht_storage().end(), 0);
200,443,082✔
666

667
  // set random number seed
668
  int64_t particle_seed = compute_transport_seed(p.id());
200,443,082✔
669
  init_particle_seeds(particle_seed, p.seeds());
200,443,082✔
670

671
  // set particle trace
672
  p.trace() = false;
200,443,082✔
673
  if (simulation::current_batch == settings::trace_batch &&
200,454,082✔
674
      simulation::current_gen == settings::trace_gen &&
200,443,082!
675
      p.id() == settings::trace_particle)
11,000✔
676
    p.trace() = true;
11✔
677

678
  // Set particle track.
679
  p.write_track() = check_track_criteria(p);
200,443,082✔
680

681
  // Set the particle's initial weight window value.
682
  if (!is_secondary) {
200,443,082✔
683
    p.wgt_ww_born() = -1.0;
179,108,573✔
684
    apply_weight_windows(p);
179,108,573✔
685
  }
686

687
  // Display message if high verbosity or trace is on
688
  if (settings::verbosity >= 9 || p.trace()) {
200,443,082!
689
    write_message("Simulating Particle {}", p.id());
22✔
690
  }
691

692
  // Compute the majorant and set the delta tracking flag.
693
  if (settings::delta_tracking) {
200,443,082✔
694
    p.delta_tracking() = true;
1,100,000✔
695
    p.update_majorant();
1,100,000✔
696
  }
697

698
  // Add particle's starting weight to count for normalizing tallies later
699
  if (!is_secondary) {
200,443,082✔
700
#pragma omp atomic
99,165,594✔
701
    simulation::total_weight += p.wgt();
179,108,573✔
702
  }
703

704
  // Force calculation of cross-sections by setting last energy to zero
705
  if (settings::run_CE) {
200,443,082✔
706
    p.invalidate_neutron_xs();
85,894,538✔
707
  }
708

709
  // Prepare to write out particle track.
710
  if (p.write_track())
200,443,082✔
711
    add_particle_track(p);
999✔
712
}
200,443,082✔
713

714
int overall_generation()
205,920,077✔
715
{
716
  using namespace simulation;
205,920,077✔
717
  return settings::gen_per_batch * (current_batch - 1) + current_gen;
205,920,077✔
718
}
719

720
int64_t compute_particle_id(int64_t index_source)
228,547,934✔
721
{
722
  if (settings::use_shared_secondary_bank) {
228,547,934✔
723
    return simulation::work_index[mpi::rank] + index_source +
22,903,534✔
724
           simulation::simulation_tracks_completed;
22,903,534✔
725
  } else {
726
    return simulation::work_index[mpi::rank] + index_source;
205,644,400✔
727
  }
728
}
729

730
int64_t compute_transport_seed(int64_t particle_id)
228,547,978✔
731
{
732
  if (settings::use_shared_secondary_bank) {
228,547,978✔
733
    return particle_id;
734
  } else {
735
    return (simulation::total_gen + overall_generation() - 1) *
205,644,433✔
736
             settings::n_particles +
737
           particle_id;
205,644,433✔
738
  }
739
}
740

741
void calculate_work(int64_t n_particles)
17,340✔
742
{
743
  // Determine minimum amount of particles to simulate on each processor
744
  int64_t min_work = n_particles / mpi::n_procs;
17,340✔
745

746
  // Determine number of processors that have one extra particle
747
  int64_t remainder = n_particles % mpi::n_procs;
17,340✔
748

749
  int64_t i_bank = 0;
17,340✔
750
  simulation::work_index.resize(mpi::n_procs + 1);
17,340✔
751
  simulation::work_index[0] = 0;
17,340✔
752
  for (int i = 0; i < mpi::n_procs; ++i) {
40,311✔
753
    // Number of particles for rank i
754
    int64_t work_i = i < remainder ? min_work + 1 : min_work;
22,971✔
755

756
    // Set number of particles
757
    if (mpi::rank == i)
22,971✔
758
      simulation::work_per_rank = work_i;
17,340✔
759

760
    // Set index into source bank for rank i
761
    i_bank += work_i;
22,971✔
762
    simulation::work_index[i + 1] = i_bank;
22,971✔
763
  }
764
}
17,340✔
765

766
void initialize_data()
6,623✔
767
{
768
  // Determine minimum/maximum energy for incident neutron/photon data
769
  data::energy_max = {INFTY, INFTY, INFTY, INFTY};
6,623✔
770
  data::energy_min = {0.0, 0.0, 0.0, 0.0};
6,623✔
771
  int neutron = ParticleType::neutron().transport_index();
6,623✔
772
  int photon = ParticleType::photon().transport_index();
6,623✔
773
  int electron = ParticleType::electron().transport_index();
6,623✔
774
  int positron = ParticleType::positron().transport_index();
6,623✔
775

776
  for (const auto& nuc : data::nuclides) {
40,544✔
777
    if (nuc->grid_.size() >= 1) {
33,921!
778
      data::energy_min[neutron] =
33,921✔
779
        std::max(data::energy_min[neutron], nuc->grid_[0].energy.front());
40,012✔
780
      data::energy_max[neutron] =
33,921✔
781
        std::min(data::energy_max[neutron], nuc->grid_[0].energy.back());
41,576✔
782
    }
783
  }
784

785
  if (settings::photon_transport) {
6,623✔
786
    for (const auto& elem : data::elements) {
2,179✔
787
      if (elem->energy_.size() >= 1) {
1,600!
788
        int n = elem->energy_.size();
1,600✔
789
        data::energy_min[photon] =
3,200✔
790
          std::max(data::energy_min[photon], std::exp(elem->energy_(1)));
2,626✔
791
        data::energy_max[photon] =
1,600✔
792
          std::min(data::energy_max[photon], std::exp(elem->energy_(n - 1)));
2,179✔
793
      }
794
    }
795

796
    if (settings::electron_treatment == ElectronTreatment::TTB) {
579✔
797
      // Determine if minimum/maximum energy for bremsstrahlung is greater/less
798
      // than the current minimum/maximum
799
      if (data::ttb_e_grid.size() >= 1) {
520!
800
        int n_e = data::ttb_e_grid.size();
520✔
801

802
        const std::vector<int> charged = {electron, positron};
520✔
803
        for (auto t : charged) {
1,560✔
804
          data::energy_min[t] = std::exp(data::ttb_e_grid(1));
1,040✔
805
          data::energy_max[t] = std::exp(data::ttb_e_grid(n_e - 1));
1,040✔
806
        }
807

808
        data::energy_min[photon] =
1,040✔
809
          std::max(data::energy_min[photon], data::energy_min[electron]);
1,040!
810

811
        data::energy_max[photon] =
1,040✔
812
          std::min(data::energy_max[photon], data::energy_max[electron]);
1,040!
813
      }
520✔
814
    }
815
  }
816

817
  // Show which nuclide results in lowest energy for neutron transport
818
  for (const auto& nuc : data::nuclides) {
8,247✔
819
    // If a nuclide is present in a material that's not used in the model, its
820
    // grid has not been allocated
821
    if (nuc->grid_.size() > 0) {
7,715!
822
      double max_E = nuc->grid_[0].energy.back();
7,715✔
823
      if (max_E == data::energy_max[neutron]) {
7,715✔
824
        write_message(7, "Maximum neutron transport energy: {} eV for {}",
6,091✔
825
          data::energy_max[neutron], nuc->name_);
6,091✔
826
        if (mpi::master && data::energy_max[neutron] < 20.0e6) {
6,091!
827
          warning("Maximum neutron energy is below 20 MeV. This may bias "
×
828
                  "the results.");
829
        }
830
        break;
831
      }
832
    }
833
  }
834

835
  // Set up logarithmic grid for nuclides
836
  for (auto& nuc : data::nuclides) {
40,544✔
837
    nuc->init_grid();
33,921✔
838
  }
839
  simulation::log_spacing =
13,246✔
840
    std::log(data::energy_max[neutron] / data::energy_min[neutron]) /
6,623✔
841
    settings::n_log_bins;
842
}
6,623✔
843

844
#ifdef OPENMC_MPI
845
void broadcast_results()
3,590✔
846
{
847
  // Broadcast tally results so that each process has access to results
848
  for (auto& t : model::tallies) {
17,383✔
849
    // Create a new datatype that consists of all values for a given filter
850
    // bin and then use that to broadcast. This is done to minimize the
851
    // chance of the 'count' argument of MPI_BCAST exceeding 2**31
852
    auto& results = t->results_;
13,793✔
853

854
    auto shape = results.shape();
13,793✔
855
    int count_per_filter = shape[1] * shape[2];
13,793✔
856
    MPI_Datatype result_block;
13,793✔
857
    MPI_Type_contiguous(count_per_filter, MPI_DOUBLE, &result_block);
13,793✔
858
    MPI_Type_commit(&result_block);
13,793✔
859
    MPI_Bcast(results.data(), shape[0], result_block, 0, mpi::intracomm);
13,793✔
860
    MPI_Type_free(&result_block);
13,793✔
861
  }
13,793✔
862

863
  // Also broadcast global tally results
864
  auto& gt = simulation::global_tallies;
3,590✔
865
  MPI_Bcast(gt.data(), gt.size(), MPI_DOUBLE, 0, mpi::intracomm);
3,590✔
866

867
  // These guys are needed so that non-master processes can calculate the
868
  // combined estimate of k-effective
869
  double temp[] {
3,590✔
870
    simulation::k_col_abs, simulation::k_col_tra, simulation::k_abs_tra};
3,590✔
871
  MPI_Bcast(temp, 3, MPI_DOUBLE, 0, mpi::intracomm);
3,590✔
872
  simulation::k_col_abs = temp[0];
3,590✔
873
  simulation::k_col_tra = temp[1];
3,590✔
874
  simulation::k_abs_tra = temp[2];
3,590✔
875
}
3,590✔
876

877
#endif
878

879
void free_memory_simulation()
9,162✔
880
{
881
  simulation::k_generation.clear();
9,162✔
882
  simulation::entropy.clear();
9,162✔
883
}
9,162✔
884

885
void transport_history_based_single_particle(Particle& p)
186,997,316✔
886
{
887
  while (p.alive()) {
2,147,483,647✔
888
    p.event_calculate_xs();
2,147,483,647✔
889
    if (p.alive()) {
2,147,483,647!
890
      p.event_advance();
2,147,483,647✔
891
    }
892
    if (p.alive()) {
2,147,483,647✔
893
      if (p.collision_distance() > p.boundary().distance()) {
2,147,483,647✔
894
        p.event_cross_surface();
2,147,483,647✔
895
      } else if (p.alive()) {
2,147,483,647✔
896
        p.event_collide();
2,147,483,647✔
897
      }
898
    }
899
    p.event_check_limit_and_revive();
2,147,483,647✔
900
  }
901
  p.event_death();
186,997,307✔
902
}
186,997,307✔
903

904
void transport_delta_history_based_single_particle(Particle& p)
800,000✔
905
{
906
  while (p.alive()) {
166,141,730✔
907
    p.event_delta_advance();
165,341,730✔
908

909
    if (p.alive()) {
165,341,730!
910
      // Electrons and positrons collide in-place, no need to rejection sample.
911
      if (p.type() == ParticleType::electron() ||
165,341,730✔
912
          p.type() == ParticleType::positron()) {
72,988,700✔
913
        p.event_collide();
92,436,050✔
914
      }
915

916
      if (p.alive() && p.collision_distance() < p.boundary().distance()) {
165,341,730✔
917
        // Collided before hitting an external boundary. Rejection sample the
918
        // majorant.
919
        p.event_calculate_xs();
63,537,450✔
920
        if (p.kill_invalid_maj()) {
63,537,450!
921
          break;
922
        }
923
        if (p.alive() &&
63,537,450!
924
            (prn(p.current_seed()) < (p.macro_xs().total / p.majorant()))) {
63,537,450✔
925
          p.event_collide();
22,537,730✔
926
        }
927
      } else if (p.alive()) {
101,804,280✔
928
        // Crossed an external boundary before colliding.
929
        p.event_cross_surface();
9,368,230✔
930
      }
931
    }
932

933
    p.event_check_limit_and_revive();
165,341,730✔
934
  }
935
  p.event_death();
800,000✔
936
}
800,000✔
937

938
void transport_history_based()
142,799✔
939
{
940
#pragma omp parallel
79,579✔
941
  {
63,220✔
942
    Particle p;
63,220✔
943
#pragma omp for schedule(runtime)
944
    for (int64_t i_work = 1; i_work <= simulation::work_per_rank; ++i_work) {
80,939,045✔
945
      initialize_particle_track(p, i_work, false);
80,875,834✔
946
      if (settings::delta_tracking) {
80,875,830✔
947
        transport_delta_history_based_single_particle(p);
400,000✔
948
      } else {
949
        transport_history_based_single_particle(p);
80,475,830✔
950
      }
951
    }
952
  }
63,211✔
953
}
142,790✔
954

955
// The shared secondary bank transport algorithm works in two phases. In the
956
// first phase, all primary particles are sampled then transported, and their
957
// secondary particles are deposited into a shared secondary bank. The second
958
// phase occurs in a loop, where all secondary tracks in the shared secondary
959
// bank are transported. Any secondary particles generated during this phase are
960
// deposited back into the shared secondary bank. The shared secondary bank is
961
// sorted for consistent ordering and load balanced across MPI ranks. This loop
962
// continues until there are no more secondary tracks left to transport.
963
void transport_history_based_shared_secondary()
667✔
964
{
965
  // Clear shared secondary banks from any prior use
966
  simulation::shared_secondary_bank_read.clear();
667✔
967
  simulation::shared_secondary_bank_write.clear();
667✔
968

969
  if (mpi::master) {
667✔
970
    write_message(fmt::format(" Primary source          particles: {}",
1,150✔
971
                    settings::n_particles),
972
      6);
973
  }
974

975
  simulation::progeny_per_particle.resize(simulation::work_per_rank);
667✔
976
  std::fill(simulation::progeny_per_particle.begin(),
1,334✔
977
    simulation::progeny_per_particle.end(), 0);
667✔
978

979
  // Phase 1: Transport primary particles and deposit first generation of
980
  // secondaries in the shared secondary bank
981
#pragma omp parallel
384✔
982
  {
283✔
983
    vector<SourceSite> thread_bank;
283✔
984
    Particle p;
283✔
985

986
#pragma omp for schedule(runtime)
987
    for (int64_t i = 1; i <= simulation::work_per_rank; i++) {
356,058✔
988
      initialize_particle_track(p, i, false);
355,775✔
989
      if (settings::delta_tracking) {
355,775!
990
        transport_delta_history_based_single_particle(p);
×
991
      } else {
992
        transport_history_based_single_particle(p);
355,775✔
993
      }
994
      for (auto& site : p.local_secondary_bank()) {
1,087,225✔
995
        thread_bank.push_back(site);
731,450✔
996
      }
997
      p.local_secondary_bank().clear();
444,500✔
998
    }
999

1000
    // Drain thread-local bank into the shared secondary bank (once per thread)
1001
#pragma omp critical(SharedSecondaryBank)
1002
    {
283✔
1003
      for (auto& site : thread_bank) {
731,733✔
1004
        simulation::shared_secondary_bank_write.thread_unsafe_append(site);
731,450✔
1005
      }
1006
    }
1007
  }
283✔
1008

1009
  simulation::simulation_tracks_completed += settings::n_particles;
667✔
1010

1011
  // Phase 2: Now that the secondary bank has been populated, enter loop over
1012
  // all secondary generations
1013
  int n_generation_depth = 1;
667✔
1014
  int64_t alive_secondary = 1;
667✔
1015
  while (alive_secondary) {
8,933✔
1016

1017
    // Sort the shared secondary bank by parent ID then progeny ID to
1018
    // ensure reproducibility.
1019
    sort_bank(simulation::shared_secondary_bank_write, false);
8,266✔
1020

1021
    // Synchronize the shared secondary bank amongst all MPI ranks, such
1022
    // that each MPI rank has an approximately equal number of secondary
1023
    // tracks. Also reports the total number of secondaries alive across
1024
    // all MPI ranks.
1025
    alive_secondary = synchronize_global_secondary_bank(
8,266✔
1026
      simulation::shared_secondary_bank_write);
1027

1028
    // Recalculate work for each MPI rank based on number of alive secondary
1029
    // tracks
1030
    calculate_work(alive_secondary);
8,266✔
1031

1032
    // Display the number of secondary tracks in this generation. This
1033
    // is useful for user monitoring so as to see if the secondary population is
1034
    // exploding and to determine how many generations of secondaries are being
1035
    // transported.
1036
    if (mpi::master) {
8,266✔
1037
      write_message(fmt::format(" Secondary generation {:<2}    tracks: {}",
13,188✔
1038
                      n_generation_depth, alive_secondary),
1039
        6);
1040
    }
1041

1042
    simulation::shared_secondary_bank_read =
8,266✔
1043
      std::move(simulation::shared_secondary_bank_write);
8,266✔
1044
    simulation::shared_secondary_bank_write = SharedArray<SourceSite>();
8,266!
1045
    simulation::progeny_per_particle.resize(
8,266✔
1046
      simulation::shared_secondary_bank_read.size());
8,266✔
1047
    std::fill(simulation::progeny_per_particle.begin(),
16,532✔
1048
      simulation::progeny_per_particle.end(), 0);
8,266✔
1049

1050
    // Transport all secondary tracks from the shared secondary bank
1051
#pragma omp parallel
4,673✔
1052
    {
3,593✔
1053
      vector<SourceSite> thread_bank;
3,593✔
1054
      Particle p;
3,593✔
1055

1056
#pragma omp for schedule(runtime)
1057
      for (int64_t i = 1; i <= simulation::shared_secondary_bank_read.size();
9,727,253✔
1058
           i++) {
1059
        initialize_particle_track(p, i, true);
9,723,660✔
1060
        SourceSite& site = simulation::shared_secondary_bank_read[i - 1];
9,723,660✔
1061
        p.event_revive_from_secondary(site);
9,723,660✔
1062
        if (settings::delta_tracking) {
9,723,660!
1063
          transport_delta_history_based_single_particle(p);
×
1064
        } else {
1065
          transport_history_based_single_particle(p);
9,723,660✔
1066
        }
1067
        for (auto& secondary_site : p.local_secondary_bank()) {
18,715,870✔
1068
          thread_bank.push_back(secondary_site);
8,992,210✔
1069
        }
1070
        p.local_secondary_bank().clear();
11,189,190✔
1071
      }
1072

1073
      // Drain thread-local bank into the shared secondary bank (once per
1074
      // thread)
1075
#pragma omp critical(SharedSecondaryBank)
1076
      {
3,593✔
1077
        for (auto& secondary_site : thread_bank) {
8,995,803✔
1078
          simulation::shared_secondary_bank_write.thread_unsafe_append(
8,992,210✔
1079
            secondary_site);
1080
        }
1081
      }
1082
    } // End of transport loop over tracks in shared secondary bank
3,593✔
1083
    n_generation_depth++;
8,266✔
1084
    simulation::simulation_tracks_completed += alive_secondary;
8,266✔
1085
  } // End of loop over secondary generations
1086

1087
  // Reset work so that fission bank etc works correctly
1088
  calculate_work(settings::n_particles);
667✔
1089
}
667✔
1090

1091
void transport_event_based()
3,600✔
1092
{
1093
  int64_t remaining_work = simulation::work_per_rank;
3,600✔
1094
  int64_t source_offset = 0;
3,600✔
1095

1096
  // To cap the total amount of memory used to store particle object data, the
1097
  // number of particles in flight at any point in time can bet set. In the case
1098
  // that the maximum in flight particle count is lower than the total number
1099
  // of particles that need to be run this iteration, the event-based transport
1100
  // loop is executed multiple times until all particles have been completed.
1101
  while (remaining_work > 0) {
7,200✔
1102
    // Figure out # of particles to run for this subiteration
1103
    int64_t n_particles =
3,600!
1104
      std::min(remaining_work, settings::max_particles_in_flight);
3,600✔
1105

1106
    // Initialize all particle histories for this subiteration
1107
    if (settings::delta_tracking) {
3,600✔
1108
      process_delta_init_events(n_particles, source_offset);
380✔
1109
      process_delta_transport_events();
380✔
1110
    } else {
1111
      process_init_events(n_particles, source_offset);
3,220✔
1112
      process_transport_events();
3,220✔
1113
    }
1114
    process_death_events(n_particles);
3,600✔
1115

1116
    // Adjust remaining work and source offset variables
1117
    remaining_work -= n_particles;
3,600✔
1118
    source_offset += n_particles;
3,600✔
1119
  }
1120
}
3,600✔
1121

1122
void transport_event_based_shared_secondary()
11✔
1123
{
1124
  // Clear shared secondary banks from any prior use
1125
  simulation::shared_secondary_bank_read.clear();
11✔
1126
  simulation::shared_secondary_bank_write.clear();
11✔
1127

1128
  if (mpi::master) {
11!
1129
    write_message(fmt::format(" Primary source          particles: {}",
22!
1130
                    settings::n_particles),
1131
      6);
1132
  }
1133

1134
  simulation::progeny_per_particle.resize(simulation::work_per_rank);
11✔
1135
  std::fill(simulation::progeny_per_particle.begin(),
22✔
1136
    simulation::progeny_per_particle.end(), 0);
11✔
1137

1138
  // Phase 1: Transport primary particles using event-based processing and
1139
  // deposit first generation of secondaries in the shared secondary bank
1140
  int64_t remaining_work = simulation::work_per_rank;
11✔
1141
  int64_t source_offset = 0;
11✔
1142

1143
  while (remaining_work > 0) {
22✔
1144
    int64_t n_particles =
11!
1145
      std::min(remaining_work, settings::max_particles_in_flight);
11✔
1146

1147
    if (settings::delta_tracking) {
11!
NEW
1148
      process_delta_init_events(n_particles, source_offset);
×
NEW
1149
      process_delta_transport_events();
×
1150
    } else {
1151
      process_init_events(n_particles, source_offset);
11✔
1152
      process_transport_events();
11✔
1153
    }
1154
    process_death_events(n_particles);
11✔
1155

1156
    // Collect secondaries from all particle buffers into shared bank
1157
    for (int64_t i = 0; i < n_particles; i++) {
1,661✔
1158
      for (auto& site : simulation::particles[i].local_secondary_bank()) {
6,132✔
1159
        simulation::shared_secondary_bank_write.thread_unsafe_append(site);
4,482✔
1160
      }
1161
      simulation::particles[i].local_secondary_bank().clear();
3,017✔
1162
    }
1163

1164
    remaining_work -= n_particles;
11✔
1165
    source_offset += n_particles;
11✔
1166
  }
1167

1168
  simulation::simulation_tracks_completed += settings::n_particles;
11✔
1169

1170
  // Phase 2: Now that the secondary bank has been populated, enter loop over
1171
  // all secondary generations
1172
  int n_generation_depth = 1;
11✔
1173
  int64_t alive_secondary = 1;
11✔
1174
  while (alive_secondary) {
417✔
1175

1176
    // Sort the shared secondary bank by parent ID then progeny ID to
1177
    // ensure reproducibility.
1178
    sort_bank(simulation::shared_secondary_bank_write, false);
406✔
1179

1180
    // Synchronize the shared secondary bank amongst all MPI ranks, such
1181
    // that each MPI rank has an approximately equal number of secondary
1182
    // tracks.
1183
    alive_secondary = synchronize_global_secondary_bank(
406✔
1184
      simulation::shared_secondary_bank_write);
1185

1186
    // Recalculate work for each MPI rank based on number of alive secondary
1187
    // tracks
1188
    calculate_work(alive_secondary);
406✔
1189

1190
    if (mpi::master) {
406!
1191
      write_message(fmt::format(" Secondary generation {:<2}    tracks: {}",
812!
1192
                      n_generation_depth, alive_secondary),
1193
        6);
1194
    }
1195

1196
    simulation::shared_secondary_bank_read =
406✔
1197
      std::move(simulation::shared_secondary_bank_write);
406✔
1198
    simulation::shared_secondary_bank_write = SharedArray<SourceSite>();
406!
1199
    simulation::progeny_per_particle.resize(
406✔
1200
      simulation::shared_secondary_bank_read.size());
406✔
1201
    std::fill(simulation::progeny_per_particle.begin(),
812✔
1202
      simulation::progeny_per_particle.end(), 0);
406✔
1203

1204
    // Ensure particle buffer is large enough for this secondary generation
1205
    int64_t sec_buffer_length = std::min(
406!
1206
      static_cast<int64_t>(simulation::shared_secondary_bank_read.size()),
406!
1207
      settings::max_particles_in_flight);
406✔
1208
    if (sec_buffer_length >
406✔
1209
        static_cast<int64_t>(simulation::particles.size())) {
406✔
1210
      init_event_queues(sec_buffer_length);
34✔
1211
    }
1212

1213
    // Transport secondary tracks using event-based processing
1214
    int64_t sec_remaining = simulation::shared_secondary_bank_read.size();
406✔
1215
    int64_t sec_offset = 0;
406✔
1216

1217
    while (sec_remaining > 0) {
801✔
1218
      int64_t n_particles =
395!
1219
        std::min(sec_remaining, settings::max_particles_in_flight);
395✔
1220

1221
      if (settings::delta_tracking) {
395!
NEW
1222
        process_delta_init_secondary_events(
×
1223
          n_particles, sec_offset, simulation::shared_secondary_bank_read);
NEW
1224
        process_delta_transport_events();
×
1225
      } else {
1226
        process_init_secondary_events(
395✔
1227
          n_particles, sec_offset, simulation::shared_secondary_bank_read);
1228
        process_transport_events();
395✔
1229
      }
1230
      process_death_events(n_particles);
395✔
1231

1232
      // Collect secondaries from all particle buffers into shared bank
1233
      for (int64_t i = 0; i < n_particles; i++) {
180,005✔
1234
        for (auto& site : simulation::particles[i].local_secondary_bank()) {
354,738✔
1235
          simulation::shared_secondary_bank_write.thread_unsafe_append(site);
175,128✔
1236
        }
1237
        simulation::particles[i].local_secondary_bank().clear();
226,133✔
1238
      }
1239

1240
      sec_remaining -= n_particles;
395✔
1241
      sec_offset += n_particles;
395✔
1242
    } // End of subiteration loop over secondary tracks
1243
    n_generation_depth++;
406✔
1244
    simulation::simulation_tracks_completed += alive_secondary;
406✔
1245
  } // End of loop over secondary generations
1246

1247
  // Reset work so that fission bank etc works correctly
1248
  calculate_work(settings::n_particles);
11✔
1249
}
11✔
1250

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