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

openmc-dev / openmc / 8596637091

08 Apr 2024 08:04AM UTC coverage: 84.835% (+0.2%) from 84.639%
8596637091

Pull #2693

github

web-flow
Merge 6ad6ea094 into db3b6f3e9
Pull Request #2693: Add reactivity control to coupled transport-depletion analyses

326 of 378 new or added lines in 5 files covered. (86.24%)

471 existing lines in 16 files now uncovered.

48143 of 56749 relevant lines covered (84.83%)

44475032.18 hits per line

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

92.07
/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
2,147,483,647✔
46
{
47
  // Determine mass in eV/c^2
48
  double mass;
49
  switch (this->type()) {
2,147,483,647✔
50
  case ParticleType::neutron:
2,147,483,647✔
51
    mass = MASS_NEUTRON_EV;
2,147,483,647✔
52
    break;
2,147,483,647✔
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
  if (this->E() < 1.0e-9 * mass) {
2,147,483,647✔
63
    // If the energy is much smaller than the mass, revert to non-relativistic
64
    // formula. The 1e-9 criterion is specifically chosen as the point below
65
    // which the error from using the non-relativistic formula is less than the
66
    // round-off eror when using the relativistic formula (see analysis at
67
    // https://gist.github.com/paulromano/da3b473fe3df33de94b265bdff0c7817)
68
    return C_LIGHT * std::sqrt(2 * this->E() / mass);
2,147,483,647✔
69
  } else {
70
    // Calculate inverse of Lorentz factor
71
    const double inv_gamma = mass / (this->E() + mass);
2,147,483,647✔
72

73
    // Calculate speed via v = c * sqrt(1 - γ^-2)
74
    return C_LIGHT * std::sqrt(1 - inv_gamma * inv_gamma);
2,147,483,647✔
75
  }
76
}
77

78
void Particle::move_distance(double length)
14,000✔
79
{
80
  for (int j = 0; j < n_coord(); ++j) {
28,000✔
81
    coord(j).r += length * coord(j).u;
14,000✔
82
  }
83
}
14,000✔
84

85
void Particle::create_secondary(
122,289,481✔
86
  double wgt, Direction u, double E, ParticleType type)
87
{
88
  // If energy is below cutoff for this particle, don't create secondary
89
  // particle
90
  if (E < settings::energy_cutoff[static_cast<int>(type)]) {
122,289,481✔
91
    return;
57,056,370✔
92
  }
93

94
  secondary_bank().emplace_back();
65,233,111✔
95

96
  auto& bank {secondary_bank().back()};
65,233,111✔
97
  bank.particle = type;
65,233,111✔
98
  bank.wgt = wgt;
65,233,111✔
99
  bank.r = r();
65,233,111✔
100
  bank.u = u;
65,233,111✔
101
  bank.E = settings::run_CE ? E : g();
65,233,111✔
102
  bank.time = time();
65,233,111✔
103

104
  n_bank_second() += 1;
65,233,111✔
105
}
106

107
void Particle::from_source(const SourceSite* src)
310,958,388✔
108
{
109
  // Reset some attributes
110
  clear();
310,958,388✔
111
  surface() = 0;
310,958,388✔
112
  cell_born() = C_NONE;
310,958,388✔
113
  material() = C_NONE;
310,958,388✔
114
  n_collision() = 0;
310,958,388✔
115
  fission() = false;
310,958,388✔
116
  zero_flux_derivs();
310,958,388✔
117

118
  // Copy attributes from source bank site
119
  type() = src->particle;
310,958,388✔
120
  wgt() = src->wgt;
310,958,388✔
121
  wgt_last() = src->wgt;
310,958,388✔
122
  r() = src->r;
310,958,388✔
123
  u() = src->u;
310,958,388✔
124
  r_born() = src->r;
310,958,388✔
125
  r_last_current() = src->r;
310,958,388✔
126
  r_last() = src->r;
310,958,388✔
127
  u_last() = src->u;
310,958,388✔
128
  if (settings::run_CE) {
310,958,388✔
129
    E() = src->E;
169,782,388✔
130
    g() = 0;
169,782,388✔
131
  } else {
132
    g() = static_cast<int>(src->E);
141,176,000✔
133
    g_last() = static_cast<int>(src->E);
141,176,000✔
134
    E() = data::mg.energy_bin_avg_[g()];
141,176,000✔
135
  }
136
  E_last() = E();
310,958,388✔
137
  time() = src->time;
310,958,388✔
138
  time_last() = src->time;
310,958,388✔
139
}
310,958,388✔
140

141
void Particle::event_calculate_xs()
2,147,483,647✔
142
{
143
  // Set the random number stream
144
  stream() = STREAM_TRACKING;
2,147,483,647✔
145

146
  // Store pre-collision particle properties
147
  wgt_last() = wgt();
2,147,483,647✔
148
  E_last() = E();
2,147,483,647✔
149
  u_last() = u();
2,147,483,647✔
150
  r_last() = r();
2,147,483,647✔
151
  time_last() = time();
2,147,483,647✔
152

153
  // Reset event variables
154
  event() = TallyEvent::KILL;
2,147,483,647✔
155
  event_nuclide() = NUCLIDE_NONE;
2,147,483,647✔
156
  event_mt() = REACTION_NONE;
2,147,483,647✔
157

158
  // If the cell hasn't been determined based on the particle's location,
159
  // initiate a search for the current cell. This generally happens at the
160
  // beginning of the history and again for any secondary particles
161
  if (lowest_coord().cell == C_NONE) {
2,147,483,647✔
162
    if (!exhaustive_find_cell(*this)) {
310,957,646✔
163
      mark_as_lost(
98✔
164
        "Could not find the cell containing particle " + std::to_string(id()));
196✔
165
      return;
98✔
166
    }
167

168
    // Set birth cell attribute
169
    if (cell_born() == C_NONE)
310,957,548✔
170
      cell_born() = lowest_coord().cell;
310,957,548✔
171
  }
172

173
  // Write particle track.
174
  if (write_track())
2,147,483,647✔
175
    write_particle_track(*this);
13,079✔
176

177
  if (settings::check_overlaps)
2,147,483,647✔
UNCOV
178
    check_cell_overlap(*this);
×
179

180
  // Calculate microscopic and macroscopic cross sections
181
  if (material() != MATERIAL_VOID) {
2,147,483,647✔
182
    if (settings::run_CE) {
2,147,483,647✔
183
      if (material() != material_last() || sqrtkT() != sqrtkT_last()) {
2,147,483,647✔
184
        // If the material is the same as the last material and the
185
        // temperature hasn't changed, we don't need to lookup cross
186
        // sections again.
187
        model::materials[material()]->calculate_xs(*this);
2,147,483,647✔
188
      }
189
    } else {
190
      // Get the MG data; unlike the CE case above, we have to re-calculate
191
      // cross sections for every collision since the cross sections may
192
      // be angle-dependent
193
      data::mg.macro_xs_[material()].calculate_xs(*this);
2,147,483,647✔
194

195
      // Update the particle's group while we know we are multi-group
196
      g_last() = g();
2,147,483,647✔
197
    }
198
  } else {
199
    macro_xs().total = 0.0;
58,457,188✔
200
    macro_xs().absorption = 0.0;
58,457,188✔
201
    macro_xs().fission = 0.0;
58,457,188✔
202
    macro_xs().nu_fission = 0.0;
58,457,188✔
203
  }
204
}
205

206
void Particle::event_advance()
2,147,483,647✔
207
{
208
  // Find the distance to the nearest boundary
209
  boundary() = distance_to_boundary(*this);
2,147,483,647✔
210

211
  // Sample a distance to collision
212
  if (type() == ParticleType::electron || type() == ParticleType::positron) {
2,147,483,647✔
213
    collision_distance() = 0.0;
57,192,954✔
214
  } else if (macro_xs().total == 0.0) {
2,147,483,647✔
215
    collision_distance() = INFINITY;
58,457,188✔
216
  } else {
217
    collision_distance() = -std::log(prn(current_seed())) / macro_xs().total;
2,147,483,647✔
218
  }
219

220
  // Select smaller of the two distances
221
  double distance = std::min(boundary().distance, collision_distance());
2,147,483,647✔
222

223
  // Advance particle in space and time
224
  // Short-term solution until the surface source is revised and we can use
225
  // this->move_distance(distance)
226
  for (int j = 0; j < n_coord(); ++j) {
2,147,483,647✔
227
    coord(j).r += distance * coord(j).u;
2,147,483,647✔
228
  }
229
  this->time() += distance / this->speed();
2,147,483,647✔
230

231
  // Kill particle if its time exceeds the cutoff
232
  bool hit_time_boundary = false;
2,147,483,647✔
233
  double time_cutoff = settings::time_cutoff[static_cast<int>(type())];
2,147,483,647✔
234
  if (time() > time_cutoff) {
2,147,483,647✔
235
    double dt = time() - time_cutoff;
14,000✔
236
    time() = time_cutoff;
14,000✔
237

238
    double push_back_distance = speed() * dt;
14,000✔
239
    this->move_distance(-push_back_distance);
14,000✔
240
    hit_time_boundary = true;
14,000✔
241
  }
242

243
  // Score track-length tallies
244
  if (!model::active_tracklength_tallies.empty()) {
2,147,483,647✔
245
    score_tracklength_tally(*this, distance);
2,147,483,647✔
246
  }
247

248
  // Score track-length estimate of k-eff
249
  if (settings::run_mode == RunMode::EIGENVALUE &&
2,147,483,647✔
250
      type() == ParticleType::neutron) {
2,147,483,647✔
251
    keff_tally_tracklength() += wgt() * distance * macro_xs().nu_fission;
2,147,483,647✔
252
  }
253

254
  // Score flux derivative accumulators for differential tallies.
255
  if (!model::active_tallies.empty()) {
2,147,483,647✔
256
    score_track_derivative(*this, distance);
2,147,483,647✔
257
  }
258

259
  // Set particle weight to zero if it hit the time boundary
260
  if (hit_time_boundary) {
2,147,483,647✔
261
    wgt() = 0.0;
14,000✔
262
  }
263
}
2,147,483,647✔
264

265
void Particle::event_cross_surface()
2,147,483,647✔
266
{
267
  // Set surface that particle is on and adjust coordinate levels
268
  surface() = boundary().surface_index;
2,147,483,647✔
269
  n_coord() = boundary().coord_level;
2,147,483,647✔
270

271
  // Saving previous cell data
272
  for (int j = 0; j < n_coord(); ++j) {
2,147,483,647✔
273
    cell_last(j) = coord(j).cell;
2,147,483,647✔
274
  }
275
  n_coord_last() = n_coord();
2,147,483,647✔
276

277
  if (boundary().lattice_translation[0] != 0 ||
2,147,483,647✔
278
      boundary().lattice_translation[1] != 0 ||
2,147,483,647✔
279
      boundary().lattice_translation[2] != 0) {
2,147,483,647✔
280
    // Particle crosses lattice boundary
281

282
    bool verbose = settings::verbosity >= 10 || trace();
66,436,322✔
283
    cross_lattice(*this, boundary(), verbose);
66,436,322✔
284
    event() = TallyEvent::LATTICE;
66,436,322✔
285
  } else {
286
    // Particle crosses surface
287
    cross_surface();
2,147,483,647✔
288
    if (settings::weight_window_checkpoint_surface) {
2,147,483,647✔
UNCOV
289
      apply_weight_windows(*this);
×
290
    }
291
    event() = TallyEvent::SURFACE;
2,147,483,647✔
292
  }
293
  // Score cell to cell partial currents
294
  if (!model::active_surface_tallies.empty()) {
2,147,483,647✔
295
    score_surface_tally(*this, model::active_surface_tallies);
5,244,164✔
296
  }
297
}
2,147,483,647✔
298

299
void Particle::event_collide()
2,147,483,647✔
300
{
301
  // Score collision estimate of keff
302
  if (settings::run_mode == RunMode::EIGENVALUE &&
2,147,483,647✔
303
      type() == ParticleType::neutron) {
2,147,483,647✔
304
    keff_tally_collision() += wgt() * macro_xs().nu_fission / macro_xs().total;
2,147,483,647✔
305
  }
306

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

311
  if (!model::active_meshsurf_tallies.empty())
2,147,483,647✔
312
    score_surface_tally(*this, model::active_meshsurf_tallies);
142,232,303✔
313

314
  // Clear surface component
315
  surface() = 0;
2,147,483,647✔
316

317
  if (settings::run_CE) {
2,147,483,647✔
318
    collision(*this);
2,147,483,647✔
319
  } else {
320
    collision_mg(*this);
2,147,483,647✔
321
  }
322

323
  // Score collision estimator tallies -- this is done after a collision
324
  // has occurred rather than before because we need information on the
325
  // outgoing energy for any tallies with an outgoing energy filter
326
  if (!model::active_collision_tallies.empty())
2,147,483,647✔
327
    score_collision_tally(*this);
106,305,202✔
328
  if (!model::active_analog_tallies.empty()) {
2,147,483,647✔
329
    if (settings::run_CE) {
173,518,649✔
330
      score_analog_tally_ce(*this);
171,989,835✔
331
    } else {
332
      score_analog_tally_mg(*this);
1,528,814✔
333
    }
334
  }
335

336
  if (!model::active_pulse_height_tallies.empty() &&
2,147,483,647✔
337
      type() == ParticleType::photon) {
21,532✔
338
    pht_collision_energy();
2,576✔
339
  }
340

341
  // Reset banked weight during collision
342
  n_bank() = 0;
2,147,483,647✔
343
  n_bank_second() = 0;
2,147,483,647✔
344
  wgt_bank() = 0.0;
2,147,483,647✔
345
  zero_delayed_bank();
2,147,483,647✔
346

347
  // Reset fission logical
348
  fission() = false;
2,147,483,647✔
349

350
  // Save coordinates for tallying purposes
351
  r_last_current() = r();
2,147,483,647✔
352

353
  // Set last material to none since cross sections will need to be
354
  // re-evaluated
355
  material_last() = C_NONE;
2,147,483,647✔
356

357
  // Set all directions to base level -- right now, after a collision, only
358
  // the base level directions are changed
359
  for (int j = 0; j < n_coord() - 1; ++j) {
2,147,483,647✔
360
    if (coord(j + 1).rotated) {
1,088,562,546✔
361
      // If next level is rotated, apply rotation matrix
362
      const auto& m {model::cells[coord(j).cell]->rotation_};
61,283,420✔
363
      const auto& u {coord(j).u};
61,283,420✔
364
      coord(j + 1).u = u.rotate(m);
61,283,420✔
365
    } else {
366
      // Otherwise, copy this level's direction
367
      coord(j + 1).u = coord(j).u;
1,027,279,126✔
368
    }
369
  }
370

371
  // Score flux derivative accumulators for differential tallies.
372
  if (!model::active_tallies.empty())
2,147,483,647✔
373
    score_collision_derivative(*this);
1,932,682,542✔
374

375
#ifdef DAGMC
376
  history().reset();
397,965,069✔
377
#endif
378
}
2,147,483,647✔
379

380
void Particle::event_revive_from_secondary()
2,147,483,647✔
381
{
382
  // If particle has too many events, display warning and kill it
383
  ++n_event();
2,147,483,647✔
384
  if (n_event() == MAX_EVENTS) {
2,147,483,647✔
UNCOV
385
    warning("Particle " + std::to_string(id()) +
×
386
            " underwent maximum number of events.");
UNCOV
387
    wgt() = 0.0;
×
388
  }
389

390
  // Check for secondary particles if this particle is dead
391
  if (!alive()) {
2,147,483,647✔
392
    // Write final position for this particle
393
    if (write_track()) {
310,957,318✔
394
      write_particle_track(*this);
7,884✔
395
    }
396

397
    // If no secondary particles, break out of event loop
398
    if (secondary_bank().empty())
310,957,318✔
399
      return;
245,520,950✔
400

401
    from_source(&secondary_bank().back());
65,436,368✔
402
    secondary_bank().pop_back();
65,436,368✔
403
    n_event() = 0;
65,436,368✔
404

405
    // Subtract secondary particle energy from interim pulse-height results
406
    if (!model::active_pulse_height_tallies.empty() &&
65,456,094✔
407
        this->type() == ParticleType::photon) {
19,726✔
408
      // Since the birth cell of the particle has not been set we
409
      // have to determine it before the energy of the secondary particle can be
410
      // removed from the pulse-height of this cell.
411
      if (lowest_coord().cell == C_NONE) {
770✔
412
        bool verbose = settings::verbosity >= 10 || trace();
770✔
413
        if (!exhaustive_find_cell(*this, verbose)) {
770✔
414
          mark_as_lost("Could not find the cell containing particle " +
×
415
                       std::to_string(id()));
×
UNCOV
416
          return;
×
417
        }
418
        // Set birth cell attribute
419
        if (cell_born() == C_NONE)
770✔
420
          cell_born() = lowest_coord().cell;
770✔
421
      }
422
      pht_secondary_particles();
770✔
423
    }
424

425
    // Enter new particle in particle track file
426
    if (write_track())
65,436,368✔
427
      add_particle_track(*this);
6,618✔
428
  }
429
}
430

431
void Particle::event_death()
245,522,048✔
432
{
433
#ifdef DAGMC
434
  history().reset();
20,476,532✔
435
#endif
436

437
  // Finish particle track output.
438
  if (write_track()) {
245,522,048✔
439
    finalize_particle_track(*this);
1,266✔
440
  }
441

442
// Contribute tally reduction variables to global accumulator
443
#pragma omp atomic
115,039,555✔
444
  global_tally_absorption += keff_tally_absorption();
245,522,048✔
445
#pragma omp atomic
115,423,362✔
446
  global_tally_collision += keff_tally_collision();
245,522,048✔
447
#pragma omp atomic
114,587,098✔
448
  global_tally_tracklength += keff_tally_tracklength();
245,522,048✔
449
#pragma omp atomic
114,209,091✔
450
  global_tally_leakage += keff_tally_leakage();
245,522,048✔
451

452
  // Reset particle tallies once accumulated
453
  keff_tally_absorption() = 0.0;
245,522,048✔
454
  keff_tally_collision() = 0.0;
245,522,048✔
455
  keff_tally_tracklength() = 0.0;
245,522,048✔
456
  keff_tally_leakage() = 0.0;
245,522,048✔
457

458
  if (!model::active_pulse_height_tallies.empty()) {
245,522,048✔
459
    score_pulse_height_tally(*this, model::active_pulse_height_tallies);
7,000✔
460
  }
461

462
  // Record the number of progeny created by this particle.
463
  // This data will be used to efficiently sort the fission bank.
464
  if (settings::run_mode == RunMode::EIGENVALUE) {
245,522,048✔
465
    int64_t offset = id() - 1 - simulation::work_index[mpi::rank];
231,301,700✔
466
    simulation::progeny_per_particle[offset] = n_progeny();
231,301,700✔
467
  }
468
}
245,522,048✔
469

470
void Particle::pht_collision_energy()
2,576✔
471
{
472
  // Adds the energy particles lose in a collision to the pulse-height
473

474
  // determine index of cell in pulse_height_cells
475
  auto it = std::find(model::pulse_height_cells.begin(),
2,576✔
476
    model::pulse_height_cells.end(), lowest_coord().cell);
2,576✔
477

478
  if (it != model::pulse_height_cells.end()) {
2,576✔
479
    int index = std::distance(model::pulse_height_cells.begin(), it);
2,576✔
480
    pht_storage()[index] += E_last() - E();
2,576✔
481

482
    // If the energy of the particle is below the cutoff, it will not be sampled
483
    // so its energy is added to the pulse-height in the cell
484
    int photon = static_cast<int>(ParticleType::photon);
2,576✔
485
    if (E() < settings::energy_cutoff[photon]) {
2,576✔
486
      pht_storage()[index] += E();
1,050✔
487
    }
488
  }
489
}
2,576✔
490

491
void Particle::pht_secondary_particles()
770✔
492
{
493
  // Removes the energy of secondary produced particles from the pulse-height
494

495
  // determine index of cell in pulse_height_cells
496
  auto it = std::find(model::pulse_height_cells.begin(),
770✔
497
    model::pulse_height_cells.end(), cell_born());
770✔
498

499
  if (it != model::pulse_height_cells.end()) {
770✔
500
    int index = std::distance(model::pulse_height_cells.begin(), it);
770✔
501
    pht_storage()[index] -= E();
770✔
502
  }
503
}
770✔
504

505
void Particle::cross_surface()
2,147,483,647✔
506
{
507
  int i_surface = std::abs(surface());
2,147,483,647✔
508
  // TODO: off-by-one
509
  const auto& surf {model::surfaces[i_surface - 1].get()};
2,147,483,647✔
510
  if (settings::verbosity >= 10 || trace()) {
2,147,483,647✔
511
    write_message(1, "    Crossing surface {}", surf->id_);
28✔
512
  }
513

514
  if (surf->surf_source_ && simulation::current_batch > settings::n_inactive &&
2,147,483,647✔
515
      !simulation::surf_source_bank.full()) {
140,385✔
516
    SourceSite site;
14,200✔
517
    site.r = r();
14,200✔
518
    site.u = u();
14,200✔
519
    site.E = E();
14,200✔
520
    site.time = time();
14,200✔
521
    site.wgt = wgt();
14,200✔
522
    site.delayed_group = delayed_group();
14,200✔
523
    site.surf_id = surf->id_;
14,200✔
524
    site.particle = type();
14,200✔
525
    site.parent_id = id();
14,200✔
526
    site.progeny_id = n_progeny();
14,200✔
527
    int64_t idx = simulation::surf_source_bank.thread_safe_append(site);
14,200✔
528
  }
529

530
// if we're crossing a CSG surface, make sure the DAG history is reset
531
#ifdef DAGMC
532
  if (surf->geom_type_ == GeometryType::CSG)
1,075,865,518✔
533
    history().reset();
1,075,847,747✔
534
#endif
535

536
  // Handle any applicable boundary conditions.
537
  if (surf->bc_ && settings::run_mode != RunMode::PLOTTING) {
2,147,483,647✔
538
    surf->bc_->handle_particle(*this, *surf);
2,147,483,647✔
539
    return;
2,147,483,647✔
540
  }
541

542
  // ==========================================================================
543
  // SEARCH NEIGHBOR LISTS FOR NEXT CELL
544

545
#ifdef DAGMC
546
  // in DAGMC, we know what the next cell should be
547
  if (surf->geom_type_ == GeometryType::DAG) {
788,011,465✔
548
    int32_t i_cell =
549
      next_cell(i_surface, cell_last(n_coord() - 1), lowest_coord().universe) -
14,156✔
550
      1;
14,156✔
551
    // save material and temp
552
    material_last() = material();
14,156✔
553
    sqrtkT_last() = sqrtkT();
14,156✔
554
    // set new cell value
555
    lowest_coord().cell = i_cell;
14,156✔
556
    cell_instance() = 0;
14,156✔
557
    material() = model::cells[i_cell]->material_[0];
14,156✔
558
    sqrtkT() = model::cells[i_cell]->sqrtkT_[0];
14,156✔
559
    return;
14,156✔
560
  }
561
#endif
562

563
  bool verbose = settings::verbosity >= 10 || trace();
2,147,483,647✔
564
  if (neighbor_list_find_cell(*this, verbose))
2,147,483,647✔
565
    return;
2,147,483,647✔
566

567
  // ==========================================================================
568
  // COULDN'T FIND PARTICLE IN NEIGHBORING CELLS, SEARCH ALL CELLS
569

570
  // Remove lower coordinate levels
571
  n_coord() = 1;
35,532✔
572
  bool found = exhaustive_find_cell(*this, verbose);
35,532✔
573

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

580
    surface() = 0;
7,182✔
581
    n_coord() = 1;
7,182✔
582
    r() += TINY_BIT * u();
7,182✔
583

584
    // Couldn't find next cell anywhere! This probably means there is an actual
585
    // undefined region in the geometry.
586

587
    if (!exhaustive_find_cell(*this, verbose)) {
7,182✔
588
      mark_as_lost("After particle " + std::to_string(id()) +
21,546✔
589
                   " crossed surface " + std::to_string(surf->id_) +
28,728✔
590
                   " it could not be located in any cell and it did not leak.");
591
      return;
7,182✔
592
    }
593
  }
594
}
595

596
void Particle::cross_vacuum_bc(const Surface& surf)
38,406,189✔
597
{
598
  // Score any surface current tallies -- note that the particle is moved
599
  // forward slightly so that if the mesh boundary is on the surface, it is
600
  // still processed
601

602
  if (!model::active_meshsurf_tallies.empty()) {
38,406,189✔
603
    // TODO: Find a better solution to score surface currents than
604
    // physically moving the particle forward slightly
605

606
    r() += TINY_BIT * u();
2,037,125✔
607
    score_surface_tally(*this, model::active_meshsurf_tallies);
2,037,125✔
608
  }
609

610
  // Score to global leakage tally
611
  keff_tally_leakage() += wgt();
38,406,189✔
612

613
  // Kill the particle
614
  wgt() = 0.0;
38,406,189✔
615

616
  // Display message
617
  if (settings::verbosity >= 10 || trace()) {
38,406,189✔
618
    write_message(1, "    Leaked out of surface {}", surf.id_);
14✔
619
  }
620
}
38,406,189✔
621

622
void Particle::cross_reflective_bc(const Surface& surf, Direction new_u)
2,147,483,647✔
623
{
624
  // Do not handle reflective boundary conditions on lower universes
625
  if (n_coord() != 1) {
2,147,483,647✔
UNCOV
626
    mark_as_lost("Cannot reflect particle " + std::to_string(id()) +
×
627
                 " off surface in a lower universe.");
UNCOV
628
    return;
×
629
  }
630

631
  // Score surface currents since reflection causes the direction of the
632
  // particle to change. For surface filters, we need to score the tallies
633
  // twice, once before the particle's surface attribute has changed and
634
  // once after. For mesh surface filters, we need to artificially move
635
  // the particle slightly back in case the surface crossing is coincident
636
  // with a mesh boundary
637

638
  if (!model::active_surface_tallies.empty()) {
2,147,483,647✔
639
    score_surface_tally(*this, model::active_surface_tallies);
358,666✔
640
  }
641

642
  if (!model::active_meshsurf_tallies.empty()) {
2,147,483,647✔
643
    Position r {this->r()};
105,391,888✔
644
    this->r() -= TINY_BIT * u();
105,391,888✔
645
    score_surface_tally(*this, model::active_meshsurf_tallies);
105,391,888✔
646
    this->r() = r;
105,391,888✔
647
  }
648

649
  // Set the new particle direction
650
  u() = new_u;
2,147,483,647✔
651

652
  // Reassign particle's cell and surface
653
  coord(0).cell = cell_last(n_coord_last() - 1);
2,147,483,647✔
654
  surface() = -surface();
2,147,483,647✔
655

656
  // If a reflective surface is coincident with a lattice or universe
657
  // boundary, it is necessary to redetermine the particle's coordinates in
658
  // the lower universes.
659
  // (unless we're using a dagmc model, which has exactly one universe)
660
  n_coord() = 1;
2,147,483,647✔
661
  if (surf.geom_type_ != GeometryType::DAG && !neighbor_list_find_cell(*this)) {
2,147,483,647✔
662
    mark_as_lost("Couldn't find particle after reflecting from surface " +
×
663
                 std::to_string(surf.id_) + ".");
×
UNCOV
664
    return;
×
665
  }
666

667
  // Set previous coordinate going slightly past surface crossing
668
  r_last_current() = r() + TINY_BIT * u();
2,147,483,647✔
669

670
  // Diagnostic message
671
  if (settings::verbosity >= 10 || trace()) {
2,147,483,647✔
UNCOV
672
    write_message(1, "    Reflected from surface {}", surf.id_);
×
673
  }
674
}
675

676
void Particle::cross_periodic_bc(
810,012✔
677
  const Surface& surf, Position new_r, Direction new_u, int new_surface)
678
{
679
  // Do not handle periodic boundary conditions on lower universes
680
  if (n_coord() != 1) {
810,012✔
681
    mark_as_lost(
×
UNCOV
682
      "Cannot transfer particle " + std::to_string(id()) +
×
683
      " across surface in a lower universe. Boundary conditions must be "
684
      "applied to root universe.");
UNCOV
685
    return;
×
686
  }
687

688
  // Score surface currents since reflection causes the direction of the
689
  // particle to change -- artificially move the particle slightly back in
690
  // case the surface crossing is coincident with a mesh boundary
691
  if (!model::active_meshsurf_tallies.empty()) {
810,012✔
692
    Position r {this->r()};
×
693
    this->r() -= TINY_BIT * u();
×
694
    score_surface_tally(*this, model::active_meshsurf_tallies);
×
UNCOV
695
    this->r() = r;
×
696
  }
697

698
  // Adjust the particle's location and direction.
699
  r() = new_r;
810,012✔
700
  u() = new_u;
810,012✔
701

702
  // Reassign particle's surface
703
  surface() = new_surface;
810,012✔
704

705
  // Figure out what cell particle is in now
706
  n_coord() = 1;
810,012✔
707

708
  if (!neighbor_list_find_cell(*this)) {
810,012✔
709
    mark_as_lost("Couldn't find particle after hitting periodic "
×
710
                 "boundary on surface " +
×
UNCOV
711
                 std::to_string(surf.id_) +
×
712
                 ". The normal vector "
713
                 "of one periodic surface may need to be reversed.");
UNCOV
714
    return;
×
715
  }
716

717
  // Set previous coordinate going slightly past surface crossing
718
  r_last_current() = r() + TINY_BIT * u();
810,012✔
719

720
  // Diagnostic message
721
  if (settings::verbosity >= 10 || trace()) {
810,012✔
UNCOV
722
    write_message(1, "    Hit periodic boundary on surface {}", surf.id_);
×
723
  }
724
}
725

726
void Particle::mark_as_lost(const char* message)
7,280✔
727
{
728
  // Print warning and write lost particle file
729
  warning(message);
7,280✔
730
  if (settings::max_write_lost_particles < 0 ||
7,280✔
731
      simulation::n_lost_particles < settings::max_write_lost_particles) {
7,000✔
732
    write_restart();
375✔
733
  }
734
  // Increment number of lost particles
735
  wgt() = 0.0;
7,280✔
736
#pragma omp atomic
3,120✔
737
  simulation::n_lost_particles += 1;
4,160✔
738

739
  // Count the total number of simulated particles (on this processor)
740
  auto n = simulation::current_batch * settings::gen_per_batch *
7,280✔
741
           simulation::work_per_rank;
742

743
  // Abort the simulation if the maximum number of lost particles has been
744
  // reached
745
  if (simulation::n_lost_particles >= settings::max_lost_particles &&
7,280✔
746
      simulation::n_lost_particles >= settings::rel_max_lost_particles * n) {
×
UNCOV
747
    fatal_error("Maximum number of lost particles has been reached.");
×
748
  }
749
}
7,280✔
750

751
void Particle::write_restart() const
375✔
752
{
753
  // Dont write another restart file if in particle restart mode
754
  if (settings::run_mode == RunMode::PARTICLE)
375✔
755
    return;
14✔
756

757
  // Set up file name
758
  auto filename = fmt::format("{}particle_{}_{}.h5", settings::path_output,
759
    simulation::current_batch, id());
361✔
760

761
#pragma omp critical(WriteParticleRestart)
318✔
762
  {
763
    // Create file
764
    hid_t file_id = file_open(filename, 'w');
361✔
765

766
    // Write filetype and version info
767
    write_attribute(file_id, "filetype", "particle restart");
361✔
768
    write_attribute(file_id, "version", VERSION_PARTICLE_RESTART);
361✔
769
    write_attribute(file_id, "openmc_version", VERSION);
361✔
770
#ifdef GIT_SHA1
771
    write_attr_string(file_id, "git_sha1", GIT_SHA1);
361✔
772
#endif
773

774
    // Write data to file
775
    write_dataset(file_id, "current_batch", simulation::current_batch);
361✔
776
    write_dataset(file_id, "generations_per_batch", settings::gen_per_batch);
361✔
777
    write_dataset(file_id, "current_generation", simulation::current_gen);
361✔
778
    write_dataset(file_id, "n_particles", settings::n_particles);
361✔
779
    switch (settings::run_mode) {
361✔
780
    case RunMode::FIXED_SOURCE:
165✔
781
      write_dataset(file_id, "run_mode", "fixed source");
165✔
782
      break;
165✔
783
    case RunMode::EIGENVALUE:
196✔
784
      write_dataset(file_id, "run_mode", "eigenvalue");
196✔
785
      break;
196✔
786
    case RunMode::PARTICLE:
×
787
      write_dataset(file_id, "run_mode", "particle restart");
×
788
      break;
×
789
    default:
×
UNCOV
790
      break;
×
791
    }
792
    write_dataset(file_id, "id", id());
361✔
793
    write_dataset(file_id, "type", static_cast<int>(type()));
361✔
794

795
    int64_t i = current_work();
361✔
796
    if (settings::run_mode == RunMode::EIGENVALUE) {
361✔
797
      // take source data from primary bank for eigenvalue simulation
798
      write_dataset(file_id, "weight", simulation::source_bank[i - 1].wgt);
196✔
799
      write_dataset(file_id, "energy", simulation::source_bank[i - 1].E);
196✔
800
      write_dataset(file_id, "xyz", simulation::source_bank[i - 1].r);
196✔
801
      write_dataset(file_id, "uvw", simulation::source_bank[i - 1].u);
196✔
802
      write_dataset(file_id, "time", simulation::source_bank[i - 1].time);
196✔
803
    } else if (settings::run_mode == RunMode::FIXED_SOURCE) {
165✔
804
      // re-sample using rng random number seed used to generate source particle
805
      int64_t id = (simulation::total_gen + overall_generation() - 1) *
165✔
806
                     settings::n_particles +
165✔
807
                   simulation::work_index[mpi::rank] + i;
165✔
808
      uint64_t seed = init_seed(id, STREAM_SOURCE);
165✔
809
      // re-sample source site
810
      auto site = sample_external_source(&seed);
165✔
811
      write_dataset(file_id, "weight", site.wgt);
165✔
812
      write_dataset(file_id, "energy", site.E);
165✔
813
      write_dataset(file_id, "xyz", site.r);
165✔
814
      write_dataset(file_id, "uvw", site.u);
165✔
815
      write_dataset(file_id, "time", site.time);
165✔
816
    }
817

818
    // Close file
819
    file_close(file_id);
361✔
820
  } // #pragma omp critical
821
}
361✔
822

823
void Particle::update_neutron_xs(
2,147,483,647✔
824
  int i_nuclide, int i_grid, int i_sab, double sab_frac, double ncrystal_xs)
825
{
826
  // Get microscopic cross section cache
827
  auto& micro = this->neutron_xs(i_nuclide);
2,147,483,647✔
828

829
  // If the cache doesn't match, recalculate micro xs
830
  if (this->E() != micro.last_E || this->sqrtkT() != micro.last_sqrtkT ||
2,147,483,647✔
831
      i_sab != micro.index_sab || sab_frac != micro.sab_frac) {
2,147,483,647✔
832
    data::nuclides[i_nuclide]->calculate_xs(i_sab, i_grid, sab_frac, *this);
2,147,483,647✔
833

834
    // If NCrystal is being used, update micro cross section cache
835
    if (ncrystal_xs >= 0.0) {
2,147,483,647✔
836
      data::nuclides[i_nuclide]->calculate_elastic_xs(*this);
1,001,722✔
837
      ncrystal_update_micro(ncrystal_xs, micro);
1,001,722✔
838
    }
839
  }
840
}
2,147,483,647✔
841

842
//==============================================================================
843
// Non-method functions
844
//==============================================================================
845

846
std::string particle_type_to_str(ParticleType type)
3,786,734✔
847
{
848
  switch (type) {
3,786,734✔
849
  case ParticleType::neutron:
2,859,234✔
850
    return "neutron";
2,859,234✔
851
  case ParticleType::photon:
927,220✔
852
    return "photon";
927,220✔
853
  case ParticleType::electron:
140✔
854
    return "electron";
140✔
855
  case ParticleType::positron:
140✔
856
    return "positron";
140✔
857
  }
UNCOV
858
  UNREACHABLE();
×
859
}
860

861
ParticleType str_to_particle_type(std::string str)
3,677,934✔
862
{
863
  if (str == "neutron") {
3,677,934✔
864
    return ParticleType::neutron;
804,171✔
865
  } else if (str == "photon") {
2,873,763✔
866
    return ParticleType::photon;
2,873,687✔
867
  } else if (str == "electron") {
76✔
868
    return ParticleType::electron;
38✔
869
  } else if (str == "positron") {
38✔
870
    return ParticleType::positron;
38✔
871
  } else {
UNCOV
872
    throw std::invalid_argument {fmt::format("Invalid particle name: {}", str)};
×
873
  }
874
}
875

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