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

openmc-dev / openmc / 6720785330

01 Nov 2023 02:13PM UTC coverage: 84.68% (-0.002%) from 84.682%
6720785330

push

github

web-flow
Add RectangularPrism and HexagonalPrism composite surfaces (#2739)

Co-authored-by: Patrick Shriwise <pshriwise@gmail.com>

102 of 146 new or added lines in 4 files covered. (69.86%)

3 existing lines in 2 files now uncovered.

47131 of 55658 relevant lines covered (84.68%)

85749123.0 hits per line

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

91.96
/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
#include "openmc/weight_windows.h"
34

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

39
namespace openmc {
40

41
//==============================================================================
42
// Particle implementation
43
//==============================================================================
44

45
double Particle::speed() const
5,571,518,939✔
46
{
47
  // Determine mass in eV/c^2
48
  double mass;
49
  switch (this->type()) {
5,571,518,939✔
50
  case ParticleType::neutron:
5,499,862,403✔
51
    mass = MASS_NEUTRON_EV;
5,499,862,403✔
52
    break;
5,499,862,403✔
53
  case ParticleType::photon:
14,463,582✔
54
    mass = 0.0;
14,463,582✔
55
    break;
14,463,582✔
56
  case ParticleType::electron:
57,192,954✔
57
  case ParticleType::positron:
58
    mass = MASS_ELECTRON_EV;
57,192,954✔
59
    break;
57,192,954✔
60
  }
61

62
  // Calculate inverse of Lorentz factor
63
  const double inv_gamma = mass / (this->E() + mass);
5,571,518,939✔
64

65
  // Calculate speed via v = c * sqrt(1 - γ^-2)
66
  return C_LIGHT * std::sqrt(1 - inv_gamma * inv_gamma);
5,571,518,939✔
67
}
68

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

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

85
  secondary_bank().emplace_back();
65,136,766✔
86

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

95
  n_bank_second() += 1;
65,136,766✔
96
}
97

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

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

131
void Particle::event_calculate_xs()
5,541,653,677✔
132
{
133
  // Set the random number stream
134
  stream() = STREAM_TRACKING;
5,541,653,677✔
135

136
  // Store pre-collision particle properties
137
  wgt_last() = wgt();
5,541,653,677✔
138
  E_last() = E();
5,541,653,677✔
139
  u_last() = u();
5,541,653,677✔
140
  r_last() = r();
5,541,653,677✔
141
  time_last() = time();
5,541,653,677✔
142

143
  // Reset event variables
144
  event() = TallyEvent::KILL;
5,541,653,677✔
145
  event_nuclide() = NUCLIDE_NONE;
5,541,653,677✔
146
  event_mt() = REACTION_NONE;
5,541,653,677✔
147

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

158
    // Set birth cell attribute
159
    if (cell_born() == C_NONE)
266,529,703✔
160
      cell_born() = lowest_coord().cell;
266,529,703✔
161
  }
162

163
  // Write particle track.
164
  if (write_track())
5,541,653,579✔
165
    write_particle_track(*this);
13,196✔
166

167
  if (settings::check_overlaps)
5,541,653,579✔
168
    check_cell_overlap(*this);
×
169

170
  // Calculate microscopic and macroscopic cross sections
171
  if (material() != MATERIAL_VOID) {
5,541,653,579✔
172
    if (settings::run_CE) {
5,483,420,674✔
173
      if (material() != material_last() || sqrtkT() != sqrtkT_last()) {
2,903,039,626✔
174
        // If the material is the same as the last material and the
175
        // temperature hasn't changed, we don't need to lookup cross
176
        // sections again.
177
        model::materials[material()]->calculate_xs(*this);
2,171,287,185✔
178
      }
179
    } else {
180
      // Get the MG data; unlike the CE case above, we have to re-calculate
181
      // cross sections for every collision since the cross sections may
182
      // be angle-dependent
183
      data::mg.macro_xs_[material()].calculate_xs(*this);
2,580,381,048✔
184

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

196
void Particle::event_advance()
5,541,653,579✔
197
{
198
  // Find the distance to the nearest boundary
199
  boundary() = distance_to_boundary(*this);
5,541,653,579✔
200

201
  // Sample a distance to collision
202
  if (type() == ParticleType::electron || type() == ParticleType::positron) {
5,541,653,579✔
203
    collision_distance() = 0.0;
57,192,954✔
204
  } else if (macro_xs().total == 0.0) {
5,484,460,625✔
205
    collision_distance() = INFINITY;
58,232,905✔
206
  } else {
207
    collision_distance() = -std::log(prn(current_seed())) / macro_xs().total;
5,426,227,720✔
208
  }
209

210
  // Select smaller of the two distances
211
  double distance = std::min(boundary().distance, collision_distance());
5,541,653,579✔
212

213
  // Advance particle in space and time
214
  // Short-term solution until the surface source is revised and we can use
215
  // this->move_distance(distance)
216
  for (int j = 0; j < n_coord(); ++j) {
12,350,800,691✔
217
    coord(j).r += distance * coord(j).u;
6,809,147,112✔
218
  }
219
  this->time() += distance / this->speed();
5,541,653,579✔
220

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

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

233
  // Score track-length tallies
234
  if (!model::active_tracklength_tallies.empty()) {
5,541,653,579✔
235
    score_tracklength_tally(*this, distance);
1,936,116,106✔
236
  }
237

238
  // Score track-length estimate of k-eff
239
  if (settings::run_mode == RunMode::EIGENVALUE &&
10,695,536,269✔
240
      type() == ParticleType::neutron) {
5,153,882,690✔
241
    keff_tally_tracklength() += wgt() * distance * macro_xs().nu_fission;
5,103,203,390✔
242
  }
243

244
  // Score flux derivative accumulators for differential tallies.
245
  if (!model::active_tallies.empty()) {
5,541,653,579✔
246
    score_track_derivative(*this, distance);
2,582,607,821✔
247
  }
248

249
  // Set particle weight to zero if it hit the time boundary
250
  if (hit_time_boundary) {
5,541,653,579✔
251
    wgt() = 0.0;
14,000✔
252
  }
253
}
5,541,653,579✔
254

255
void Particle::event_cross_surface()
2,087,943,057✔
256
{
257
  // Set surface that particle is on and adjust coordinate levels
258
  surface() = boundary().surface_index;
2,087,943,057✔
259
  n_coord() = boundary().coord_level;
2,087,943,057✔
260

261
  // Saving previous cell data
262
  for (int j = 0; j < n_coord(); ++j) {
4,939,078,789✔
263
    cell_last(j) = coord(j).cell;
2,851,135,732✔
264
  }
265
  n_coord_last() = n_coord();
2,087,943,057✔
266

267
  if (boundary().lattice_translation[0] != 0 ||
2,087,943,057✔
268
      boundary().lattice_translation[1] != 0 ||
4,077,921,843✔
269
      boundary().lattice_translation[2] != 0) {
1,989,978,786✔
270
    // Particle crosses lattice boundary
271
    cross_lattice(*this, boundary());
101,817,373✔
272
    event() = TallyEvent::LATTICE;
101,817,373✔
273
  } else {
274
    // Particle crosses surface
275
    cross_surface();
1,986,125,684✔
276
    if (settings::weight_window_checkpoint_surface) {
1,986,125,684✔
277
      apply_weight_windows(*this);
×
278
    }
279
    event() = TallyEvent::SURFACE;
1,986,125,684✔
280
  }
281
  // Score cell to cell partial currents
282
  if (!model::active_surface_tallies.empty()) {
2,087,943,057✔
283
    score_surface_tally(*this, model::active_surface_tallies);
5,244,164✔
284
  }
285
}
2,087,943,057✔
286

287
void Particle::event_collide()
3,453,709,522✔
288
{
289
  // Score collision estimate of keff
290
  if (settings::run_mode == RunMode::EIGENVALUE &&
6,547,730,475✔
291
      type() == ParticleType::neutron) {
3,094,020,953✔
292
    keff_tally_collision() += wgt() * macro_xs().nu_fission / macro_xs().total;
3,043,365,075✔
293
  }
294

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

299
  if (!model::active_meshsurf_tallies.empty())
3,453,709,522✔
300
    score_surface_tally(*this, model::active_meshsurf_tallies);
481,278,923✔
301

302
  // Clear surface component
303
  surface() = 0;
3,453,709,522✔
304

305
  if (settings::run_CE) {
3,453,709,522✔
306
    collision(*this);
1,190,837,092✔
307
  } else {
308
    collision_mg(*this);
2,262,872,430✔
309
  }
310

311
  // Score collision estimator tallies -- this is done after a collision
312
  // has occurred rather than before because we need information on the
313
  // outgoing energy for any tallies with an outgoing energy filter
314
  if (!model::active_collision_tallies.empty())
3,453,709,522✔
315
    score_collision_tally(*this);
80,661,878✔
316
  if (!model::active_analog_tallies.empty()) {
3,453,709,522✔
317
    if (settings::run_CE) {
512,565,269✔
318
      score_analog_tally_ce(*this);
511,036,455✔
319
    } else {
320
      score_analog_tally_mg(*this);
1,528,814✔
321
    }
322
  }
323

324
  if (!model::active_pulse_height_tallies.empty() &&
3,453,731,054✔
325
      type() == ParticleType::photon) {
21,532✔
326
    pht_collision_energy();
2,576✔
327
  }
328

329
  // Reset banked weight during collision
330
  n_bank() = 0;
3,453,709,522✔
331
  n_bank_second() = 0;
3,453,709,522✔
332
  wgt_bank() = 0.0;
3,453,709,522✔
333
  zero_delayed_bank();
3,453,709,522✔
334

335
  // Reset fission logical
336
  fission() = false;
3,453,709,522✔
337

338
  // Save coordinates for tallying purposes
339
  r_last_current() = r();
3,453,709,522✔
340

341
  // Set last material to none since cross sections will need to be
342
  // re-evaluated
343
  material_last() = C_NONE;
3,453,709,522✔
344

345
  // Set all directions to base level -- right now, after a collision, only
346
  // the base level directions are changed
347
  for (int j = 0; j < n_coord() - 1; ++j) {
3,674,671,824✔
348
    if (coord(j + 1).rotated) {
220,962,302✔
349
      // If next level is rotated, apply rotation matrix
350
      const auto& m {model::cells[coord(j).cell]->rotation_};
13,229,090✔
351
      const auto& u {coord(j).u};
13,229,090✔
352
      coord(j + 1).u = u.rotate(m);
13,229,090✔
353
    } else {
354
      // Otherwise, copy this level's direction
355
      coord(j + 1).u = coord(j).u;
207,733,212✔
356
    }
357
  }
358

359
  // Score flux derivative accumulators for differential tallies.
360
  if (!model::active_tallies.empty())
3,453,709,522✔
361
    score_collision_derivative(*this);
1,071,457,620✔
362

363
#ifdef DAGMC
364
  history().reset();
304,880,402✔
365
#endif
366
}
3,453,709,522✔
367

368
void Particle::event_revive_from_secondary()
5,541,652,579✔
369
{
370
  // If particle has too many events, display warning and kill it
371
  ++n_event();
5,541,652,579✔
372
  if (n_event() == MAX_EVENTS) {
5,541,652,579✔
373
    warning("Particle " + std::to_string(id()) +
×
374
            " underwent maximum number of events.");
375
    wgt() = 0.0;
×
376
  }
377

378
  // Check for secondary particles if this particle is dead
379
  if (!alive()) {
5,541,652,579✔
380
    // Write final position for this particle
381
    if (write_track()) {
266,529,473✔
382
      write_particle_track(*this);
7,962✔
383
    }
384

385
    // If no secondary particles, break out of event loop
386
    if (secondary_bank().empty())
266,529,473✔
387
      return;
201,189,450✔
388

389
    from_source(&secondary_bank().back());
65,340,023✔
390
    secondary_bank().pop_back();
65,340,023✔
391
    n_event() = 0;
65,340,023✔
392

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

412
    // Enter new particle in particle track file
413
    if (write_track())
65,340,023✔
414
      add_particle_track(*this);
6,696✔
415
  }
416
}
417

418
void Particle::event_death()
201,190,548✔
419
{
420
#ifdef DAGMC
421
  history().reset();
17,129,032✔
422
#endif
423

424
  // Finish particle track output.
425
  if (write_track()) {
201,190,548✔
426
    finalize_particle_track(*this);
1,266✔
427
  }
428

429
// Contribute tally reduction variables to global accumulator
430
#pragma omp atomic
87,854,807✔
431
  global_tally_absorption += keff_tally_absorption();
201,190,548✔
432
#pragma omp atomic
87,907,824✔
433
  global_tally_collision += keff_tally_collision();
201,190,548✔
434
#pragma omp atomic
87,831,997✔
435
  global_tally_tracklength += keff_tally_tracklength();
201,190,548✔
436
#pragma omp atomic
87,735,334✔
437
  global_tally_leakage += keff_tally_leakage();
201,190,548✔
438

439
  // Reset particle tallies once accumulated
440
  keff_tally_absorption() = 0.0;
201,190,548✔
441
  keff_tally_collision() = 0.0;
201,190,548✔
442
  keff_tally_tracklength() = 0.0;
201,190,548✔
443
  keff_tally_leakage() = 0.0;
201,190,548✔
444

445
  if (!model::active_pulse_height_tallies.empty()) {
201,190,548✔
446
    score_pulse_height_tally(*this, model::active_pulse_height_tallies);
7,000✔
447
  }
448

449
  // Record the number of progeny created by this particle.
450
  // This data will be used to efficiently sort the fission bank.
451
  if (settings::run_mode == RunMode::EIGENVALUE) {
201,190,548✔
452
    int64_t offset = id() - 1 - simulation::work_index[mpi::rank];
187,642,200✔
453
    simulation::progeny_per_particle[offset] = n_progeny();
187,642,200✔
454
  }
455
}
201,190,548✔
456

457
void Particle::pht_collision_energy()
2,576✔
458
{
459
  // Adds the energy particles lose in a collision to the pulse-height
460

461
  // determine index of cell in pulse_height_cells
462
  auto it = std::find(model::pulse_height_cells.begin(),
2,576✔
463
    model::pulse_height_cells.end(), lowest_coord().cell);
2,576✔
464

465
  if (it != model::pulse_height_cells.end()) {
2,576✔
466
    int index = std::distance(model::pulse_height_cells.begin(), it);
2,576✔
467
    pht_storage()[index] += E_last() - E();
2,576✔
468

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

478
void Particle::pht_secondary_particles()
770✔
479
{
480
  // Removes the energy of secondary produced particles from the pulse-height
481

482
  // determine index of cell in pulse_height_cells
483
  auto it = std::find(model::pulse_height_cells.begin(),
770✔
484
    model::pulse_height_cells.end(), cell_born());
770✔
485

486
  if (it != model::pulse_height_cells.end()) {
770✔
487
    int index = std::distance(model::pulse_height_cells.begin(), it);
770✔
488
    pht_storage()[index] -= E();
770✔
489
  }
490
}
770✔
491

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

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

517
// if we're crossing a CSG surface, make sure the DAG history is reset
518
#ifdef DAGMC
519
  if (surf->geom_type_ == GeometryType::CSG)
222,735,244✔
520
    history().reset();
222,718,743✔
521
#endif
522

523
  // Handle any applicable boundary conditions.
524
  if (surf->bc_ && settings::run_mode != RunMode::PLOTTING) {
1,986,125,684✔
525
    surf->bc_->handle_particle(*this, *surf);
998,064,411✔
526
    return;
1,986,097,334✔
527
  }
528

529
  // ==========================================================================
530
  // SEARCH NEIGHBOR LISTS FOR NEXT CELL
531

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

550
  if (neighbor_list_find_cell(*this))
988,048,118✔
551
    return;
988,012,586✔
552

553
  // ==========================================================================
554
  // COULDN'T FIND PARTICLE IN NEIGHBORING CELLS, SEARCH ALL CELLS
555

556
  // Remove lower coordinate levels
557
  n_coord() = 1;
35,532✔
558
  bool found = exhaustive_find_cell(*this);
35,532✔
559

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

566
    surface() = 0;
7,182✔
567
    n_coord() = 1;
7,182✔
568
    r() += TINY_BIT * u();
7,182✔
569

570
    // Couldn't find next cell anywhere! This probably means there is an actual
571
    // undefined region in the geometry.
572

573
    if (!exhaustive_find_cell(*this)) {
7,182✔
574
      mark_as_lost("After particle " + std::to_string(id()) +
21,546✔
575
                   " crossed surface " + std::to_string(surf->id_) +
28,728✔
576
                   " it could not be located in any cell and it did not leak.");
577
      return;
7,182✔
578
    }
579
  }
580
}
581

582
void Particle::cross_vacuum_bc(const Surface& surf)
29,617,486✔
583
{
584
  // Score any surface current tallies -- note that the particle is moved
585
  // forward slightly so that if the mesh boundary is on the surface, it is
586
  // still processed
587

588
  if (!model::active_meshsurf_tallies.empty()) {
29,617,486✔
589
    // TODO: Find a better solution to score surface currents than
590
    // physically moving the particle forward slightly
591

592
    r() += TINY_BIT * u();
6,586,025✔
593
    score_surface_tally(*this, model::active_meshsurf_tallies);
6,586,025✔
594
  }
595

596
  // Score to global leakage tally
597
  keff_tally_leakage() += wgt();
29,617,486✔
598

599
  // Kill the particle
600
  wgt() = 0.0;
29,617,486✔
601

602
  // Display message
603
  if (settings::verbosity >= 10 || trace()) {
29,617,486✔
604
    write_message(1, "    Leaked out of surface {}", surf.id_);
14✔
605
  }
606
}
29,617,486✔
607

608
void Particle::cross_reflective_bc(const Surface& surf, Direction new_u)
968,944,457✔
609
{
610
  // Do not handle reflective boundary conditions on lower universes
611
  if (n_coord() != 1) {
968,944,457✔
612
    mark_as_lost("Cannot reflect particle " + std::to_string(id()) +
×
613
                 " off surface in a lower universe.");
614
    return;
×
615
  }
616

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

624
  if (!model::active_surface_tallies.empty()) {
968,944,457✔
625
    score_surface_tally(*this, model::active_surface_tallies);
358,666✔
626
  }
627

628
  if (!model::active_meshsurf_tallies.empty()) {
968,944,457✔
629
    Position r {this->r()};
356,576,248✔
630
    this->r() -= TINY_BIT * u();
356,576,248✔
631
    score_surface_tally(*this, model::active_meshsurf_tallies);
356,576,248✔
632
    this->r() = r;
356,576,248✔
633
  }
634

635
  // Set the new particle direction
636
  u() = new_u;
968,944,457✔
637

638
  // Reassign particle's cell and surface
639
  coord(0).cell = cell_last(n_coord_last() - 1);
968,944,457✔
640
  surface() = -surface();
968,944,457✔
641

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

653
  // Set previous coordinate going slightly past surface crossing
654
  r_last_current() = r() + TINY_BIT * u();
968,944,457✔
655

656
  // Diagnostic message
657
  if (settings::verbosity >= 10 || trace()) {
968,944,457✔
658
    write_message(1, "    Reflected from surface {}", surf.id_);
×
659
  }
660
}
661

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

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

684
  // Adjust the particle's location and direction.
685
  r() = new_r;
810,012✔
686
  u() = new_u;
810,012✔
687

688
  // Reassign particle's surface
689
  surface() = new_surface;
810,012✔
690

691
  // Figure out what cell particle is in now
692
  n_coord() = 1;
810,012✔
693

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

703
  // Set previous coordinate going slightly past surface crossing
704
  r_last_current() = r() + TINY_BIT * u();
810,012✔
705

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

712
void Particle::mark_as_lost(const char* message)
7,280✔
713
{
714
  // Print warning and write lost particle file
715
  warning(message);
7,280✔
716
  if (settings::max_write_lost_particles < 0 ||
7,280✔
717
      simulation::n_lost_particles < settings::max_write_lost_particles) {
7,000✔
718
    write_restart();
375✔
719
  }
720
  // Increment number of lost particles
721
  wgt() = 0.0;
7,280✔
722
#pragma omp atomic
3,120✔
723
  simulation::n_lost_particles += 1;
4,160✔
724

725
  // Count the total number of simulated particles (on this processor)
726
  auto n = simulation::current_batch * settings::gen_per_batch *
7,280✔
727
           simulation::work_per_rank;
728

729
  // Abort the simulation if the maximum number of lost particles has been
730
  // reached
731
  if (simulation::n_lost_particles >= settings::max_lost_particles &&
7,280✔
UNCOV
732
      simulation::n_lost_particles >= settings::rel_max_lost_particles * n) {
×
UNCOV
733
    fatal_error("Maximum number of lost particles has been reached.");
×
734
  }
735
}
7,280✔
736

737
void Particle::write_restart() const
375✔
738
{
739
  // Dont write another restart file if in particle restart mode
740
  if (settings::run_mode == RunMode::PARTICLE)
375✔
741
    return;
14✔
742

743
  // Set up file name
744
  auto filename = fmt::format("{}particle_{}_{}.h5", settings::path_output,
745
    simulation::current_batch, id());
361✔
746

747
#pragma omp critical(WriteParticleRestart)
318✔
748
  {
749
    // Create file
750
    hid_t file_id = file_open(filename, 'w');
361✔
751

752
    // Write filetype and version info
753
    write_attribute(file_id, "filetype", "particle restart");
361✔
754
    write_attribute(file_id, "version", VERSION_PARTICLE_RESTART);
361✔
755
    write_attribute(file_id, "openmc_version", VERSION);
361✔
756
#ifdef GIT_SHA1
757
    write_attr_string(file_id, "git_sha1", GIT_SHA1);
361✔
758
#endif
759

760
    // Write data to file
761
    write_dataset(file_id, "current_batch", simulation::current_batch);
361✔
762
    write_dataset(file_id, "generations_per_batch", settings::gen_per_batch);
361✔
763
    write_dataset(file_id, "current_generation", simulation::current_gen);
361✔
764
    write_dataset(file_id, "n_particles", settings::n_particles);
361✔
765
    switch (settings::run_mode) {
361✔
766
    case RunMode::FIXED_SOURCE:
165✔
767
      write_dataset(file_id, "run_mode", "fixed source");
165✔
768
      break;
165✔
769
    case RunMode::EIGENVALUE:
196✔
770
      write_dataset(file_id, "run_mode", "eigenvalue");
196✔
771
      break;
196✔
772
    case RunMode::PARTICLE:
×
773
      write_dataset(file_id, "run_mode", "particle restart");
×
774
      break;
×
775
    default:
×
776
      break;
×
777
    }
778
    write_dataset(file_id, "id", id());
361✔
779
    write_dataset(file_id, "type", static_cast<int>(type()));
361✔
780

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

804
    // Close file
805
    file_close(file_id);
361✔
806
  } // #pragma omp critical
807
}
361✔
808

809
void Particle::update_neutron_xs(
11,822,837,576✔
810
  int i_nuclide, int i_grid, int i_sab, double sab_frac, double ncrystal_xs)
811
{
812
  // Get microscopic cross section cache
813
  auto& micro = this->neutron_xs(i_nuclide);
11,822,837,576✔
814

815
  // If the cache doesn't match, recalculate micro xs
816
  if (this->E() != micro.last_E || this->sqrtkT() != micro.last_sqrtkT ||
16,330,635,284✔
817
      i_sab != micro.index_sab || sab_frac != micro.sab_frac) {
16,330,635,284✔
818
    data::nuclides[i_nuclide]->calculate_xs(i_sab, i_grid, sab_frac, *this);
6,974,192,868✔
819

820
    // If NCrystal is being used, update micro cross section cache
821
    if (ncrystal_xs >= 0.0) {
6,974,192,868✔
822
      data::nuclides[i_nuclide]->calculate_elastic_xs(*this);
1,001,722✔
823
      ncrystal_update_micro(ncrystal_xs, micro);
1,001,722✔
824
    }
825
  }
826
}
11,822,837,576✔
827

828
//==============================================================================
829
// Non-method functions
830
//==============================================================================
831

832
std::string particle_type_to_str(ParticleType type)
3,786,734✔
833
{
834
  switch (type) {
3,786,734✔
835
  case ParticleType::neutron:
2,859,234✔
836
    return "neutron";
2,859,234✔
837
  case ParticleType::photon:
927,220✔
838
    return "photon";
927,220✔
839
  case ParticleType::electron:
140✔
840
    return "electron";
140✔
841
  case ParticleType::positron:
140✔
842
    return "positron";
140✔
843
  }
844
  UNREACHABLE();
×
845
}
846

847
ParticleType str_to_particle_type(std::string str)
4,303,728✔
848
{
849
  if (str == "neutron") {
4,303,728✔
850
    return ParticleType::neutron;
980,315✔
851
  } else if (str == "photon") {
3,323,413✔
852
    return ParticleType::photon;
3,323,337✔
853
  } else if (str == "electron") {
76✔
854
    return ParticleType::electron;
38✔
855
  } else if (str == "positron") {
38✔
856
    return ParticleType::positron;
38✔
857
  } else {
858
    throw std::invalid_argument {fmt::format("Invalid particle name: {}", str)};
×
859
  }
860
}
861

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

© 2025 Coveralls, Inc