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

openmc-dev / openmc / 6050282475

01 Sep 2023 01:52PM UTC coverage: 84.438% (-0.005%) from 84.443%
6050282475

push

github

web-flow
Prevent segfault in distance to boundary calculation (#2659)

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

2 existing lines in 1 file now uncovered.

46824 of 55454 relevant lines covered (84.44%)

84639265.51 hits per line

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

92.12
/src/particle.cpp
1
#include "openmc/particle.h"
2

3
#include <algorithm> // copy, min
4
#include <cmath>     // log, abs
5

6
#include <fmt/core.h>
7

8
#include "openmc/bank.h"
9
#include "openmc/capi.h"
10
#include "openmc/cell.h"
11
#include "openmc/constants.h"
12
#include "openmc/dagmc.h"
13
#include "openmc/error.h"
14
#include "openmc/geometry.h"
15
#include "openmc/hdf5_interface.h"
16
#include "openmc/material.h"
17
#include "openmc/message_passing.h"
18
#include "openmc/mgxs_interface.h"
19
#include "openmc/nuclide.h"
20
#include "openmc/particle_data.h"
21
#include "openmc/photon.h"
22
#include "openmc/physics.h"
23
#include "openmc/physics_mg.h"
24
#include "openmc/random_lcg.h"
25
#include "openmc/settings.h"
26
#include "openmc/simulation.h"
27
#include "openmc/source.h"
28
#include "openmc/surface.h"
29
#include "openmc/tallies/derivative.h"
30
#include "openmc/tallies/tally.h"
31
#include "openmc/tallies/tally_scoring.h"
32
#include "openmc/track_output.h"
33

34
#ifdef DAGMC
35
#include "DagMC.hpp"
36
#endif
37

38
namespace openmc {
39

40
//==============================================================================
41
// Particle implementation
42
//==============================================================================
43

44
double Particle::speed() const
5,372,784,445✔
45
{
46
  // Determine mass in eV/c^2
47
  double mass;
48
  switch (this->type()) {
5,372,784,445✔
49
  case ParticleType::neutron:
5,301,129,099✔
50
    mass = MASS_NEUTRON_EV;
5,301,129,099✔
51
    break;
5,301,129,099✔
52
  case ParticleType::photon:
14,463,386✔
53
    mass = 0.0;
14,463,386✔
54
    break;
14,463,386✔
55
  case ParticleType::electron:
57,191,960✔
56
  case ParticleType::positron:
57
    mass = MASS_ELECTRON_EV;
57,191,960✔
58
    break;
57,191,960✔
59
  }
60

61
  // Calculate inverse of Lorentz factor
62
  const double inv_gamma = mass / (this->E() + mass);
5,372,784,445✔
63

64
  // Calculate speed via v = c * sqrt(1 - γ^-2)
65
  return C_LIGHT * std::sqrt(1 - inv_gamma * inv_gamma);
5,372,784,445✔
66
}
67

68
void Particle::move_distance(double length)
14,000✔
69
{
70
  for (int j = 0; j < n_coord(); ++j) {
28,000✔
71
    coord(j).r += length * coord(j).u;
14,000✔
72
  }
73
}
14,000✔
74

75
void Particle::create_secondary(
122,179,530✔
76
  double wgt, Direction u, double E, ParticleType type)
77
{
78
  // If energy is below cutoff for this particle, don't create secondary
79
  // particle
80
  if (E < settings::energy_cutoff[static_cast<int>(type)]) {
122,179,530✔
81
    return;
57,055,334✔
82
  }
83

84
  secondary_bank().emplace_back();
65,124,196✔
85

86
  auto& bank {secondary_bank().back()};
65,124,196✔
87
  bank.particle = type;
65,124,196✔
88
  bank.wgt = wgt;
65,124,196✔
89
  bank.r = r();
65,124,196✔
90
  bank.u = u;
65,124,196✔
91
  bank.E = settings::run_CE ? E : g();
65,124,196✔
92
  bank.time = time();
65,124,196✔
93

94
  n_bank_second() += 1;
65,124,196✔
95
}
96

97
void Particle::from_source(const SourceSite* src)
265,030,073✔
98
{
99
  // Reset some attributes
100
  clear();
265,030,073✔
101
  surface() = 0;
265,030,073✔
102
  cell_born() = C_NONE;
265,030,073✔
103
  material() = C_NONE;
265,030,073✔
104
  n_collision() = 0;
265,030,073✔
105
  fission() = false;
265,030,073✔
106
  zero_flux_derivs();
265,030,073✔
107

108
  // Copy attributes from source bank site
109
  type() = src->particle;
265,030,073✔
110
  wgt() = src->wgt;
265,030,073✔
111
  wgt_last() = src->wgt;
265,030,073✔
112
  r() = src->r;
265,030,073✔
113
  u() = src->u;
265,030,073✔
114
  r_last_current() = src->r;
265,030,073✔
115
  r_last() = src->r;
265,030,073✔
116
  u_last() = src->u;
265,030,073✔
117
  if (settings::run_CE) {
265,030,073✔
118
    E() = src->E;
123,854,073✔
119
    g() = 0;
123,854,073✔
120
  } else {
121
    g() = static_cast<int>(src->E);
141,176,000✔
122
    g_last() = static_cast<int>(src->E);
141,176,000✔
123
    E() = data::mg.energy_bin_avg_[g()];
141,176,000✔
124
  }
125
  E_last() = E();
265,030,073✔
126
  time() = src->time;
265,030,073✔
127
  time_last() = src->time;
265,030,073✔
128
}
265,030,073✔
129

130
void Particle::event_calculate_xs()
5,342,919,183✔
131
{
132
  // Set the random number stream
133
  stream() = STREAM_TRACKING;
5,342,919,183✔
134

135
  // Store pre-collision particle properties
136
  wgt_last() = wgt();
5,342,919,183✔
137
  E_last() = E();
5,342,919,183✔
138
  u_last() = u();
5,342,919,183✔
139
  r_last() = r();
5,342,919,183✔
140
  time_last() = time();
5,342,919,183✔
141

142
  // Reset event variables
143
  event() = TallyEvent::KILL;
5,342,919,183✔
144
  event_nuclide() = NUCLIDE_NONE;
5,342,919,183✔
145
  event_mt() = REACTION_NONE;
5,342,919,183✔
146

147
  // If the cell hasn't been determined based on the particle's location,
148
  // initiate a search for the current cell. This generally happens at the
149
  // beginning of the history and again for any secondary particles
150
  if (lowest_coord().cell == C_NONE) {
5,342,919,183✔
151
    if (!exhaustive_find_cell(*this)) {
262,229,231✔
152
      mark_as_lost(
98✔
153
        "Could not find the cell containing particle " + std::to_string(id()));
196✔
154
      return;
98✔
155
    }
156

157
    // Set birth cell attribute
158
    if (cell_born() == C_NONE)
262,229,133✔
159
      cell_born() = lowest_coord().cell;
262,229,133✔
160
  }
161

162
  // Write particle track.
163
  if (write_track())
5,342,919,085✔
164
    write_particle_track(*this);
13,194✔
165

166
  if (settings::check_overlaps)
5,342,919,085✔
167
    check_cell_overlap(*this);
×
168

169
  // Calculate microscopic and macroscopic cross sections
170
  if (material() != MATERIAL_VOID) {
5,342,919,085✔
171
    if (settings::run_CE) {
5,284,686,180✔
172
      if (material() != material_last() || sqrtkT() != sqrtkT_last()) {
2,704,305,132✔
173
        // If the material is the same as the last material and the
174
        // temperature hasn't changed, we don't need to lookup cross
175
        // sections again.
176
        model::materials[material()]->calculate_xs(*this);
2,038,345,497✔
177
      }
178
    } else {
179
      // Get the MG data; unlike the CE case above, we have to re-calculate
180
      // cross sections for every collision since the cross sections may
181
      // be angle-dependent
182
      data::mg.macro_xs_[material()].calculate_xs(*this);
2,580,381,048✔
183

184
      // Update the particle's group while we know we are multi-group
185
      g_last() = g();
2,580,381,048✔
186
    }
187
  } else {
188
    macro_xs().total = 0.0;
58,232,905✔
189
    macro_xs().absorption = 0.0;
58,232,905✔
190
    macro_xs().fission = 0.0;
58,232,905✔
191
    macro_xs().nu_fission = 0.0;
58,232,905✔
192
  }
193
}
194

195
void Particle::event_advance()
5,342,919,085✔
196
{
197
  // Find the distance to the nearest boundary
198
  boundary() = distance_to_boundary(*this);
5,342,919,085✔
199

200
  // Sample a distance to collision
201
  if (type() == ParticleType::electron || type() == ParticleType::positron) {
5,342,919,085✔
202
    collision_distance() = 0.0;
57,191,960✔
203
  } else if (macro_xs().total == 0.0) {
5,285,727,125✔
204
    collision_distance() = INFINITY;
58,232,905✔
205
  } else {
206
    collision_distance() = -std::log(prn(current_seed())) / macro_xs().total;
5,227,494,220✔
207
  }
208

209
  // Select smaller of the two distances
210
  double distance = std::min(boundary().distance, collision_distance());
5,342,919,085✔
211

212
  // Advance particle in space and time
213
  // Short-term solution until the surface source is revised and we can use
214
  // this->move_distance(distance)
215
  for (int j = 0; j < n_coord(); ++j) {
11,814,165,362✔
216
    coord(j).r += distance * coord(j).u;
6,471,246,277✔
217
  }
218
  this->time() += distance / this->speed();
5,342,919,085✔
219

220
  // Kill particle if its time exceeds the cutoff
221
  bool hit_time_boundary = false;
5,342,919,085✔
222
  double time_cutoff = settings::time_cutoff[static_cast<int>(type())];
5,342,919,085✔
223
  if (time() > time_cutoff) {
5,342,919,085✔
224
    double dt = time() - time_cutoff;
14,000✔
225
    time() = time_cutoff;
14,000✔
226

227
    double push_back_distance = speed() * dt;
14,000✔
228
    this->move_distance(-push_back_distance);
14,000✔
229
    hit_time_boundary = true;
14,000✔
230
  }
231

232
  // Score track-length tallies
233
  if (!model::active_tracklength_tallies.empty()) {
5,342,919,085✔
234
    score_tracklength_tally(*this, distance);
1,837,526,265✔
235
  }
236

237
  // Score track-length estimate of k-eff
238
  if (settings::run_mode == RunMode::EIGENVALUE &&
10,299,591,775✔
239
      type() == ParticleType::neutron) {
4,956,672,690✔
240
    keff_tally_tracklength() += wgt() * distance * macro_xs().nu_fission;
4,905,993,390✔
241
  }
242

243
  // Score flux derivative accumulators for differential tallies.
244
  if (!model::active_tallies.empty()) {
5,342,919,085✔
245
    score_track_derivative(*this, distance);
2,394,135,551✔
246
  }
247

248
  // Set particle weight to zero if it hit the time boundary
249
  if (hit_time_boundary) {
5,342,919,085✔
250
    wgt() = 0.0;
14,000✔
251
  }
252
}
5,342,919,085✔
253

254
void Particle::event_cross_surface()
1,968,386,919✔
255
{
256
  // Set surface that particle is on and adjust coordinate levels
257
  surface() = boundary().surface_index;
1,968,386,919✔
258
  n_coord() = boundary().coord_level;
1,968,386,919✔
259

260
  // Saving previous cell data
261
  for (int j = 0; j < n_coord(); ++j) {
4,608,277,792✔
262
    cell_last(j) = coord(j).cell;
2,639,890,873✔
263
  }
264
  n_coord_last() = n_coord();
1,968,386,919✔
265

266
  if (boundary().lattice_translation[0] != 0 ||
1,968,386,919✔
267
      boundary().lattice_translation[1] != 0 ||
3,846,226,424✔
268
      boundary().lattice_translation[2] != 0) {
1,877,839,505✔
269
    // Particle crosses lattice boundary
270
    cross_lattice(*this, boundary());
94,400,516✔
271
    event() = TallyEvent::LATTICE;
94,400,516✔
272
  } else {
273
    // Particle crosses surface
274
    cross_surface();
1,873,986,403✔
275
    event() = TallyEvent::SURFACE;
1,873,986,403✔
276
  }
277
  // Score cell to cell partial currents
278
  if (!model::active_surface_tallies.empty()) {
1,968,386,919✔
279
    score_surface_tally(*this, model::active_surface_tallies);
5,244,164✔
280
  }
281
}
1,968,386,919✔
282

283
void Particle::event_collide()
3,374,531,166✔
284
{
285
  // Score collision estimate of keff
286
  if (settings::run_mode == RunMode::EIGENVALUE &&
6,390,620,932✔
287
      type() == ParticleType::neutron) {
3,016,089,766✔
288
    keff_tally_collision() += wgt() * macro_xs().nu_fission / macro_xs().total;
2,965,433,888✔
289
  }
290

291
  // Score surface current tallies -- this has to be done before the collision
292
  // since the direction of the particle will change and we need to use the
293
  // pre-collision direction to figure out what mesh surfaces were crossed
294

295
  if (!model::active_meshsurf_tallies.empty())
3,374,531,166✔
296
    score_surface_tally(*this, model::active_meshsurf_tallies);
407,818,822✔
297

298
  // Clear surface component
299
  surface() = 0;
3,374,531,166✔
300

301
  if (settings::run_CE) {
3,374,531,166✔
302
    collision(*this);
1,111,658,736✔
303
  } else {
304
    collision_mg(*this);
2,262,872,430✔
305
  }
306

307
  // Score collision estimator tallies -- this is done after a collision
308
  // has occurred rather than before because we need information on the
309
  // outgoing energy for any tallies with an outgoing energy filter
310
  if (!model::active_collision_tallies.empty())
3,374,531,166✔
311
    score_collision_tally(*this);
80,660,688✔
312
  if (!model::active_analog_tallies.empty()) {
3,374,531,166✔
313
    if (settings::run_CE) {
439,105,168✔
314
      score_analog_tally_ce(*this);
437,576,354✔
315
    } else {
316
      score_analog_tally_mg(*this);
1,528,814✔
317
    }
318
  }
319

320
  if (!model::active_pulse_height_tallies.empty() &&
3,374,552,698✔
321
      type() == ParticleType::photon) {
21,532✔
322
    pht_collision_energy();
2,576✔
323
  }
324

325
  // Reset banked weight during collision
326
  n_bank() = 0;
3,374,531,166✔
327
  n_bank_second() = 0;
3,374,531,166✔
328
  wgt_bank() = 0.0;
3,374,531,166✔
329
  zero_delayed_bank();
3,374,531,166✔
330

331
  // Reset fission logical
332
  fission() = false;
3,374,531,166✔
333

334
  // Save coordinates for tallying purposes
335
  r_last_current() = r();
3,374,531,166✔
336

337
  // Set last material to none since cross sections will need to be
338
  // re-evaluated
339
  material_last() = C_NONE;
3,374,531,166✔
340

341
  // Set all directions to base level -- right now, after a collision, only
342
  // the base level directions are changed
343
  for (int j = 0; j < n_coord() - 1; ++j) {
3,574,700,512✔
344
    if (coord(j + 1).rotated) {
200,169,346✔
345
      // If next level is rotated, apply rotation matrix
346
      const auto& m {model::cells[coord(j).cell]->rotation_};
13,229,090✔
347
      const auto& u {coord(j).u};
13,229,090✔
348
      coord(j + 1).u = u.rotate(m);
13,229,090✔
349
    } else {
350
      // Otherwise, copy this level's direction
351
      coord(j + 1).u = coord(j).u;
186,940,256✔
352
    }
353
  }
354

355
  // Score flux derivative accumulators for differential tallies.
356
  if (!model::active_tallies.empty())
3,374,531,166✔
357
    score_collision_derivative(*this);
995,285,694✔
358

359
#ifdef DAGMC
360
  history().reset();
316,357,498✔
361
#endif
362
}
3,374,531,166✔
363

364
void Particle::event_revive_from_secondary()
5,342,918,085✔
365
{
366
  // If particle has too many events, display warning and kill it
367
  ++n_event();
5,342,918,085✔
368
  if (n_event() == MAX_EVENTS) {
5,342,918,085✔
369
    warning("Particle " + std::to_string(id()) +
×
370
            " underwent maximum number of events.");
371
    wgt() = 0.0;
×
372
  }
373

374
  // Check for secondary particles if this particle is dead
375
  if (!alive()) {
5,342,918,085✔
376
    // Write final position for this particle
377
    if (write_track()) {
262,228,903✔
378
      write_particle_track(*this);
7,962✔
379
    }
380

381
    // If no secondary particles, break out of event loop
382
    if (secondary_bank().empty())
262,228,903✔
383
      return;
196,901,450✔
384

385
    from_source(&secondary_bank().back());
65,327,453✔
386
    secondary_bank().pop_back();
65,327,453✔
387
    n_event() = 0;
65,327,453✔
388

389
    // Subtract secondary particle energy from interim pulse-height results
390
    if (!model::active_pulse_height_tallies.empty() &&
65,347,179✔
391
        this->type() == ParticleType::photon) {
19,726✔
392
      // Since the birth cell of the particle has not been set we
393
      // have to determine it before the energy of the secondary particle can be
394
      // removed from the pulse-height of this cell.
395
      if (lowest_coord().cell == C_NONE) {
770✔
396
        if (!exhaustive_find_cell(*this)) {
770✔
397
          mark_as_lost("Could not find the cell containing particle " +
×
398
                       std::to_string(id()));
×
399
          return;
×
400
        }
401
        // Set birth cell attribute
402
        if (cell_born() == C_NONE)
770✔
403
          cell_born() = lowest_coord().cell;
770✔
404
      }
405
      pht_secondary_particles();
770✔
406
    }
407

408
    // Enter new particle in particle track file
409
    if (write_track())
65,327,453✔
410
      add_particle_track(*this);
6,696✔
411
  }
412
}
413

414
void Particle::event_death()
196,902,548✔
415
{
416
#ifdef DAGMC
417
  history().reset();
17,656,532✔
418
#endif
419

420
  // Finish particle track output.
421
  if (write_track()) {
196,902,548✔
422
    finalize_particle_track(*this);
1,266✔
423
  }
424

425
// Contribute tally reduction variables to global accumulator
426
#pragma omp atomic
87,407,658✔
427
  global_tally_absorption += keff_tally_absorption();
196,902,548✔
428
#pragma omp atomic
87,500,537✔
429
  global_tally_collision += keff_tally_collision();
196,902,548✔
430
#pragma omp atomic
87,332,034✔
431
  global_tally_tracklength += keff_tally_tracklength();
196,902,548✔
432
#pragma omp atomic
87,212,437✔
433
  global_tally_leakage += keff_tally_leakage();
196,902,548✔
434

435
  // Reset particle tallies once accumulated
436
  keff_tally_absorption() = 0.0;
196,902,548✔
437
  keff_tally_collision() = 0.0;
196,902,548✔
438
  keff_tally_tracklength() = 0.0;
196,902,548✔
439
  keff_tally_leakage() = 0.0;
196,902,548✔
440

441
  if (!model::active_pulse_height_tallies.empty()) {
196,902,548✔
442
    score_pulse_height_tally(*this, model::active_pulse_height_tallies);
7,000✔
443
  }
444

445
  // Record the number of progeny created by this particle.
446
  // This data will be used to efficiently sort the fission bank.
447
  if (settings::run_mode == RunMode::EIGENVALUE) {
196,902,548✔
448
    int64_t offset = id() - 1 - simulation::work_index[mpi::rank];
183,641,200✔
449
    simulation::progeny_per_particle[offset] = n_progeny();
183,641,200✔
450
  }
451
}
196,902,548✔
452

453
void Particle::pht_collision_energy()
2,576✔
454
{
455
  // Adds the energy particles lose in a collision to the pulse-height
456

457
  // determine index of cell in pulse_height_cells
458
  auto it = std::find(model::pulse_height_cells.begin(),
2,576✔
459
    model::pulse_height_cells.end(), lowest_coord().cell);
2,576✔
460

461
  if (it != model::pulse_height_cells.end()) {
2,576✔
462
    int index = std::distance(model::pulse_height_cells.begin(), it);
2,576✔
463
    pht_storage()[index] += E_last() - E();
2,576✔
464

465
    // If the energy of the particle is below the cutoff, it will not be sampled
466
    // so its energy is added to the pulse-height in the cell
467
    int photon = static_cast<int>(ParticleType::photon);
2,576✔
468
    if (E() < settings::energy_cutoff[photon]) {
2,576✔
469
      pht_storage()[index] += E();
1,050✔
470
    }
471
  }
472
}
2,576✔
473

474
void Particle::pht_secondary_particles()
770✔
475
{
476
  // Removes the energy of secondary produced particles from the pulse-height
477

478
  // determine index of cell in pulse_height_cells
479
  auto it = std::find(model::pulse_height_cells.begin(),
770✔
480
    model::pulse_height_cells.end(), cell_born());
770✔
481

482
  if (it != model::pulse_height_cells.end()) {
770✔
483
    int index = std::distance(model::pulse_height_cells.begin(), it);
770✔
484
    pht_storage()[index] -= E();
770✔
485
  }
486
}
770✔
487

488
void Particle::cross_surface()
1,873,986,403✔
489
{
490
  int i_surface = std::abs(surface());
1,873,986,403✔
491
  // TODO: off-by-one
492
  const auto& surf {model::surfaces[i_surface - 1].get()};
1,873,986,403✔
493
  if (settings::verbosity >= 10 || trace()) {
1,873,986,403✔
494
    write_message(1, "    Crossing surface {}", surf->id_);
28✔
495
  }
496

497
  if (surf->surf_source_ && simulation::current_batch > settings::n_inactive &&
1,874,126,403✔
498
      !simulation::surf_source_bank.full()) {
140,000✔
499
    SourceSite site;
14,000✔
500
    site.r = r();
14,000✔
501
    site.u = u();
14,000✔
502
    site.E = E();
14,000✔
503
    site.time = time();
14,000✔
504
    site.wgt = wgt();
14,000✔
505
    site.delayed_group = delayed_group();
14,000✔
506
    site.surf_id = surf->id_;
14,000✔
507
    site.particle = type();
14,000✔
508
    site.parent_id = id();
14,000✔
509
    site.progeny_id = n_progeny();
14,000✔
510
    int64_t idx = simulation::surf_source_bank.thread_safe_append(site);
14,000✔
511
  }
512

513
// if we're crossing a CSG surface, make sure the DAG history is reset
514
#ifdef DAGMC
515
  if (surf->geom_type_ == GeometryType::CSG)
242,697,025✔
516
    history().reset();
242,680,524✔
517
#endif
518

519
  // Handle any applicable boundary conditions.
520
  if (surf->bc_ && settings::run_mode != RunMode::PLOTTING) {
1,873,986,403✔
521
    surf->bc_->handle_particle(*this, *surf);
938,432,576✔
522
    return;
1,873,958,053✔
523
  }
524

525
  // ==========================================================================
526
  // SEARCH NEIGHBOR LISTS FOR NEXT CELL
527

528
#ifdef DAGMC
529
  // in DAGMC, we know what the next cell should be
530
  if (surf->geom_type_ == GeometryType::DAG) {
118,833,099✔
531
    int32_t i_cell =
532
      next_cell(i_surface, cell_last(n_coord() - 1), lowest_coord().universe) -
13,155✔
533
      1;
13,155✔
534
    // save material and temp
535
    material_last() = material();
13,155✔
536
    sqrtkT_last() = sqrtkT();
13,155✔
537
    // set new cell value
538
    lowest_coord().cell = i_cell;
13,155✔
539
    cell_instance() = 0;
13,155✔
540
    material() = model::cells[i_cell]->material_[0];
13,155✔
541
    sqrtkT() = model::cells[i_cell]->sqrtkT_[0];
13,155✔
542
    return;
13,155✔
543
  }
544
#endif
545

546
  if (neighbor_list_find_cell(*this))
935,540,672✔
547
    return;
935,512,140✔
548

549
  // ==========================================================================
550
  // COULDN'T FIND PARTICLE IN NEIGHBORING CELLS, SEARCH ALL CELLS
551

552
  // Remove lower coordinate levels
553
  n_coord() = 1;
28,532✔
554
  bool found = exhaustive_find_cell(*this);
28,532✔
555

556
  if (settings::run_mode != RunMode::PLOTTING && (!found)) {
28,532✔
557
    // If a cell is still not found, there are two possible causes: 1) there is
558
    // a void in the model, and 2) the particle hit a surface at a tangent. If
559
    // the particle is really traveling tangent to a surface, if we move it
560
    // forward a tiny bit it should fix the problem.
561

562
    surface() = 0;
182✔
563
    n_coord() = 1;
182✔
564
    r() += TINY_BIT * u();
182✔
565

566
    // Couldn't find next cell anywhere! This probably means there is an actual
567
    // undefined region in the geometry.
568

569
    if (!exhaustive_find_cell(*this)) {
182✔
570
      mark_as_lost("After particle " + std::to_string(id()) +
546✔
571
                   " crossed surface " + std::to_string(surf->id_) +
728✔
572
                   " it could not be located in any cell and it did not leak.");
573
      return;
182✔
574
    }
575
  }
576
}
577

578
void Particle::cross_vacuum_bc(const Surface& surf)
27,054,069✔
579
{
580
  // Score any surface current tallies -- note that the particle is moved
581
  // forward slightly so that if the mesh boundary is on the surface, it is
582
  // still processed
583

584
  if (!model::active_meshsurf_tallies.empty()) {
27,054,069✔
585
    // TODO: Find a better solution to score surface currents than
586
    // physically moving the particle forward slightly
587

588
    r() += TINY_BIT * u();
5,600,430✔
589
    score_surface_tally(*this, model::active_meshsurf_tallies);
5,600,430✔
590
  }
591

592
  // Score to global leakage tally
593
  keff_tally_leakage() += wgt();
27,054,069✔
594

595
  // Kill the particle
596
  wgt() = 0.0;
27,054,069✔
597

598
  // Display message
599
  if (settings::verbosity >= 10 || trace()) {
27,054,069✔
600
    write_message(1, "    Leaked out of surface {}", surf.id_);
14✔
601
  }
602
}
27,054,069✔
603

604
void Particle::cross_reflective_bc(const Surface& surf, Direction new_u)
910,568,495✔
605
{
606
  // Do not handle reflective boundary conditions on lower universes
607
  if (n_coord() != 1) {
910,568,495✔
608
    mark_as_lost("Cannot reflect particle " + std::to_string(id()) +
×
609
                 " off surface in a lower universe.");
610
    return;
×
611
  }
612

613
  // Score surface currents since reflection causes the direction of the
614
  // particle to change. For surface filters, we need to score the tallies
615
  // twice, once before the particle's surface attribute has changed and
616
  // once after. For mesh surface filters, we need to artificially move
617
  // the particle slightly back in case the surface crossing is coincident
618
  // with a mesh boundary
619

620
  if (!model::active_surface_tallies.empty()) {
910,568,495✔
621
    score_surface_tally(*this, model::active_surface_tallies);
358,666✔
622
  }
623

624
  if (!model::active_meshsurf_tallies.empty()) {
910,568,495✔
625
    Position r {this->r()};
302,152,970✔
626
    this->r() -= TINY_BIT * u();
302,152,970✔
627
    score_surface_tally(*this, model::active_meshsurf_tallies);
302,152,970✔
628
    this->r() = r;
302,152,970✔
629
  }
630

631
  // Set the new particle direction
632
  u() = new_u;
910,568,495✔
633

634
  // Reassign particle's cell and surface
635
  coord(0).cell = cell_last(n_coord_last() - 1);
910,568,495✔
636
  surface() = -surface();
910,568,495✔
637

638
  // If a reflective surface is coincident with a lattice or universe
639
  // boundary, it is necessary to redetermine the particle's coordinates in
640
  // the lower universes.
641
  // (unless we're using a dagmc model, which has exactly one universe)
642
  n_coord() = 1;
910,568,495✔
643
  if (surf.geom_type_ != GeometryType::DAG && !neighbor_list_find_cell(*this)) {
910,568,495✔
644
    this->mark_as_lost("Couldn't find particle after reflecting from surface " +
×
645
                       std::to_string(surf.id_) + ".");
×
646
    return;
×
647
  }
648

649
  // Set previous coordinate going slightly past surface crossing
650
  r_last_current() = r() + TINY_BIT * u();
910,568,495✔
651

652
  // Diagnostic message
653
  if (settings::verbosity >= 10 || trace()) {
910,568,495✔
654
    write_message(1, "    Reflected from surface {}", surf.id_);
×
655
  }
656
}
657

658
void Particle::cross_periodic_bc(
810,012✔
659
  const Surface& surf, Position new_r, Direction new_u, int new_surface)
660
{
661
  // Do not handle periodic boundary conditions on lower universes
662
  if (n_coord() != 1) {
810,012✔
663
    mark_as_lost(
×
664
      "Cannot transfer particle " + std::to_string(id()) +
×
665
      " across surface in a lower universe. Boundary conditions must be "
666
      "applied to root universe.");
667
    return;
×
668
  }
669

670
  // Score surface currents since reflection causes the direction of the
671
  // particle to change -- artificially move the particle slightly back in
672
  // case the surface crossing is coincident with a mesh boundary
673
  if (!model::active_meshsurf_tallies.empty()) {
810,012✔
674
    Position r {this->r()};
×
675
    this->r() -= TINY_BIT * u();
×
676
    score_surface_tally(*this, model::active_meshsurf_tallies);
×
677
    this->r() = r;
×
678
  }
679

680
  // Adjust the particle's location and direction.
681
  r() = new_r;
810,012✔
682
  u() = new_u;
810,012✔
683

684
  // Reassign particle's surface
685
  surface() = new_surface;
810,012✔
686

687
  // Figure out what cell particle is in now
688
  n_coord() = 1;
810,012✔
689

690
  if (!neighbor_list_find_cell(*this)) {
810,012✔
691
    mark_as_lost("Couldn't find particle after hitting periodic "
×
692
                 "boundary on surface " +
×
693
                 std::to_string(surf.id_) +
×
694
                 ". The normal vector "
695
                 "of one periodic surface may need to be reversed.");
696
    return;
×
697
  }
698

699
  // Set previous coordinate going slightly past surface crossing
700
  r_last_current() = r() + TINY_BIT * u();
810,012✔
701

702
  // Diagnostic message
703
  if (settings::verbosity >= 10 || trace()) {
810,012✔
704
    write_message(1, "    Hit periodic boundary on surface {}", surf.id_);
×
705
  }
706
}
707

708
void Particle::mark_as_lost(const char* message)
280✔
709
{
710
  // Print warning and write lost particle file
711
  warning(message);
280✔
712
  write_restart();
280✔
713

714
  // Increment number of lost particles
715
  wgt() = 0.0;
280✔
716
#pragma omp atomic
120✔
717
  simulation::n_lost_particles += 1;
160✔
718

719
  // Count the total number of simulated particles (on this processor)
720
  auto n = simulation::current_batch * settings::gen_per_batch *
280✔
721
           simulation::work_per_rank;
722

723
  // Abort the simulation if the maximum number of lost particles has been
724
  // reached
725
  if (simulation::n_lost_particles >= settings::max_lost_particles &&
280✔
UNCOV
726
      simulation::n_lost_particles >= settings::rel_max_lost_particles * n) {
×
UNCOV
727
    fatal_error("Maximum number of lost particles has been reached.");
×
728
  }
729
}
280✔
730

731
void Particle::write_restart() const
280✔
732
{
733
  // Dont write another restart file if in particle restart mode
734
  if (settings::run_mode == RunMode::PARTICLE)
280✔
735
    return;
14✔
736

737
  // Set up file name
738
  auto filename = fmt::format("{}particle_{}_{}.h5", settings::path_output,
739
    simulation::current_batch, id());
266✔
740

741
#pragma omp critical(WriteParticleRestart)
228✔
742
  {
743
    // Create file
744
    hid_t file_id = file_open(filename, 'w');
266✔
745

746
    // Write filetype and version info
747
    write_attribute(file_id, "filetype", "particle restart");
266✔
748
    write_attribute(file_id, "version", VERSION_PARTICLE_RESTART);
266✔
749
    write_attribute(file_id, "openmc_version", VERSION);
266✔
750
#ifdef GIT_SHA1
751
    write_attr_string(file_id, "git_sha1", GIT_SHA1);
266✔
752
#endif
753

754
    // Write data to file
755
    write_dataset(file_id, "current_batch", simulation::current_batch);
266✔
756
    write_dataset(file_id, "generations_per_batch", settings::gen_per_batch);
266✔
757
    write_dataset(file_id, "current_generation", simulation::current_gen);
266✔
758
    write_dataset(file_id, "n_particles", settings::n_particles);
266✔
759
    switch (settings::run_mode) {
266✔
760
    case RunMode::FIXED_SOURCE:
70✔
761
      write_dataset(file_id, "run_mode", "fixed source");
70✔
762
      break;
70✔
763
    case RunMode::EIGENVALUE:
196✔
764
      write_dataset(file_id, "run_mode", "eigenvalue");
196✔
765
      break;
196✔
766
    case RunMode::PARTICLE:
×
767
      write_dataset(file_id, "run_mode", "particle restart");
×
768
      break;
×
769
    default:
×
770
      break;
×
771
    }
772
    write_dataset(file_id, "id", id());
266✔
773
    write_dataset(file_id, "type", static_cast<int>(type()));
266✔
774

775
    int64_t i = current_work();
266✔
776
    if (settings::run_mode == RunMode::EIGENVALUE) {
266✔
777
      // take source data from primary bank for eigenvalue simulation
778
      write_dataset(file_id, "weight", simulation::source_bank[i - 1].wgt);
196✔
779
      write_dataset(file_id, "energy", simulation::source_bank[i - 1].E);
196✔
780
      write_dataset(file_id, "xyz", simulation::source_bank[i - 1].r);
196✔
781
      write_dataset(file_id, "uvw", simulation::source_bank[i - 1].u);
196✔
782
      write_dataset(file_id, "time", simulation::source_bank[i - 1].time);
196✔
783
    } else if (settings::run_mode == RunMode::FIXED_SOURCE) {
70✔
784
      // re-sample using rng random number seed used to generate source particle
785
      int64_t id = (simulation::total_gen + overall_generation() - 1) *
70✔
786
                     settings::n_particles +
70✔
787
                   simulation::work_index[mpi::rank] + i;
70✔
788
      uint64_t seed = init_seed(id, STREAM_SOURCE);
70✔
789
      // re-sample source site
790
      auto site = sample_external_source(&seed);
70✔
791
      write_dataset(file_id, "weight", site.wgt);
70✔
792
      write_dataset(file_id, "energy", site.E);
70✔
793
      write_dataset(file_id, "xyz", site.r);
70✔
794
      write_dataset(file_id, "uvw", site.u);
70✔
795
      write_dataset(file_id, "time", site.time);
70✔
796
    }
797

798
    // Close file
799
    file_close(file_id);
266✔
800
  } // #pragma omp critical
801
}
266✔
802

803
void Particle::update_neutron_xs(
10,899,049,274✔
804
  int i_nuclide, int i_grid, int i_sab, double sab_frac, double ncrystal_xs)
805
{
806
  // Get microscopic cross section cache
807
  auto& micro = this->neutron_xs(i_nuclide);
10,899,049,274✔
808

809
  // If the cache doesn't match, recalculate micro xs
810
  if (this->E() != micro.last_E || this->sqrtkT() != micro.last_sqrtkT ||
15,218,895,514✔
811
      i_sab != micro.index_sab || sab_frac != micro.sab_frac) {
15,218,895,514✔
812
    data::nuclides[i_nuclide]->calculate_xs(i_sab, i_grid, sab_frac, *this);
6,463,518,540✔
813

814
    // If NCrystal is being used, update micro cross section cache
815
    if (ncrystal_xs >= 0.0) {
6,463,518,540✔
816
      data::nuclides[i_nuclide]->calculate_elastic_xs(*this);
1,001,722✔
817
      ncrystal_update_micro(ncrystal_xs, micro);
1,001,722✔
818
    }
819
  }
820
}
10,899,049,274✔
821

822
//==============================================================================
823
// Non-method functions
824
//==============================================================================
825

826
std::string particle_type_to_str(ParticleType type)
3,786,734✔
827
{
828
  switch (type) {
3,786,734✔
829
  case ParticleType::neutron:
2,859,234✔
830
    return "neutron";
2,859,234✔
831
  case ParticleType::photon:
927,220✔
832
    return "photon";
927,220✔
833
  case ParticleType::electron:
140✔
834
    return "electron";
140✔
835
  case ParticleType::positron:
140✔
836
    return "positron";
140✔
837
  }
838
  UNREACHABLE();
×
839
}
840

841
ParticleType str_to_particle_type(std::string str)
3,876,009✔
842
{
843
  if (str == "neutron") {
3,876,009✔
844
    return ParticleType::neutron;
886,897✔
845
  } else if (str == "photon") {
2,989,112✔
846
    return ParticleType::photon;
2,989,036✔
847
  } else if (str == "electron") {
76✔
848
    return ParticleType::electron;
38✔
849
  } else if (str == "positron") {
38✔
850
    return ParticleType::positron;
38✔
851
  } else {
852
    throw std::invalid_argument {fmt::format("Invalid particle name: {}", str)};
×
853
  }
854
}
855

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