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

openmc-dev / openmc / 26125164380

19 May 2026 09:02PM UTC coverage: 81.414% (+0.09%) from 81.324%
26125164380

Pull #3734

github

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

17929 of 25865 branches covered (69.32%)

Branch coverage included in aggregate %.

278 of 308 new or added lines in 18 files covered. (90.26%)

1332 existing lines in 29 files now uncovered.

58995 of 68620 relevant lines covered (85.97%)

47961357.16 hits per line

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

86.04
/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,200,367✔
74
  case PDG_POSITRON:
56,200,367✔
75
    return MASS_ELECTRON_EV;
56,200,367✔
76
  default:
23,222,892✔
77
    return this->type().mass() * AMU_EV;
23,222,892✔
78
  }
79
}
80

81
bool Particle::create_secondary(
116,840,532✔
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,840,532✔
87
  if (idx == C_NONE) {
116,840,532!
88
    return false;
89
  }
90
  if (E < settings::energy_cutoff[idx]) {
116,840,532✔
91
    return false;
92
  }
93

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

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

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

118
  // Convert signed index to a signed surface ID
119
  if (surface() == SURFACE_NONE) {
4,254,453✔
120
    bank.surf_id = SURFACE_NONE;
4,253,039✔
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,453✔
126

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

146
  // Copy attributes from source bank site
147
  type() = src->particle;
252,697,330✔
148
  wgt() = src->wgt;
252,697,330✔
149
  wgt_last() = src->wgt;
252,697,330✔
150
  r() = src->r;
252,697,330✔
151
  u() = src->u;
252,697,330✔
152
  r_born() = src->r;
252,697,330✔
153
  r_last_current() = src->r;
252,697,330✔
154
  r_last() = src->r;
252,697,330✔
155
  u_last() = src->u;
252,697,330✔
156
  if (settings::run_CE) {
252,697,330✔
157
    E() = src->E;
136,606,383✔
158
    g() = 0;
136,606,383✔
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,697,330✔
165
  time() = src->time;
252,697,330✔
166
  time_last() = src->time;
252,697,330✔
167
  parent_nuclide() = src->parent_nuclide;
252,697,330✔
168
  delayed_group() = src->delayed_group;
252,697,330✔
169

170
  // Convert signed surface ID to signed index
171
  if (src->surf_id != SURFACE_NONE) {
252,697,330✔
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,697,330✔
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,641,179✔
201
      tf_bin() = simulation::temperature_field.get_bin(r());
544,577✔
202
    }
203

204
    if (!exhaustive_find_cell(*this)) {
243,641,179!
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,641,179!
212
      cell_born() = lowest_coord().cell();
243,641,179✔
213

214
    // Initialize last cells from current cell
215
    for (int j = 0; j < n_coord(); ++j) {
505,018,501✔
216
      cell_last(j) = coord(j).cell();
261,377,322✔
217
    }
218
    n_coord_last() = n_coord();
243,641,179✔
219
  }
220

221
  // Write particle track.
222
  if (write_track())
2,147,483,647✔
223
    write_particle_track(*this);
10,305✔
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()) {
904,882,328✔
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,501✔
249
    macro_xs().absorption = 0.0;
111,926,501✔
250
    macro_xs().fission = 0.0;
111,926,501✔
251
    macro_xs().nu_fission = 0.0;
111,926,501✔
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,400,734!
264
  } else if (macro_xs().total == 0.0) {
2,147,483,647✔
265
    collision_distance() = INFINITY;
111,926,501✔
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(
11,669,832✔
274
      tf_bin(), r(), u(), tf_bin_next());
11,669,832✔
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
      // Update temperature field bin
354
      if (settings::temperature_field_on) {
803,527,030✔
355
        if (next_event().cross_surface_temperature_field) {
1,901,988✔
356
          tf_bin() = tf_bin_next();
748,792✔
357
        }
358
      }
359

360
      bool verbose = settings::verbosity >= 10 || trace();
803,527,030!
361
      cross_lattice(*this, boundary(), verbose);
803,527,030✔
362
      event() = TallyEvent::LATTICE;
803,527,030✔
363

364
      // Score cell to cell partial currents
365
      if (!model::active_surface_tallies.empty()) {
803,527,030!
NEW
366
        auto& lat {*model::lattices[lowest_coord().lattice()]};
×
NEW
367
        bool is_valid;
×
NEW
368
        Direction normal =
×
NEW
369
          lat.get_normal(boundary().lattice_translation(), is_valid);
×
NEW
370
        if (is_valid) {
×
NEW
371
          normal /= normal.norm();
×
NEW
372
          score_surface_tally(*this, model::active_surface_tallies, normal);
×
373
        }
374
      }
375

376
    } else {
377

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

380
      // Particle crosses surface
381
      // If BC, add particle to surface source before crossing surface
382
      if (surf.surf_source_ && surf.bc_) {
2,147,483,647✔
383
        add_surf_source_to_bank(*this, surf);
1,009,444,934✔
384
      }
385
      this->cross_surface(surf);
2,147,483,647✔
386
      // If no BC, add particle to surface source after crossing surface
387
      if (surf.surf_source_ && !surf.bc_) {
2,147,483,647✔
388
        add_surf_source_to_bank(*this, surf);
1,849,797,963✔
389
      }
390
      if (settings::weight_window_checkpoint_surface) {
2,147,483,647✔
391
        apply_weight_windows(*this);
83,008✔
392
      }
393
      event() = TallyEvent::SURFACE;
2,147,483,647✔
394

395
      // Score cell to cell partial currents
396
      if (!model::active_surface_tallies.empty()) {
2,147,483,647✔
397
        Direction normal = surf.normal(r());
34,931,567✔
398
        normal /= normal.norm();
34,931,567✔
399
        score_surface_tally(*this, model::active_surface_tallies, normal);
34,931,567✔
400
      }
401
    }
402

403
    // Update particle temperature from the temperature field
404
  } else if (next_event().cross_surface_temperature_field) {
723,867!
405

406
    sqrtkT_last() = sqrtkT();
723,867✔
407

408
    tf_bin() = tf_bin_next();
723,867✔
409

410
    if (tf_bin() != C_NONE) {
723,867!
411
      sqrtkT() = simulation::temperature_field.get_sqrtkT(tf_bin());
723,867✔
412
    } else {
NEW
413
      int i_cell = lowest_coord().cell();
×
UNCOV
414
      Cell& c {*model::cells[i_cell]};
×
UNCOV
415
      sqrtkT() = c.sqrtkT(cell_instance());
×
416
    }
417

418
#ifdef OPENMC_DAGMC_ENABLED
419
    history().reset();
96,447✔
420
#endif
421
  }
422
}
2,147,483,647✔
423

424
void Particle::event_collide()
2,147,483,647✔
425
{
426

427
  // Score collision estimate of keff
428
  if (settings::run_mode == RunMode::EIGENVALUE && type().is_neutron()) {
2,147,483,647✔
429
    keff_tally_collision() += wgt() * macro_xs().nu_fission / macro_xs().total;
2,147,483,647✔
430
  }
431

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

436
  if (!model::active_meshsurf_tallies.empty())
2,147,483,647✔
437
    score_meshsurface_tally(*this, model::active_meshsurf_tallies);
63,098,926✔
438

439
  // Clear surface component
440
  surface() = SURFACE_NONE;
2,147,483,647✔
441

442
  if (settings::run_CE) {
2,147,483,647✔
443
    collision(*this);
1,277,237,173✔
444
  } else {
445
    collision_mg(*this);
1,783,060,477✔
446
  }
447

448
  // Collision track feature to recording particle interaction
449
  if (settings::collision_track) {
2,147,483,647✔
450
    collision_track_record(*this);
150,087✔
451
  }
452

453
  // Score collision estimator tallies -- this is done after a collision
454
  // has occurred rather than before because we need information on the
455
  // outgoing energy for any tallies with an outgoing energy filter
456
  if (!model::active_collision_tallies.empty())
2,147,483,647✔
457
    score_collision_tally(*this);
107,868,349✔
458
  if (!model::active_analog_tallies.empty()) {
2,147,483,647✔
459
    if (settings::run_CE) {
406,034,945✔
460
      score_analog_tally_ce(*this);
404,826,683✔
461
    } else {
462
      score_analog_tally_mg(*this);
1,208,262✔
463
    }
464
  }
465

466
  if (!model::active_pulse_height_tallies.empty() && type().is_photon()) {
2,147,483,647✔
467
    pht_collision_energy();
2,024✔
468
  }
469

470
  // Reset banked weight during collision
471
  n_bank() = 0;
2,147,483,647✔
472
  bank_second_E() = 0.0;
2,147,483,647✔
473
  wgt_bank() = 0.0;
2,147,483,647✔
474

475
  // Clear number of secondaries in this collision. This is
476
  // distinct from the number of created neutrons n_bank() above!
477
  n_secondaries() = 0;
2,147,483,647✔
478

479
  zero_delayed_bank();
2,147,483,647✔
480

481
  // Reset fission logical
482
  fission() = false;
2,147,483,647✔
483

484
  // Save coordinates for tallying purposes
485
  r_last_current() = r();
2,147,483,647✔
486

487
  // Set last material to none since cross sections will need to be
488
  // re-evaluated
489
  material_last() = C_NONE;
2,147,483,647✔
490

491
  // Set all directions to base level -- right now, after a collision, only
492
  // the base level directions are changed
493
  for (int j = 0; j < n_coord() - 1; ++j) {
2,147,483,647✔
494
    if (coord(j + 1).rotated()) {
289,516,672✔
495
      // If next level is rotated, apply rotation matrix
496
      const auto& m {model::cells[coord(j).cell()]->rotation_};
11,724,229✔
497
      const auto& u {coord(j).u()};
11,724,229✔
498
      coord(j + 1).u() = u.rotate(m);
11,724,229✔
499
    } else {
500
      // Otherwise, copy this level's direction
501
      coord(j + 1).u() = coord(j).u();
277,792,443✔
502
    }
503
  }
504

505
  // Score flux derivative accumulators for differential tallies.
506
  if (!model::active_tallies.empty())
2,147,483,647✔
507
    score_collision_derivative(*this);
1,100,683,021✔
508

509
#ifdef OPENMC_DAGMC_ENABLED
510
  history().reset();
280,361,702✔
511
#endif
512
}
2,147,483,647✔
513

514
void Particle::event_revive_from_secondary()
2,147,483,647✔
515
{
516
  // If particle has too many events, display warning and kill it
517
  ++n_event();
2,147,483,647✔
518
  if (n_event() == settings::max_particle_events) {
2,147,483,647!
UNCOV
519
    warning("Particle " + std::to_string(id()) +
×
520
            " underwent maximum number of events.");
UNCOV
521
    wgt() = 0.0;
×
522
  }
523

524
  // Check for secondary particles if this particle is dead
525
  if (!alive()) {
2,147,483,647✔
526
    // Write final position for this particle
527
    if (write_track()) {
243,640,775✔
528
      write_particle_track(*this);
6,242✔
529
    }
530

531
    // If no secondary particles, break out of event loop
532
    if (secondary_bank().empty())
243,640,775✔
533
      return;
534

535
    from_source(&secondary_bank().back());
65,483,474✔
536
    secondary_bank().pop_back();
65,483,474✔
537
    n_event() = 0;
65,483,474✔
538
    bank_second_E() = 0.0;
65,483,474✔
539

540
    // Subtract secondary particle energy from interim pulse-height results
541
    if (!model::active_pulse_height_tallies.empty() &&
65,483,474✔
542
        this->type().is_photon()) {
15,499✔
543
      // Since the birth cell of the particle has not been set we
544
      // have to determine it before the energy of the secondary particle can be
545
      // removed from the pulse-height of this cell.
546
      if (lowest_coord().cell() == C_NONE) {
605!
547
        bool verbose = settings::verbosity >= 10 || trace();
605!
548

549
        // Define temperature field cell
550
        if (settings::temperature_field_on) {
605!
UNCOV
551
          tf_bin() = simulation::temperature_field.get_bin(r());
×
552
        }
553

554
        if (!exhaustive_find_cell(*this, verbose)) {
605!
UNCOV
555
          mark_as_lost("Could not find the cell containing particle " +
×
UNCOV
556
                       std::to_string(id()));
×
UNCOV
557
          return;
×
558
        }
559
        // Set birth cell attribute
560
        if (cell_born() == C_NONE)
605!
561
          cell_born() = lowest_coord().cell();
605✔
562

563
        // Initialize last cells from current cell
564
        for (int j = 0; j < n_coord(); ++j) {
1,210✔
565
          cell_last(j) = coord(j).cell();
605✔
566
        }
567
        n_coord_last() = n_coord();
605✔
568
      }
569
      pht_secondary_particles();
605✔
570
    }
571

572
    // Enter new particle in particle track file
573
    if (write_track())
65,483,474✔
574
      add_particle_track(*this);
5,232✔
575
  }
576
}
577

578
void Particle::event_death()
178,158,301✔
579
{
580
#ifdef OPENMC_DAGMC_ENABLED
581
  history().reset();
16,287,415✔
582
#endif
583

584
  // Finish particle track output.
585
  if (write_track()) {
178,158,301✔
586
    finalize_particle_track(*this);
1,010✔
587
  }
588

589
// Contribute tally reduction variables to global accumulator
590
#pragma omp atomic
98,429,259✔
591
  global_tally_absorption += keff_tally_absorption();
178,158,301✔
592
#pragma omp atomic
98,168,673✔
593
  global_tally_collision += keff_tally_collision();
178,158,301✔
594
#pragma omp atomic
98,072,013✔
595
  global_tally_tracklength += keff_tally_tracklength();
178,158,301✔
596
#pragma omp atomic
97,558,056✔
597
  global_tally_leakage += keff_tally_leakage();
178,158,301✔
598

599
  // Reset particle tallies once accumulated
600
  keff_tally_absorption() = 0.0;
178,158,301✔
601
  keff_tally_collision() = 0.0;
178,158,301✔
602
  keff_tally_tracklength() = 0.0;
178,158,301✔
603
  keff_tally_leakage() = 0.0;
178,158,301✔
604

605
  if (!model::active_pulse_height_tallies.empty()) {
178,158,301✔
606
    score_pulse_height_tally(*this, model::active_pulse_height_tallies);
5,500✔
607
  }
608

609
  // Record the number of progeny created by this particle.
610
  // This data will be used to efficiently sort the fission bank.
611
  if (settings::run_mode == RunMode::EIGENVALUE) {
178,158,301✔
612
    int64_t offset = id() - 1 - simulation::work_index[mpi::rank];
150,438,700✔
613
    simulation::progeny_per_particle[offset] = n_progeny();
150,438,700✔
614
  }
615
}
178,158,301✔
616

617
void Particle::pht_collision_energy()
2,024✔
618
{
619
  // Adds the energy particles lose in a collision to the pulse-height
620

621
  // determine index of cell in pulse_height_cells
622
  auto it = std::find(model::pulse_height_cells.begin(),
2,024✔
623
    model::pulse_height_cells.end(), lowest_coord().cell());
2,024!
624

625
  if (it != model::pulse_height_cells.end()) {
2,024!
626
    int index = std::distance(model::pulse_height_cells.begin(), it);
2,024✔
627
    pht_storage()[index] += E_last() - E();
2,024✔
628

629
    // If the energy of the particle is below the cutoff, it will not be sampled
630
    // so its energy is added to the pulse-height in the cell
631
    int photon = ParticleType::photon().transport_index();
2,024✔
632
    if (E() < settings::energy_cutoff[photon]) {
2,024✔
633
      pht_storage()[index] += E();
825✔
634
    }
635
  }
636
}
2,024✔
637

638
void Particle::pht_secondary_particles()
605✔
639
{
640
  // Removes the energy of secondary produced particles from the pulse-height
641

642
  // determine index of cell in pulse_height_cells
643
  auto it = std::find(model::pulse_height_cells.begin(),
605✔
644
    model::pulse_height_cells.end(), cell_born());
605!
645

646
  if (it != model::pulse_height_cells.end()) {
605!
647
    int index = std::distance(model::pulse_height_cells.begin(), it);
605✔
648
    pht_storage()[index] -= E();
605✔
649
  }
650
}
605✔
651

652
void Particle::cross_surface(const Surface& surf)
2,147,483,647✔
653
{
654

655
  if (settings::verbosity >= 10 || trace()) {
2,147,483,647✔
656
    write_message(1, "    Crossing surface {}", surf.id_);
66✔
657
  }
658

659
// if we're crossing a CSG surface, make sure the DAG history is reset
660
#ifdef OPENMC_DAGMC_ENABLED
661
  if (surf.geom_type() == GeometryType::CSG)
260,990,864✔
662
    history().reset();
260,885,536✔
663
#endif
664

665
  // Handle any applicable boundary conditions.
666
  if (surf.bc_ && settings::run_mode != RunMode::PLOTTING &&
2,147,483,647!
667
      settings::run_mode != RunMode::VOLUME) {
668
    surf.bc_->handle_particle(*this, surf);
1,009,797,042✔
669
    return;
1,009,797,042✔
670
  }
671

672
  // Update temperature field bin after handling boundary conditions
673
  if (settings::temperature_field_on) {
1,852,621,267✔
674
    if (next_event().cross_surface_temperature_field) {
512,344✔
675
      tf_bin() = tf_bin_next();
67,207✔
676
    }
677
  }
678

679
  // ==========================================================================
680
  // SEARCH NEIGHBOR LISTS FOR NEXT CELL
681

682
#ifdef OPENMC_DAGMC_ENABLED
683
  // in DAGMC, we know what the next cell should be
684
  if (surf.geom_type() == GeometryType::DAG) {
168,711,910✔
685
    int32_t i_cell = next_cell(surface_index(), cell_last(n_coord() - 1),
49,977✔
686
                       lowest_coord().universe()) -
49,977✔
687
                     1;
49,977✔
688
    // save material, temperature, and density multiplier
689
    material_last() = material();
49,977✔
690
    sqrtkT_last() = sqrtkT();
49,977✔
691
    density_mult_last() = density_mult();
49,977✔
692
    // set new cell value
693
    lowest_coord().cell() = i_cell;
49,977✔
694
    auto& cell = model::cells[i_cell];
49,977✔
695

696
    cell_instance() = 0;
49,977✔
697
    if (cell->distribcell_index_ >= 0)
49,977✔
698
      cell_instance() = cell_instance_at_level(*this, n_coord() - 1);
48,953✔
699

700
    material() = cell->material(cell_instance());
49,977!
701
    if (settings::temperature_field_on && tf_bin() != C_NONE) {
49,977✔
702
      sqrtkT() = simulation::temperature_field.get_sqrtkT(tf_bin());
1,693✔
703
    } else {
704
      sqrtkT() = cell->sqrtkT(cell_instance());
96,568!
705
    }
706
    density_mult() = cell->density_mult(cell_instance());
49,977✔
707
    return;
49,977✔
708
  }
709
#endif
710

711
  bool verbose = settings::verbosity >= 10 || trace();
1,852,571,290!
712
  if (neighbor_list_find_cell(*this, verbose)) {
1,852,571,290✔
713
    return;
714
  }
715

716
  // ==========================================================================
717
  // COULDN'T FIND PARTICLE IN NEIGHBORING CELLS, SEARCH ALL CELLS
718

719
  // Remove lower coordinate levels
720
  n_coord() = 1;
29,911✔
721
  bool found = exhaustive_find_cell(*this, verbose);
29,911✔
722

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

729
    surface() = SURFACE_NONE;
5,799✔
730
    n_coord() = 1;
5,799✔
731
    r() += TINY_BIT * u();
5,799✔
732

733
    // Couldn't find next cell anywhere! This probably means there is an actual
734
    // undefined region in the geometry.
735

736
    if (!exhaustive_find_cell(*this, verbose)) {
5,799!
737
      mark_as_lost("After particle " + std::to_string(id()) +
17,388✔
738
                   " crossed surface " + std::to_string(surf.id_) +
17,388✔
739
                   " it could not be located in any cell and it did not leak.");
740
      return;
5,790✔
741
    }
742
  }
743
}
744

745
void Particle::cross_vacuum_bc(const Surface& surf)
37,364,396✔
746
{
747
  // Score any surface current tallies -- note that the particle is moved
748
  // forward slightly so that if the mesh boundary is on the surface, it is
749
  // still processed
750

751
  if (!model::active_meshsurf_tallies.empty()) {
37,364,396✔
752
    // TODO: Find a better solution to score surface currents than
753
    // physically moving the particle forward slightly
754

755
    r() += TINY_BIT * u();
937,222✔
756
    score_meshsurface_tally(*this, model::active_meshsurf_tallies);
937,222✔
757
  }
758

759
  // Score to global leakage tally
760
  keff_tally_leakage() += wgt();
37,364,396✔
761

762
  // Kill the particle
763
  wgt() = 0.0;
37,364,396✔
764

765
  // Display message
766
  if (settings::verbosity >= 10 || trace()) {
37,364,396!
767
    write_message(1, "    Leaked out of surface {}", surf.id_);
22✔
768
  }
769
}
37,364,396✔
770

771
void Particle::cross_reflective_bc(const Surface& surf, Direction new_u)
970,372,468✔
772
{
773
  // Do not handle reflective boundary conditions on lower universes
774
  if (n_coord() != 1) {
970,372,468!
UNCOV
775
    mark_as_lost("Cannot reflect particle " + std::to_string(id()) +
×
776
                 " off surface in a lower universe.");
UNCOV
777
    return;
×
778
  }
779

780
  // Score surface currents since reflection causes the direction of the
781
  // particle to change. For surface filters, we need to score the tallies
782
  // twice, once before the particle's surface attribute has changed and
783
  // once after. For mesh surface filters, we need to artificially move
784
  // the particle slightly back in case the surface crossing is coincident
785
  // with a mesh boundary
786

787
  if (!model::active_surface_tallies.empty()) {
970,372,468✔
788
    Direction normal = surf.normal(r());
285,021✔
789
    normal /= normal.norm();
285,021✔
790
    score_surface_tally(*this, model::active_surface_tallies, normal);
285,021✔
791
  }
792

793
  if (!model::active_meshsurf_tallies.empty()) {
970,372,468✔
794
    Position r {this->r()};
46,885,487✔
795
    this->r() -= TINY_BIT * u();
46,885,487✔
796
    score_meshsurface_tally(*this, model::active_meshsurf_tallies);
46,885,487✔
797
    this->r() = r;
46,885,487✔
798
  }
799

800
  // Set the new particle direction
801
  u() = new_u;
970,372,468✔
802

803
  // Reassign particle's cell and surface
804
  coord(0).cell() = cell_last(0);
970,372,468✔
805
  surface() = -surface();
970,372,468✔
806

807
  // Particle's temperature field bin is unchanged
808

809
  // If a reflective surface is coincident with a lattice or universe
810
  // boundary, it is necessary to redetermine the particle's coordinates in
811
  // the lower universes.
812
  // (unless we're using a dagmc model, which has exactly one universe)
813
  n_coord() = 1;
970,372,468✔
814
  if (surf.geom_type() != GeometryType::DAG &&
1,940,704,615!
815
      !neighbor_list_find_cell(*this)) {
970,332,147✔
UNCOV
816
    mark_as_lost("Couldn't find particle after reflecting from surface " +
×
UNCOV
817
                 std::to_string(surf.id_) + ".");
×
UNCOV
818
    return;
×
819
  }
820

821
  // Set previous coordinate going slightly past surface crossing
822
  r_last_current() = r() + TINY_BIT * u();
970,372,468✔
823

824
  // Diagnostic message
825
  if (settings::verbosity >= 10 || trace()) {
970,372,468!
UNCOV
826
    write_message(1, "    Reflected from surface {}", surf.id_);
×
827
  }
828
}
829

830
void Particle::cross_periodic_bc(
3,065,644✔
831
  const Surface& surf, Position new_r, Direction new_u, int new_surface)
832
{
833
  // Do not handle periodic boundary conditions on lower universes
834
  if (n_coord() != 1) {
3,065,644!
UNCOV
835
    mark_as_lost(
×
UNCOV
836
      "Cannot transfer particle " + std::to_string(id()) +
×
837
      " across surface in a lower universe. Boundary conditions must be "
838
      "applied to root universe.");
839
    return;
×
840
  }
841

842
  // Score surface currents since reflection causes the direction of the
843
  // particle to change -- artificially move the particle slightly back in
844
  // case the surface crossing is coincident with a mesh boundary
845
  if (!model::active_meshsurf_tallies.empty()) {
3,065,644!
UNCOV
846
    Position r {this->r()};
×
UNCOV
847
    this->r() -= TINY_BIT * u();
×
UNCOV
848
    score_meshsurface_tally(*this, model::active_meshsurf_tallies);
×
UNCOV
849
    this->r() = r;
×
850
  }
851

852
  // Adjust the particle's location and direction.
853
  r() = new_r;
3,065,644✔
854
  u() = new_u;
3,065,644✔
855

856
  // Reassign particle's surface
857
  surface() = new_surface;
3,065,644✔
858

859
  // Reassign particle's temperature field bin
860
  if (settings::temperature_field_on) {
3,065,644✔
861
    tf_bin() = simulation::temperature_field.get_bin(r());
821,502✔
862
  }
863

864
  // Figure out what cell particle is in now
865
  n_coord() = 1;
3,065,644✔
866

867
  if (!neighbor_list_find_cell(*this)) {
3,065,644!
UNCOV
868
    mark_as_lost("Couldn't find particle after hitting periodic "
×
UNCOV
869
                 "boundary on surface " +
×
UNCOV
870
                 std::to_string(surf.id_) + ".");
×
UNCOV
871
    return;
×
872
  }
873

874
  // Set previous coordinate going slightly past surface crossing
875
  r_last_current() = r() + TINY_BIT * u();
3,065,644✔
876

877
  // Diagnostic message
878
  if (settings::verbosity >= 10 || trace()) {
3,065,644!
UNCOV
879
    write_message(1, "    Hit periodic boundary on surface {}", surf.id_);
×
880
  }
881
}
882

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

896
  // Count the total number of simulated particles (on this processor)
897
  auto n = simulation::current_batch * settings::gen_per_batch *
5,799✔
898
           simulation::work_per_rank;
899

900
  // Abort the simulation if the maximum number of lost particles has been
901
  // reached
902
  if (simulation::n_lost_particles >= settings::max_lost_particles &&
5,799✔
903
      simulation::n_lost_particles >= settings::rel_max_lost_particles * n) {
9!
904
    fatal_error("Maximum number of lost particles has been reached.");
9✔
905
  }
906
}
5,790✔
907

908
void Particle::write_restart() const
374✔
909
{
910
  // Dont write another restart file if in particle restart mode
911
  if (settings::run_mode == RunMode::PARTICLE)
374✔
912
    return;
22✔
913

914
  // Set up file name
915
  auto filename = fmt::format("{}particle_{}_{}.h5", settings::path_output,
352✔
916
    simulation::current_batch, id());
352✔
917

918
#pragma omp critical(WriteParticleRestart)
187✔
919
  {
352✔
920
    // Create file
921
    hid_t file_id = file_open(filename, 'w');
352✔
922

923
    // Write filetype and version info
924
    write_attribute(file_id, "filetype", "particle restart");
352✔
925
    write_attribute(file_id, "version", VERSION_PARTICLE_RESTART);
352✔
926
    write_attribute(file_id, "openmc_version", VERSION);
352✔
927
#ifdef GIT_SHA1
928
    write_attr_string(file_id, "git_sha1", GIT_SHA1);
929
#endif
930

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

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

975
    // Close file
976
    file_close(file_id);
352✔
977
  } // #pragma omp critical
978
}
352✔
979

980
void Particle::update_neutron_xs(
2,147,483,647✔
981
  int i_nuclide, int i_grid, int i_sab, double sab_frac, double ncrystal_xs)
982
{
983
  // Get microscopic cross section cache
984
  auto& micro = this->neutron_xs(i_nuclide);
2,147,483,647✔
985

986
  // If the cache doesn't match, recalculate micro xs
987
  if (this->E() != micro.last_E || this->sqrtkT() != micro.last_sqrtkT ||
2,147,483,647✔
988
      i_sab != micro.index_sab || sab_frac != micro.sab_frac ||
2,147,483,647✔
989
      ncrystal_xs != micro.ncrystal_xs) {
2,147,483,647!
990
    data::nuclides[i_nuclide]->calculate_xs(i_sab, i_grid, sab_frac, *this);
2,147,483,647✔
991

992
    // If NCrystal is being used, update micro cross section cache
993
    micro.ncrystal_xs = ncrystal_xs;
2,147,483,647✔
994
    if (ncrystal_xs >= 0.0) {
2,147,483,647✔
995
      data::nuclides[i_nuclide]->calculate_elastic_xs(*this);
11,018,953✔
996
      ncrystal_update_micro(ncrystal_xs, micro);
11,018,953✔
997
    }
998
  }
999
}
2,147,483,647✔
1000

1001
//==============================================================================
1002
// Non-method functions
1003
//==============================================================================
1004
void add_surf_source_to_bank(Particle& p, const Surface& surf)
2,147,483,647✔
1005
{
1006
  if (simulation::current_batch <= settings::n_inactive ||
2,147,483,647✔
1007
      simulation::surf_source_bank.full()) {
2,147,483,647✔
1008
    return;
2,147,483,647✔
1009
  }
1010

1011
  // If a cell/cellfrom/cellto parameter is defined
1012
  if (settings::ssw_cell_id != C_NONE) {
337,078✔
1013

1014
    // Retrieve cell index and storage type
1015
    int cell_idx = model::cell_map[settings::ssw_cell_id];
254,433✔
1016

1017
    if (surf.bc_) {
254,433✔
1018
      // Leave if cellto with vacuum boundary condition
1019
      if (surf.bc_->type() == "vacuum" &&
298,916✔
1020
          settings::ssw_cell_type == SSWCellType::To) {
33,098✔
1021
        return;
1022
      }
1023

1024
      // Leave if other boundary condition than vacuum
1025
      if (surf.bc_->type() != "vacuum") {
274,646✔
1026
        return;
1027
      }
1028
    }
1029

1030
    // Check if the cell of interest has been exited
1031
    bool exited = false;
1032
    for (int i = 0; i < p.n_coord_last(); ++i) {
333,665✔
1033
      if (p.cell_last(i) == cell_idx) {
207,727✔
1034
        exited = true;
73,763✔
1035
      }
1036
    }
1037

1038
    // Check if the cell of interest has been entered
1039
    bool entered = false;
1040
    for (int i = 0; i < p.n_coord(); ++i) {
297,967✔
1041
      if (p.coord(i).cell() == cell_idx) {
172,029✔
1042
        entered = true;
57,516✔
1043
      }
1044
    }
1045

1046
    // Vacuum boundary conditions: return if cell is not exited
1047
    if (surf.bc_) {
125,938✔
1048
      if (surf.bc_->type() == "vacuum" && !exited) {
41,926!
1049
        return;
1050
      }
1051
    } else {
1052

1053
      // If we both enter and exit the cell of interest
1054
      if (entered && exited) {
104,975✔
1055
        return;
1056
      }
1057

1058
      // If we did not enter nor exit the cell of interest
1059
      if (!entered && !exited) {
77,772✔
1060
        return;
1061
      }
1062

1063
      // If cellfrom and the cell before crossing is not the cell of
1064
      // interest
1065
      if (settings::ssw_cell_type == SSWCellType::From && !exited) {
64,273✔
1066
        return;
1067
      }
1068

1069
      // If cellto and the cell after crossing is not the cell of interest
1070
      if (settings::ssw_cell_type == SSWCellType::To && !entered) {
52,731✔
1071
        return;
1072
      }
1073
    }
1074
  }
1075

1076
  SourceSite site;
129,653✔
1077
  site.r = p.r();
129,653✔
1078
  site.u = p.u();
129,653✔
1079
  site.E = p.E();
129,653✔
1080
  site.time = p.time();
129,653✔
1081
  site.wgt = p.wgt();
129,653✔
1082
  site.delayed_group = p.delayed_group();
129,653✔
1083
  site.surf_id = surf.id_;
129,653✔
1084
  site.particle = p.type();
129,653✔
1085
  site.parent_id = p.id();
129,653✔
1086
  site.progeny_id = p.n_progeny();
129,653✔
1087
  int64_t idx = simulation::surf_source_bank.thread_safe_append(site);
129,653✔
1088
}
1089

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