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

openmc-dev / openmc / 25930342666

15 May 2026 04:56PM UTC coverage: 81.414% (+0.09%) from 81.324%
25930342666

Pull #3734

github

web-flow
Merge 0af48acaf into d56cda254
Pull Request #3734: Specify temperature from a field (structured mesh only)

17919 of 25855 branches covered (69.31%)

Branch coverage included in aggregate %.

275 of 305 new or added lines in 18 files covered. (90.16%)

1321 existing lines in 29 files now uncovered.

58986 of 68607 relevant lines covered (85.98%)

47884537.9 hits per line

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

85.8
/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/collision_track.h"
12
#include "openmc/constants.h"
13
#include "openmc/dagmc.h"
14
#include "openmc/error.h"
15
#include "openmc/geometry.h"
16
#include "openmc/hdf5_interface.h"
17
#include "openmc/lattice.h"
18
#include "openmc/material.h"
19
#include "openmc/message_passing.h"
20
#include "openmc/mgxs_interface.h"
21
#include "openmc/nuclide.h"
22
#include "openmc/particle_data.h"
23
#include "openmc/photon.h"
24
#include "openmc/physics.h"
25
#include "openmc/physics_mg.h"
26
#include "openmc/random_lcg.h"
27
#include "openmc/settings.h"
28
#include "openmc/simulation.h"
29
#include "openmc/source.h"
30
#include "openmc/surface.h"
31
#include "openmc/tallies/derivative.h"
32
#include "openmc/tallies/tally.h"
33
#include "openmc/tallies/tally_scoring.h"
34
#include "openmc/track_output.h"
35
#include "openmc/weight_windows.h"
36

37
#ifdef OPENMC_DAGMC_ENABLED
38
#include "DagMC.hpp"
39
#endif
40

41
namespace openmc {
42

43
//==============================================================================
44
// Particle implementation
45
//==============================================================================
46

47
double Particle::speed() const
2,147,483,647✔
48
{
49
  if (settings::run_CE) {
2,147,483,647✔
50
    // Determine mass in eV/c^2
51
    double mass = this->mass();
2,147,483,647✔
52

53
    // Equivalent to C * sqrt(1-(m/(m+E))^2) without problem at E<<m:
54
    return C_LIGHT * std::sqrt(this->E() * (this->E() + 2 * mass)) /
2,147,483,647✔
55
           (this->E() + mass);
2,147,483,647✔
56
  } else {
57
    auto mat = this->material();
2,063,937,744✔
58
    if (mat == MATERIAL_VOID)
2,063,937,744!
UNCOV
59
      return 1.0 / data::mg.default_inverse_velocity_[this->g()];
×
60
    auto& macro_xs = data::mg.macro_xs_[mat];
2,063,937,744✔
61
    int macro_t = this->mg_xs_cache().t;
2,063,937,744✔
62
    int macro_a = macro_xs.get_angle_index(this->u());
2,063,937,744✔
63
    return 1.0 / macro_xs.get_xs(
2,147,483,647✔
64
                   MgxsType::INVERSE_VELOCITY, this->g(), macro_t, macro_a);
2,063,937,744✔
65
  }
66
}
67

68
double Particle::mass() const
2,147,483,647✔
69
{
70
  switch (type().pdg_number()) {
2,147,483,647✔
71
  case PDG_NEUTRON:
72
    return MASS_NEUTRON_EV;
73
  case PDG_ELECTRON:
56,196,802✔
74
  case PDG_POSITRON:
56,196,802✔
75
    return MASS_ELECTRON_EV;
56,196,802✔
76
  default:
23,223,883✔
77
    return this->type().mass() * AMU_EV;
23,223,883✔
78
  }
79
}
80

81
bool Particle::create_secondary(
116,832,698✔
82
  double wgt, Direction u, double E, ParticleType type)
83
{
84
  // If energy is below cutoff for this particle, don't create secondary
85
  // particle
86
  int idx = type.transport_index();
116,832,698✔
87
  if (idx == C_NONE) {
116,832,698!
88
    return false;
89
  }
90
  if (E < settings::energy_cutoff[idx]) {
116,832,698✔
91
    return false;
92
  }
93

94
  // Increment number of secondaries created (for ParticleProductionFilter)
95
  n_secondaries()++;
61,021,249✔
96

97
  auto& bank = secondary_bank().emplace_back();
61,021,249✔
98
  bank.particle = type;
61,021,249✔
99
  bank.wgt = wgt;
61,021,249✔
100
  bank.r = r();
61,021,249!
101
  bank.u = u;
61,021,249✔
102
  bank.E = settings::run_CE ? E : g();
61,021,249!
103
  bank.time = time();
61,021,249✔
104
  bank_second_E() += bank.E;
61,021,249✔
105
  return true;
61,021,249✔
106
}
107

108
void Particle::split(double wgt)
4,254,511✔
109
{
110
  auto& bank = secondary_bank().emplace_back();
4,254,511✔
111
  bank.particle = type();
4,254,511✔
112
  bank.wgt = wgt;
4,254,511✔
113
  bank.r = r();
4,254,511✔
114
  bank.u = u();
4,254,511✔
115
  bank.E = settings::run_CE ? E() : g();
4,254,511✔
116
  bank.time = time();
4,254,511✔
117

118
  // Convert signed index to a signed surface ID
119
  if (surface() == SURFACE_NONE) {
4,254,511✔
120
    bank.surf_id = SURFACE_NONE;
4,253,097✔
121
  } else {
122
    int surf_id = model::surfaces[surface_index()]->id_;
1,414✔
123
    bank.surf_id = (surface() > 0) ? surf_id : -surf_id;
1,414✔
124
  }
125
}
4,254,511✔
126

127
void Particle::from_source(const SourceSite* src)
252,413,135✔
128
{
129
  // Reset some attributes
130
  clear();
252,413,135✔
131
  surface() = SURFACE_NONE;
252,413,135✔
132
  cell_born() = C_NONE;
252,413,135✔
133
  material() = C_NONE;
252,413,135✔
134
  n_collision() = 0;
252,413,135✔
135
  fission() = false;
252,413,135✔
136
  zero_flux_derivs();
252,413,135✔
137
  lifetime() = 0.0;
252,413,135✔
138
  if (settings::temperature_field_on) {
252,413,135✔
139
    tf_bin() = C_NONE;
264,330✔
140
    tf_bin_next() = C_NONE;
264,330✔
141
  }
142
#ifdef OPENMC_DAGMC_ENABLED
143
  history().reset();
23,108,661✔
144
#endif
145

146
  // Copy attributes from source bank site
147
  type() = src->particle;
252,413,135✔
148
  wgt() = src->wgt;
252,413,135✔
149
  wgt_last() = src->wgt;
252,413,135✔
150
  r() = src->r;
252,413,135✔
151
  u() = src->u;
252,413,135✔
152
  r_born() = src->r;
252,413,135✔
153
  r_last_current() = src->r;
252,413,135✔
154
  r_last() = src->r;
252,413,135✔
155
  u_last() = src->u;
252,413,135✔
156
  if (settings::run_CE) {
252,413,135✔
157
    E() = src->E;
136,322,188✔
158
    g() = 0;
136,322,188✔
159
  } else {
160
    g() = static_cast<int>(src->E);
116,090,947✔
161
    g_last() = static_cast<int>(src->E);
116,090,947✔
162
    E() = data::mg.energy_bin_avg_[g()];
116,090,947✔
163
  }
164
  E_last() = E();
252,413,135✔
165
  time() = src->time;
252,413,135✔
166
  time_last() = src->time;
252,413,135✔
167
  parent_nuclide() = src->parent_nuclide;
252,413,135✔
168
  delayed_group() = src->delayed_group;
252,413,135✔
169

170
  // Convert signed surface ID to signed index
171
  if (src->surf_id != SURFACE_NONE) {
252,413,135✔
172
    int index_plus_one = model::surface_map[std::abs(src->surf_id)] + 1;
111,414✔
173
    surface() = (src->surf_id > 0) ? index_plus_one : -index_plus_one;
111,414✔
174
  }
175
}
252,413,135✔
176

177
void Particle::event_calculate_xs()
2,147,483,647✔
178
{
179
  // Set the random number stream
180
  stream() = STREAM_TRACKING;
2,147,483,647✔
181

182
  // Store pre-collision particle properties
183
  wgt_last() = wgt();
2,147,483,647✔
184
  E_last() = E();
2,147,483,647✔
185
  u_last() = u();
2,147,483,647✔
186
  r_last() = r();
2,147,483,647✔
187
  time_last() = time();
2,147,483,647✔
188

189
  // Reset event variables
190
  event() = TallyEvent::KILL;
2,147,483,647✔
191
  event_nuclide() = NUCLIDE_NONE;
2,147,483,647✔
192
  event_mt() = REACTION_NONE;
2,147,483,647✔
193

194
  // If the cell hasn't been determined based on the particle's location,
195
  // initiate a search for the current cell. This generally happens at the
196
  // beginning of the history and again for any secondary particles
197
  if (lowest_coord().cell() == C_NONE) {
2,147,483,647✔
198

199
    // Define temperature field cell
200
    if (settings::temperature_field_on) {
243,356,984✔
201
      tf_bin() = simulation::temperature_field.get_bin(r());
264,330✔
202
    }
203

204
    if (!exhaustive_find_cell(*this)) {
243,356,984!
UNCOV
205
      mark_as_lost(
×
UNCOV
206
        "Could not find the cell containing particle " + std::to_string(id()));
×
UNCOV
207
      return;
×
208
    }
209

210
    // Set birth cell attribute
211
    if (cell_born() == C_NONE)
243,356,984!
212
      cell_born() = lowest_coord().cell();
243,356,984✔
213

214
    // Initialize last cells from current cell
215
    for (int j = 0; j < n_coord(); ++j) {
504,185,880✔
216
      cell_last(j) = coord(j).cell();
260,828,896✔
217
    }
218
    n_coord_last() = n_coord();
243,356,984✔
219
  }
220

221
  // Write particle track.
222
  if (write_track())
2,147,483,647✔
223
    write_particle_track(*this);
10,309✔
224

225
  if (settings::check_overlaps)
2,147,483,647!
UNCOV
226
    check_cell_overlap(*this);
×
227

228
  // Calculate microscopic and macroscopic cross sections
229
  if (material() != MATERIAL_VOID) {
2,147,483,647✔
230
    if (settings::run_CE) {
2,147,483,647✔
231
      if (material() != material_last() || sqrtkT() != sqrtkT_last() ||
2,147,483,647✔
232
          density_mult() != density_mult_last()) {
903,177,848✔
233
        // If the material is the same as the last material and the
234
        // temperature hasn't changed, we don't need to lookup cross
235
        // sections again.
236
        model::materials[material()]->calculate_xs(*this);
2,147,483,647✔
237
      }
238
    } else {
239
      // Get the MG data; unlike the CE case above, we have to re-calculate
240
      // cross sections for every collision since the cross sections may
241
      // be angle-dependent
242
      data::mg.macro_xs_[material()].calculate_xs(*this);
2,063,937,744✔
243

244
      // Update the particle's group while we know we are multi-group
245
      g_last() = g();
2,063,937,744✔
246
    }
247
  } else {
248
    macro_xs().total = 0.0;
111,926,541✔
249
    macro_xs().absorption = 0.0;
111,926,541✔
250
    macro_xs().fission = 0.0;
111,926,541✔
251
    macro_xs().nu_fission = 0.0;
111,926,541✔
252
  }
253
}
254

255
void Particle::event_advance()
2,147,483,647✔
256
{
257
  // Find the distance to the nearest geometry boundary
258
  boundary() = distance_to_boundary(*this);
2,147,483,647✔
259

260
  // Sample a distance to collision
261
  if (type() == ParticleType::electron() ||
2,147,483,647✔
262
      type() == ParticleType::positron()) {
2,147,483,647✔
263
    collision_distance() = material() == MATERIAL_VOID ? INFINITY : 0.0;
112,393,604!
264
  } else if (macro_xs().total == 0.0) {
2,147,483,647✔
265
    collision_distance() = INFINITY;
111,926,541✔
266
  } else {
267
    collision_distance() = -std::log(prn(current_seed())) / macro_xs().total;
2,147,483,647✔
268
  }
269

270
  // Find the distance to the nearest temperature mesh cell surface
271
  double distance_tmesh = INFTY;
2,147,483,647✔
272
  if (settings::temperature_field_on) {
2,147,483,647✔
273
    distance_tmesh = simulation::temperature_field.distance_to_next_boundary(
5,372,081✔
274
      tf_bin(), r(), u(), tf_bin_next());
5,372,081✔
275
  }
276

277
  // Calculate the distance corresponding to the time cutoff
278
  double speed = this->speed();
2,147,483,647✔
279
  double time_cutoff = settings::time_cutoff[type().transport_index()];
2,147,483,647✔
280
  double distance_cutoff =
2,147,483,647✔
281
    (time_cutoff < INFTY) ? (time_cutoff - time()) * speed : INFTY;
2,147,483,647✔
282

283
  // Determine minimal distance for cross surface events
284
  double distance_cross_surface =
2,147,483,647✔
285
    std::min({boundary().distance(), distance_tmesh});
2,147,483,647✔
286

287
  // Determine minimal distance of all events
288
  double distance =
2,147,483,647✔
289
    std::min({distance_cross_surface, collision_distance(), distance_cutoff});
2,147,483,647✔
290

291
  // Determine next event
292
  next_event().clear();
2,147,483,647✔
293
  if (distance == distance_cutoff) {
2,147,483,647✔
294
    next_event().event_type = EVENT_TIME_CUTOFF;
224,928✔
295
  } else {
296
    if (collision_distance() > distance_cross_surface) {
2,147,483,647✔
297
      next_event().event_type = EVENT_CROSS_SURFACE;
2,147,483,647✔
298
      next_event().cross_surface_geometry =
2,147,483,647✔
299
        (std::abs(distance - boundary().distance()) <= FP_COINCIDENT);
2,147,483,647✔
300
      next_event().cross_surface_temperature_field =
2,147,483,647✔
301
        (std::abs(distance - distance_tmesh) <= FP_COINCIDENT);
2,147,483,647✔
302
    } else {
303
      next_event().event_type = EVENT_COLLIDE;
2,147,483,647✔
304
    }
305
  }
306

307
  // Advance particle in space and time
308
  this->move_distance(distance);
2,147,483,647✔
309
  double dt = distance / speed;
2,147,483,647✔
310
  this->time() += dt;
2,147,483,647✔
311
  this->lifetime() += dt;
2,147,483,647✔
312

313
  // Score timed track-length tallies
314
  if (!model::active_timed_tracklength_tallies.empty()) {
2,147,483,647✔
315
    score_timed_tracklength_tally(*this, distance);
3,628,317✔
316
  }
317

318
  // Score track-length tallies
319
  if (!model::active_tracklength_tallies.empty()) {
2,147,483,647✔
320
    score_tracklength_tally(*this, distance);
2,147,483,647✔
321
  }
322

323
  // Score track-length estimate of k-eff
324
  if (settings::run_mode == RunMode::EIGENVALUE && type().is_neutron()) {
2,147,483,647✔
325
    keff_tally_tracklength() += wgt() * distance * macro_xs().nu_fission;
2,147,483,647✔
326
  }
327

328
  // Score flux derivative accumulators for differential tallies.
329
  if (!model::active_tallies.empty()) {
2,147,483,647✔
330
    score_track_derivative(*this, distance);
2,147,483,647✔
331
  }
332
}
2,147,483,647✔
333

334
void Particle::event_cross_surface()
2,147,483,647✔
335
{
336
  if (next_event().cross_surface_geometry) {
2,147,483,647✔
337

338
    // Saving previous cell data
339
    for (int j = 0; j < n_coord(); ++j) {
2,147,483,647✔
340
      cell_last(j) = coord(j).cell();
2,147,483,647✔
341
    }
342
    n_coord_last() = n_coord();
2,147,483,647✔
343

344
    // Set surface that particle is on and adjust coordinate levels
345
    surface() = boundary().surface();
2,147,483,647✔
346
    n_coord() = boundary().coord_level();
2,147,483,647✔
347

348
    if (boundary().lattice_translation()[0] != 0 ||
2,147,483,647✔
349
        boundary().lattice_translation()[1] != 0 ||
2,147,483,647✔
350
        boundary().lattice_translation()[2] != 0) {
2,147,483,647✔
351
      // Particle crosses lattice boundary
352

353
      bool verbose = settings::verbosity >= 10 || trace();
801,625,082!
354
      cross_lattice(*this, boundary(), verbose); // TODO
801,625,082✔
355
      event() = TallyEvent::LATTICE;
801,625,082✔
356

357
      // Score cell to cell partial currents
358
      if (!model::active_surface_tallies.empty()) {
801,625,082!
NEW
359
        auto& lat {*model::lattices[lowest_coord().lattice()]};
×
NEW
360
        bool is_valid;
×
NEW
361
        Direction normal =
×
NEW
362
          lat.get_normal(boundary().lattice_translation(), is_valid);
×
NEW
363
        if (is_valid) {
×
NEW
364
          normal /= normal.norm();
×
NEW
365
          score_surface_tally(*this, model::active_surface_tallies, normal);
×
366
        }
367
      }
368

369
    } else {
370

371
      const auto& surf {*model::surfaces[surface_index()].get()};
2,147,483,647✔
372

373
      // Particle crosses surface
374
      // If BC, add particle to surface source before crossing surface
375
      if (surf.surf_source_ && surf.bc_) {
2,147,483,647✔
376
        add_surf_source_to_bank(*this, surf);
1,008,506,172✔
377
      }
378
      this->cross_surface(surf);
2,147,483,647✔
379
      // If no BC, add particle to surface source after crossing surface
380
      if (surf.surf_source_ && !surf.bc_) {
2,147,483,647✔
381
        add_surf_source_to_bank(*this, surf);
1,849,784,036✔
382
      }
383
      if (settings::weight_window_checkpoint_surface) {
2,147,483,647✔
384
        apply_weight_windows(*this);
83,008✔
385
      }
386
      event() = TallyEvent::SURFACE;
2,147,483,647✔
387

388
      // Score cell to cell partial currents
389
      if (!model::active_surface_tallies.empty()) {
2,147,483,647✔
390
        Direction normal = surf.normal(r());
34,931,567✔
391
        normal /= normal.norm();
34,931,567✔
392
        score_surface_tally(*this, model::active_surface_tallies, normal);
34,931,567✔
393
      }
394
    }
395

396
    // Update particle temperature from the temperature field
397
  } else if (next_event().cross_surface_temperature_field) {
690,162!
398

399
    sqrtkT_last() = sqrtkT();
690,162✔
400

401
    tf_bin() = tf_bin_next();
690,162✔
402

403
    if (tf_bin() != C_NONE) {
690,162!
404
      sqrtkT() = simulation::temperature_field.get_sqrtkT(tf_bin());
690,162✔
405
    } else {
NEW
406
      int i_cell = lowest_coord().cell();
×
UNCOV
407
      Cell& c {*model::cells[i_cell]};
×
UNCOV
408
      sqrtkT() = c.sqrtkT(cell_instance());
×
409
    }
410

411
#ifdef OPENMC_DAGMC_ENABLED
412
    history().reset();
62,742✔
413
#endif
414
  }
415
}
2,147,483,647✔
416

417
void Particle::event_collide()
2,147,483,647✔
418
{
419

420
  // Score collision estimate of keff
421
  if (settings::run_mode == RunMode::EIGENVALUE && type().is_neutron()) {
2,147,483,647✔
422
    keff_tally_collision() += wgt() * macro_xs().nu_fission / macro_xs().total;
2,147,483,647✔
423
  }
424

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

429
  if (!model::active_meshsurf_tallies.empty())
2,147,483,647✔
430
    score_meshsurface_tally(*this, model::active_meshsurf_tallies);
63,098,926✔
431

432
  // Clear surface component
433
  surface() = SURFACE_NONE;
2,147,483,647✔
434

435
  if (settings::run_CE) {
2,147,483,647✔
436
    collision(*this);
1,272,969,851✔
437
  } else {
438
    collision_mg(*this);
1,783,060,477✔
439
  }
440

441
  // Collision track feature to recording particle interaction
442
  if (settings::collision_track) {
2,147,483,647✔
443
    collision_track_record(*this);
150,087✔
444
  }
445

446
  // Score collision estimator tallies -- this is done after a collision
447
  // has occurred rather than before because we need information on the
448
  // outgoing energy for any tallies with an outgoing energy filter
449
  if (!model::active_collision_tallies.empty())
2,147,483,647✔
450
    score_collision_tally(*this);
107,865,775✔
451
  if (!model::active_analog_tallies.empty()) {
2,147,483,647✔
452
    if (settings::run_CE) {
406,034,945✔
453
      score_analog_tally_ce(*this);
404,826,683✔
454
    } else {
455
      score_analog_tally_mg(*this);
1,208,262✔
456
    }
457
  }
458

459
  if (!model::active_pulse_height_tallies.empty() && type().is_photon()) {
2,147,483,647✔
460
    pht_collision_energy();
2,024✔
461
  }
462

463
  // Reset banked weight during collision
464
  n_bank() = 0;
2,147,483,647✔
465
  bank_second_E() = 0.0;
2,147,483,647✔
466
  wgt_bank() = 0.0;
2,147,483,647✔
467

468
  // Clear number of secondaries in this collision. This is
469
  // distinct from the number of created neutrons n_bank() above!
470
  n_secondaries() = 0;
2,147,483,647✔
471

472
  zero_delayed_bank();
2,147,483,647✔
473

474
  // Reset fission logical
475
  fission() = false;
2,147,483,647✔
476

477
  // Save coordinates for tallying purposes
478
  r_last_current() = r();
2,147,483,647✔
479

480
  // Set last material to none since cross sections will need to be
481
  // re-evaluated
482
  material_last() = C_NONE;
2,147,483,647✔
483

484
  // Set all directions to base level -- right now, after a collision, only
485
  // the base level directions are changed
486
  for (int j = 0; j < n_coord() - 1; ++j) {
2,147,483,647✔
487
    if (coord(j + 1).rotated()) {
286,299,263✔
488
      // If next level is rotated, apply rotation matrix
489
      const auto& m {model::cells[coord(j).cell()]->rotation_};
11,724,229✔
490
      const auto& u {coord(j).u()};
11,724,229✔
491
      coord(j + 1).u() = u.rotate(m);
11,724,229✔
492
    } else {
493
      // Otherwise, copy this level's direction
494
      coord(j + 1).u() = coord(j).u();
274,575,034✔
495
    }
496
  }
497

498
  // Score flux derivative accumulators for differential tallies.
499
  if (!model::active_tallies.empty())
2,147,483,647✔
500
    score_collision_derivative(*this);
1,096,416,506✔
501

502
#ifdef OPENMC_DAGMC_ENABLED
503
  history().reset();
280,177,359✔
504
#endif
505
}
2,147,483,647✔
506

507
void Particle::event_revive_from_secondary()
2,147,483,647✔
508
{
509
  // If particle has too many events, display warning and kill it
510
  ++n_event();
2,147,483,647✔
511
  if (n_event() == settings::max_particle_events) {
2,147,483,647!
UNCOV
512
    warning("Particle " + std::to_string(id()) +
×
513
            " underwent maximum number of events.");
UNCOV
514
    wgt() = 0.0;
×
515
  }
516

517
  // Check for secondary particles if this particle is dead
518
  if (!alive()) {
2,147,483,647✔
519
    // Write final position for this particle
520
    if (write_track()) {
243,356,580✔
521
      write_particle_track(*this);
6,244✔
522
    }
523

524
    // If no secondary particles, break out of event loop
525
    if (secondary_bank().empty())
243,356,580✔
526
      return;
527

528
    from_source(&secondary_bank().back());
65,479,279✔
529
    secondary_bank().pop_back();
65,479,279✔
530
    n_event() = 0;
65,479,279✔
531
    bank_second_E() = 0.0;
65,479,279✔
532

533
    // Subtract secondary particle energy from interim pulse-height results
534
    if (!model::active_pulse_height_tallies.empty() &&
65,479,279✔
535
        this->type().is_photon()) {
15,499✔
536
      // Since the birth cell of the particle has not been set we
537
      // have to determine it before the energy of the secondary particle can be
538
      // removed from the pulse-height of this cell.
539
      if (lowest_coord().cell() == C_NONE) {
605!
540
        bool verbose = settings::verbosity >= 10 || trace();
605!
541

542
        // Define temperature field cell
543
        if (settings::temperature_field_on) {
605!
UNCOV
544
          tf_bin() = simulation::temperature_field.get_bin(r());
×
545
        }
546

547
        if (!exhaustive_find_cell(*this, verbose)) {
605!
UNCOV
548
          mark_as_lost("Could not find the cell containing particle " +
×
UNCOV
549
                       std::to_string(id()));
×
UNCOV
550
          return;
×
551
        }
552
        // Set birth cell attribute
553
        if (cell_born() == C_NONE)
605!
554
          cell_born() = lowest_coord().cell();
605✔
555

556
        // Initialize last cells from current cell
557
        for (int j = 0; j < n_coord(); ++j) {
1,210✔
558
          cell_last(j) = coord(j).cell();
605✔
559
        }
560
        n_coord_last() = n_coord();
605✔
561
      }
562
      pht_secondary_particles();
605✔
563
    }
564

565
    // Enter new particle in particle track file
566
    if (write_track())
65,479,279✔
567
      add_particle_track(*this);
5,234✔
568
  }
569
}
570

571
void Particle::event_death()
177,878,301✔
572
{
573
#ifdef OPENMC_DAGMC_ENABLED
574
  history().reset();
16,247,415✔
575
#endif
576

577
  // Finish particle track output.
578
  if (write_track()) {
177,878,301✔
579
    finalize_particle_track(*this);
1,010✔
580
  }
581

582
// Contribute tally reduction variables to global accumulator
583
#pragma omp atomic
98,267,985✔
584
  global_tally_absorption += keff_tally_absorption();
177,878,301✔
585
#pragma omp atomic
98,260,343✔
586
  global_tally_collision += keff_tally_collision();
177,878,301✔
587
#pragma omp atomic
97,789,617✔
588
  global_tally_tracklength += keff_tally_tracklength();
177,878,301✔
589
#pragma omp atomic
97,344,050✔
590
  global_tally_leakage += keff_tally_leakage();
177,878,301✔
591

592
  // Reset particle tallies once accumulated
593
  keff_tally_absorption() = 0.0;
177,878,301✔
594
  keff_tally_collision() = 0.0;
177,878,301✔
595
  keff_tally_tracklength() = 0.0;
177,878,301✔
596
  keff_tally_leakage() = 0.0;
177,878,301✔
597

598
  if (!model::active_pulse_height_tallies.empty()) {
177,878,301✔
599
    score_pulse_height_tally(*this, model::active_pulse_height_tallies);
5,500✔
600
  }
601

602
  // Record the number of progeny created by this particle.
603
  // This data will be used to efficiently sort the fission bank.
604
  if (settings::run_mode == RunMode::EIGENVALUE) {
177,878,301✔
605
    int64_t offset = id() - 1 - simulation::work_index[mpi::rank];
150,158,700✔
606
    simulation::progeny_per_particle[offset] = n_progeny();
150,158,700✔
607
  }
608
}
177,878,301✔
609

610
void Particle::pht_collision_energy()
2,024✔
611
{
612
  // Adds the energy particles lose in a collision to the pulse-height
613

614
  // determine index of cell in pulse_height_cells
615
  auto it = std::find(model::pulse_height_cells.begin(),
2,024✔
616
    model::pulse_height_cells.end(), lowest_coord().cell());
2,024!
617

618
  if (it != model::pulse_height_cells.end()) {
2,024!
619
    int index = std::distance(model::pulse_height_cells.begin(), it);
2,024✔
620
    pht_storage()[index] += E_last() - E();
2,024✔
621

622
    // If the energy of the particle is below the cutoff, it will not be sampled
623
    // so its energy is added to the pulse-height in the cell
624
    int photon = ParticleType::photon().transport_index();
2,024✔
625
    if (E() < settings::energy_cutoff[photon]) {
2,024✔
626
      pht_storage()[index] += E();
825✔
627
    }
628
  }
629
}
2,024✔
630

631
void Particle::pht_secondary_particles()
605✔
632
{
633
  // Removes the energy of secondary produced particles from the pulse-height
634

635
  // determine index of cell in pulse_height_cells
636
  auto it = std::find(model::pulse_height_cells.begin(),
605✔
637
    model::pulse_height_cells.end(), cell_born());
605!
638

639
  if (it != model::pulse_height_cells.end()) {
605!
640
    int index = std::distance(model::pulse_height_cells.begin(), it);
605✔
641
    pht_storage()[index] -= E();
605✔
642
  }
643
}
605✔
644

645
void Particle::cross_surface(const Surface& surf)
2,147,483,647✔
646
{
647

648
  if (settings::verbosity >= 10 || trace()) {
2,147,483,647✔
649
    write_message(1, "    Crossing surface {}", surf.id_);
66✔
650
  }
651

652
// if we're crossing a CSG surface, make sure the DAG history is reset
653
#ifdef OPENMC_DAGMC_ENABLED
654
  if (surf.geom_type() == GeometryType::CSG)
260,834,875✔
655
    history().reset();
260,777,716✔
656
#endif
657

658
  // Handle any applicable boundary conditions.
659
  if (surf.bc_ && settings::run_mode != RunMode::PLOTTING &&
2,147,483,647!
660
      settings::run_mode != RunMode::VOLUME) {
661
    surf.bc_->handle_particle(*this, surf);
1,008,858,280✔
662
    return;
1,008,858,280✔
663
  }
664

665
  // Update temperature field bin
666
  if (settings::temperature_field_on) {
1,852,617,444✔
667
    if (next_event().cross_surface_temperature_field) {
508,717✔
668
      tf_bin() = tf_bin_next();
63,580✔
669
    }
670
  }
671

672
  // ==========================================================================
673
  // SEARCH NEIGHBOR LISTS FOR NEXT CELL
674

675
#ifdef OPENMC_DAGMC_ENABLED
676
  // in DAGMC, we know what the next cell should be
677
  if (surf.geom_type() == GeometryType::DAG) {
168,690,558✔
678
    int32_t i_cell = next_cell(surface_index(), cell_last(n_coord() - 1),
46,350✔
679
                       lowest_coord().universe()) -
46,350✔
680
                     1;
46,350✔
681
    // save material, temperature, and density multiplier
682
    material_last() = material();
46,350✔
683
    sqrtkT_last() = sqrtkT();
46,350✔
684
    density_mult_last() = density_mult();
46,350✔
685
    // set new cell value
686
    lowest_coord().cell() = i_cell;
46,350✔
687
    auto& cell = model::cells[i_cell];
46,350✔
688

689
    cell_instance() = 0;
46,350✔
690
    if (cell->distribcell_index_ >= 0)
46,350✔
691
      cell_instance() = cell_instance_at_level(*this, n_coord() - 1);
45,326✔
692

693
    material() = cell->material(cell_instance());
46,350!
694
    if (settings::temperature_field_on && tf_bin() != C_NONE) {
46,350!
695
      sqrtkT() = simulation::temperature_field.get_sqrtkT(tf_bin());
696
    } else {
697
      sqrtkT() = cell->sqrtkT(cell_instance());
92,700!
698
    }
699
    density_mult() = cell->density_mult(cell_instance());
46,350✔
700
    return;
46,350✔
701
  }
702
#endif
703

704
  bool verbose = settings::verbosity >= 10 || trace();
1,852,571,094!
705
  if (neighbor_list_find_cell(*this, verbose)) {
1,852,571,094✔
706
    return;
707
  }
708

709
  // ==========================================================================
710
  // COULDN'T FIND PARTICLE IN NEIGHBORING CELLS, SEARCH ALL CELLS
711

712
  // Remove lower coordinate levels
713
  n_coord() = 1;
29,911✔
714
  bool found = exhaustive_find_cell(*this, verbose);
29,911✔
715

716
  if (settings::run_mode != RunMode::PLOTTING && (!found)) {
29,911!
717
    // If a cell is still not found, there are two possible causes: 1) there is
718
    // a void in the model, and 2) the particle hit a surface at a tangent. If
719
    // the particle is really traveling tangent to a surface, if we move it
720
    // forward a tiny bit it should fix the problem.
721

722
    surface() = SURFACE_NONE;
5,799✔
723
    n_coord() = 1;
5,799✔
724
    r() += TINY_BIT * u();
5,799✔
725

726
    // Couldn't find next cell anywhere! This probably means there is an actual
727
    // undefined region in the geometry.
728

729
    if (!exhaustive_find_cell(*this, verbose)) {
5,799!
730
      mark_as_lost("After particle " + std::to_string(id()) +
17,388✔
731
                   " crossed surface " + std::to_string(surf.id_) +
17,388✔
732
                   " it could not be located in any cell and it did not leak.");
733
      return;
5,790✔
734
    }
735
  }
736
}
737

738
void Particle::cross_vacuum_bc(const Surface& surf)
37,288,435✔
739
{
740
  // Score any surface current tallies -- note that the particle is moved
741
  // forward slightly so that if the mesh boundary is on the surface, it is
742
  // still processed
743

744
  if (!model::active_meshsurf_tallies.empty()) {
37,288,435✔
745
    // TODO: Find a better solution to score surface currents than
746
    // physically moving the particle forward slightly
747

748
    r() += TINY_BIT * u();
937,222✔
749
    score_meshsurface_tally(*this, model::active_meshsurf_tallies);
937,222✔
750
  }
751

752
  // Score to global leakage tally
753
  keff_tally_leakage() += wgt();
37,288,435✔
754

755
  // Kill the particle
756
  wgt() = 0.0;
37,288,435✔
757

758
  // Display message
759
  if (settings::verbosity >= 10 || trace()) {
37,288,435!
760
    write_message(1, "    Leaked out of surface {}", surf.id_);
22✔
761
  }
762
}
37,288,435✔
763

764
void Particle::cross_reflective_bc(const Surface& surf, Direction new_u)
969,917,735✔
765
{
766
  // Do not handle reflective boundary conditions on lower universes
767
  if (n_coord() != 1) {
969,917,735!
UNCOV
768
    mark_as_lost("Cannot reflect particle " + std::to_string(id()) +
×
769
                 " off surface in a lower universe.");
UNCOV
770
    return;
×
771
  }
772

773
  // Score surface currents since reflection causes the direction of the
774
  // particle to change. For surface filters, we need to score the tallies
775
  // twice, once before the particle's surface attribute has changed and
776
  // once after. For mesh surface filters, we need to artificially move
777
  // the particle slightly back in case the surface crossing is coincident
778
  // with a mesh boundary
779

780
  if (!model::active_surface_tallies.empty()) {
969,917,735✔
781
    Direction normal = surf.normal(r());
285,021✔
782
    normal /= normal.norm();
285,021✔
783
    score_surface_tally(*this, model::active_surface_tallies, normal);
285,021✔
784
  }
785

786
  if (!model::active_meshsurf_tallies.empty()) {
969,917,735✔
787
    Position r {this->r()};
46,885,487✔
788
    this->r() -= TINY_BIT * u();
46,885,487✔
789
    score_meshsurface_tally(*this, model::active_meshsurf_tallies);
46,885,487✔
790
    this->r() = r;
46,885,487✔
791
  }
792

793
  // Set the new particle direction
794
  u() = new_u;
969,917,735✔
795

796
  // Reassign particle's cell and surface
797
  coord(0).cell() = cell_last(0);
969,917,735✔
798
  surface() = -surface();
969,917,735✔
799

800
  // Particle's temperature field bin is unchanged
801

802
  // If a reflective surface is coincident with a lattice or universe
803
  // boundary, it is necessary to redetermine the particle's coordinates in
804
  // the lower universes.
805
  // (unless we're using a dagmc model, which has exactly one universe)
806
  n_coord() = 1;
969,917,735✔
807
  if (surf.geom_type() != GeometryType::DAG &&
1,939,832,712!
808
      !neighbor_list_find_cell(*this)) {
969,914,977✔
UNCOV
809
    mark_as_lost("Couldn't find particle after reflecting from surface " +
×
UNCOV
810
                 std::to_string(surf.id_) + ".");
×
UNCOV
811
    return;
×
812
  }
813

814
  // Set previous coordinate going slightly past surface crossing
815
  r_last_current() = r() + TINY_BIT * u();
969,917,735✔
816

817
  // Diagnostic message
818
  if (settings::verbosity >= 10 || trace()) {
969,917,735!
UNCOV
819
    write_message(1, "    Reflected from surface {}", surf.id_);
×
820
  }
821
}
822

823
void Particle::cross_periodic_bc(
2,657,576✔
824
  const Surface& surf, Position new_r, Direction new_u, int new_surface)
825
{
826
  // Do not handle periodic boundary conditions on lower universes
827
  if (n_coord() != 1) {
2,657,576!
UNCOV
828
    mark_as_lost(
×
UNCOV
829
      "Cannot transfer particle " + std::to_string(id()) +
×
830
      " across surface in a lower universe. Boundary conditions must be "
831
      "applied to root universe.");
832
    return;
×
833
  }
834

835
  // Score surface currents since reflection causes the direction of the
836
  // particle to change -- artificially move the particle slightly back in
837
  // case the surface crossing is coincident with a mesh boundary
838
  if (!model::active_meshsurf_tallies.empty()) {
2,657,576!
UNCOV
839
    Position r {this->r()};
×
UNCOV
840
    this->r() -= TINY_BIT * u();
×
UNCOV
841
    score_meshsurface_tally(*this, model::active_meshsurf_tallies);
×
UNCOV
842
    this->r() = r;
×
843
  }
844

845
  // Adjust the particle's location and direction.
846
  r() = new_r;
2,657,576✔
847
  u() = new_u;
2,657,576✔
848

849
  // Reassign particle's surface
850
  surface() = new_surface;
2,657,576✔
851

852
  // Reassign particle's temperature field bin
853
  if (settings::temperature_field_on) {
2,657,576✔
854
    tf_bin() = simulation::temperature_field.get_bin(r());
413,006✔
855
  }
856

857
  // Figure out what cell particle is in now
858
  n_coord() = 1;
2,657,576✔
859

860
  if (!neighbor_list_find_cell(*this)) {
2,657,576!
UNCOV
861
    mark_as_lost("Couldn't find particle after hitting periodic "
×
UNCOV
862
                 "boundary on surface " +
×
UNCOV
863
                 std::to_string(surf.id_) + ".");
×
UNCOV
864
    return;
×
865
  }
866

867
  // Set previous coordinate going slightly past surface crossing
868
  r_last_current() = r() + TINY_BIT * u();
2,657,576✔
869

870
  // Diagnostic message
871
  if (settings::verbosity >= 10 || trace()) {
2,657,576!
UNCOV
872
    write_message(1, "    Hit periodic boundary on surface {}", surf.id_);
×
873
  }
874
}
875

876
void Particle::mark_as_lost(const char* message)
5,799✔
877
{
878
  // Print warning and write lost particle file
879
  warning(message);
5,799✔
880
  if (settings::max_write_lost_particles < 0 ||
5,799✔
881
      simulation::n_lost_particles < settings::max_write_lost_particles) {
5,500✔
882
    write_restart();
374✔
883
  }
884
  // Increment number of lost particles
885
  wgt() = 0.0;
5,799✔
886
#pragma omp atomic
3,154✔
887
  simulation::n_lost_particles += 1;
2,645✔
888

889
  // Count the total number of simulated particles (on this processor)
890
  auto n = simulation::current_batch * settings::gen_per_batch *
5,799✔
891
           simulation::work_per_rank;
892

893
  // Abort the simulation if the maximum number of lost particles has been
894
  // reached
895
  if (simulation::n_lost_particles >= settings::max_lost_particles &&
5,799✔
896
      simulation::n_lost_particles >= settings::rel_max_lost_particles * n) {
9!
897
    fatal_error("Maximum number of lost particles has been reached.");
9✔
898
  }
899
}
5,790✔
900

901
void Particle::write_restart() const
374✔
902
{
903
  // Dont write another restart file if in particle restart mode
904
  if (settings::run_mode == RunMode::PARTICLE)
374✔
905
    return;
22✔
906

907
  // Set up file name
908
  auto filename = fmt::format("{}particle_{}_{}.h5", settings::path_output,
352✔
909
    simulation::current_batch, id());
352✔
910

911
#pragma omp critical(WriteParticleRestart)
187✔
912
  {
352✔
913
    // Create file
914
    hid_t file_id = file_open(filename, 'w');
352✔
915

916
    // Write filetype and version info
917
    write_attribute(file_id, "filetype", "particle restart");
352✔
918
    write_attribute(file_id, "version", VERSION_PARTICLE_RESTART);
352✔
919
    write_attribute(file_id, "openmc_version", VERSION);
352✔
920
#ifdef GIT_SHA1
921
    write_attr_string(file_id, "git_sha1", GIT_SHA1);
922
#endif
923

924
    // Write data to file
925
    write_dataset(file_id, "current_batch", simulation::current_batch);
352✔
926
    write_dataset(file_id, "generations_per_batch", settings::gen_per_batch);
352✔
927
    write_dataset(file_id, "current_generation", simulation::current_gen);
352✔
928
    write_dataset(file_id, "n_particles", settings::n_particles);
352✔
929
    switch (settings::run_mode) {
352!
930
    case RunMode::FIXED_SOURCE:
220✔
931
      write_dataset(file_id, "run_mode", "fixed source");
220✔
932
      break;
115✔
933
    case RunMode::EIGENVALUE:
132✔
934
      write_dataset(file_id, "run_mode", "eigenvalue");
132✔
935
      break;
72✔
UNCOV
936
    case RunMode::PARTICLE:
×
UNCOV
937
      write_dataset(file_id, "run_mode", "particle restart");
×
938
      break;
939
    default:
940
      break;
941
    }
942
    write_dataset(file_id, "id", id());
352✔
943
    write_dataset(file_id, "type", type().pdg_number());
352✔
944

945
    int64_t i = current_work();
352✔
946
    if (settings::run_mode == RunMode::EIGENVALUE) {
352✔
947
      // take source data from primary bank for eigenvalue simulation
948
      write_dataset(file_id, "weight", simulation::source_bank[i - 1].wgt);
132✔
949
      write_dataset(file_id, "energy", simulation::source_bank[i - 1].E);
132✔
950
      write_dataset(file_id, "xyz", simulation::source_bank[i - 1].r);
132✔
951
      write_dataset(file_id, "uvw", simulation::source_bank[i - 1].u);
132✔
952
      write_dataset(file_id, "time", simulation::source_bank[i - 1].time);
132✔
953
    } else if (settings::run_mode == RunMode::FIXED_SOURCE) {
220!
954
      // re-sample using rng random number seed used to generate source particle
955
      int64_t id = (simulation::total_gen + overall_generation() - 1) *
220✔
956
                     settings::n_particles +
220✔
957
                   simulation::work_index[mpi::rank] + i;
220✔
958
      uint64_t seed = init_seed(id, STREAM_SOURCE);
220✔
959
      // re-sample source site
960
      auto site = sample_external_source(&seed);
220✔
961
      write_dataset(file_id, "weight", site.wgt);
220✔
962
      write_dataset(file_id, "energy", site.E);
220✔
963
      write_dataset(file_id, "xyz", site.r);
220✔
964
      write_dataset(file_id, "uvw", site.u);
220✔
965
      write_dataset(file_id, "time", site.time);
220✔
966
    }
967

968
    // Close file
969
    file_close(file_id);
352✔
970
  } // #pragma omp critical
971
}
352✔
972

973
void Particle::update_neutron_xs(
2,147,483,647✔
974
  int i_nuclide, int i_grid, int i_sab, double sab_frac, double ncrystal_xs)
975
{
976
  // Get microscopic cross section cache
977
  auto& micro = this->neutron_xs(i_nuclide);
2,147,483,647✔
978

979
  // If the cache doesn't match, recalculate micro xs
980
  if (this->E() != micro.last_E || this->sqrtkT() != micro.last_sqrtkT ||
2,147,483,647✔
981
      i_sab != micro.index_sab || sab_frac != micro.sab_frac ||
2,147,483,647✔
982
      ncrystal_xs != micro.ncrystal_xs) {
2,147,483,647!
983
    data::nuclides[i_nuclide]->calculate_xs(i_sab, i_grid, sab_frac, *this);
2,147,483,647✔
984

985
    // If NCrystal is being used, update micro cross section cache
986
    micro.ncrystal_xs = ncrystal_xs;
2,147,483,647✔
987
    if (ncrystal_xs >= 0.0) {
2,147,483,647✔
988
      data::nuclides[i_nuclide]->calculate_elastic_xs(*this);
11,018,953✔
989
      ncrystal_update_micro(ncrystal_xs, micro);
11,018,953✔
990
    }
991
  }
992
}
2,147,483,647✔
993

994
//==============================================================================
995
// Non-method functions
996
//==============================================================================
997
void add_surf_source_to_bank(Particle& p, const Surface& surf)
2,147,483,647✔
998
{
999
  if (simulation::current_batch <= settings::n_inactive ||
2,147,483,647✔
1000
      simulation::surf_source_bank.full()) {
2,147,483,647✔
1001
    return;
2,147,483,647✔
1002
  }
1003

1004
  // If a cell/cellfrom/cellto parameter is defined
1005
  if (settings::ssw_cell_id != C_NONE) {
337,081✔
1006

1007
    // Retrieve cell index and storage type
1008
    int cell_idx = model::cell_map[settings::ssw_cell_id];
254,436✔
1009

1010
    if (surf.bc_) {
254,436✔
1011
      // Leave if cellto with vacuum boundary condition
1012
      if (surf.bc_->type() == "vacuum" &&
298,918✔
1013
          settings::ssw_cell_type == SSWCellType::To) {
33,099✔
1014
        return;
1015
      }
1016

1017
      // Leave if other boundary condition than vacuum
1018
      if (surf.bc_->type() != "vacuum") {
274,648✔
1019
        return;
1020
      }
1021
    }
1022

1023
    // Check if the cell of interest has been exited
1024
    bool exited = false;
1025
    for (int i = 0; i < p.n_coord_last(); ++i) {
333,671✔
1026
      if (p.cell_last(i) == cell_idx) {
207,730✔
1027
        exited = true;
73,764✔
1028
      }
1029
    }
1030

1031
    // Check if the cell of interest has been entered
1032
    bool entered = false;
1033
    for (int i = 0; i < p.n_coord(); ++i) {
297,973✔
1034
      if (p.coord(i).cell() == cell_idx) {
172,032✔
1035
        entered = true;
57,517✔
1036
      }
1037
    }
1038

1039
    // Vacuum boundary conditions: return if cell is not exited
1040
    if (surf.bc_) {
125,941✔
1041
      if (surf.bc_->type() == "vacuum" && !exited) {
41,928!
1042
        return;
1043
      }
1044
    } else {
1045

1046
      // If we both enter and exit the cell of interest
1047
      if (entered && exited) {
104,977✔
1048
        return;
1049
      }
1050

1051
      // If we did not enter nor exit the cell of interest
1052
      if (!entered && !exited) {
77,774✔
1053
        return;
1054
      }
1055

1056
      // If cellfrom and the cell before crossing is not the cell of
1057
      // interest
1058
      if (settings::ssw_cell_type == SSWCellType::From && !exited) {
64,275✔
1059
        return;
1060
      }
1061

1062
      // If cellto and the cell after crossing is not the cell of interest
1063
      if (settings::ssw_cell_type == SSWCellType::To && !entered) {
52,732✔
1064
        return;
1065
      }
1066
    }
1067
  }
1068

1069
  SourceSite site;
129,653✔
1070
  site.r = p.r();
129,653✔
1071
  site.u = p.u();
129,653✔
1072
  site.E = p.E();
129,653✔
1073
  site.time = p.time();
129,653✔
1074
  site.wgt = p.wgt();
129,653✔
1075
  site.delayed_group = p.delayed_group();
129,653✔
1076
  site.surf_id = surf.id_;
129,653✔
1077
  site.particle = p.type();
129,653✔
1078
  site.parent_id = p.id();
129,653✔
1079
  site.progeny_id = p.n_progeny();
129,653✔
1080
  int64_t idx = simulation::surf_source_bank.thread_safe_append(site);
129,653✔
1081
}
1082

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