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

openmc-dev / openmc / 27319341588

11 Jun 2026 02:16AM UTC coverage: 81.244% (-0.04%) from 81.281%
27319341588

Pull #3969

github

web-flow
Merge 25a68b6ea into 02eb999af
Pull Request #3969: Overlap detection for plotter

18177 of 26406 branches covered (68.84%)

Branch coverage included in aggregate %.

73 of 86 new or added lines in 5 files covered. (84.88%)

2 existing lines in 2 files now uncovered.

59310 of 68970 relevant lines covered (85.99%)

48357958.5 hits per line

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

82.72
/src/geometry.cpp
1
#include "openmc/geometry.h"
2

3
#include <fmt/core.h>
4
#include <fmt/ostream.h>
5

6
#include "openmc/array.h"
7
#include "openmc/cell.h"
8
#include "openmc/constants.h"
9
#include "openmc/error.h"
10
#include "openmc/lattice.h"
11
#include "openmc/settings.h"
12
#include "openmc/simulation.h"
13
#include "openmc/string_utils.h"
14
#include "openmc/surface.h"
15

16
namespace openmc {
17

18
//==============================================================================
19
// Global variables
20
//==============================================================================
21

22
namespace model {
23

24
int root_universe {-1};
25
int n_coord_levels;
26

27
vector<int64_t> overlap_check_count;
28

29
} // namespace model
30

31
//==============================================================================
32
// Non-member functions
33
//==============================================================================
34

35
OverlapResult check_cell_overlap(GeometryState& p, bool error)
1,389,300✔
36
{
37
  OverlapResult overlaps;
1,389,300✔
38
  int n_coord = p.n_coord();
1,389,300✔
39

40
  // Loop through each coordinate level
41
  for (int j = 0; j < n_coord; j++) {
2,778,600✔
42
    Universe& univ = *model::universes[p.coord(j).universe()];
1,389,300✔
43

44
    // Loop through each cell on this level
45
    for (auto index_cell : univ.cells_) {
5,120,500✔
46
      Cell& c = *model::cells[index_cell];
3,731,200✔
47
      if (c.contains(p.coord(j).r(), p.coord(j).u(), p.surface())) {
3,731,200✔
48
        if (index_cell != p.coord(j).cell()) {
1,323,234✔
49
          if (error) {
381,260!
50
            fatal_error(
×
51
              fmt::format("Overlapping cells detected: {}, {} on universe {}",
×
52
                c.id_, model::cells[p.coord(j).cell()]->id_, univ.id_));
×
53
          }
54

55
          // With no fatal error (plotter is calling), now adds overlaps and
56
          // calls them; ensures order does not matter when making overlap key
57
          int cell_a = model::cells[index_cell]->id_;
381,260!
58
          int cell_b = model::cells[p.coord(j).cell()]->id_;
381,260!
59
          int a = std::min(cell_a, cell_b);
381,260!
60
          int b = std::max(cell_a, cell_b);
381,260!
61
          OverlapKey key {univ.id_, a, b};
381,260✔
62

63
          // in case of duplicates
64
          if (std::find(overlaps.pairs.begin(), overlaps.pairs.end(), key) ==
381,260✔
65
              overlaps.pairs.end()) {
381,260!
66
            overlaps.pairs.push_back(key);
381,260✔
67
          }
68
        }
69
#pragma omp atomic
721,764✔
70
        ++model::overlap_check_count[index_cell];
1,323,234✔
71
      }
72
    }
73
  }
74

75
  return overlaps;
1,389,300✔
UNCOV
76
}
×
77

78
//==============================================================================
79

80
int cell_instance_at_level(const GeometryState& p, int level)
2,147,483,647✔
81
{
82
  // throw error if the requested level is too deep for the geometry
83
  if (level > model::n_coord_levels) {
2,147,483,647!
84
    fatal_error(fmt::format("Cell instance at level {} requested, but only {} "
×
85
                            "levels exist in the geometry.",
86
      level, p.n_coord()));
87
  }
88

89
  // determine the cell instance
90
  Cell& c {*model::cells[p.coord(level).cell()]};
2,147,483,647!
91

92
  // quick exit if this cell doesn't have distribcell instances
93
  if (c.distribcell_index_ == C_NONE)
2,147,483,647!
94
    return C_NONE;
95

96
  // compute the cell's instance
97
  int instance = 0;
98
  for (int i = 0; i < level; i++) {
2,147,483,647✔
99
    const auto& c_i {*model::cells[p.coord(i).cell()]};
2,147,483,647✔
100
    if (c_i.type_ == Fill::UNIVERSE) {
2,147,483,647✔
101
      instance += c_i.offset_[c.distribcell_index_];
1,890,585,059✔
102
    } else if (c_i.type_ == Fill::LATTICE) {
1,299,445,065!
103
      instance += c_i.offset_[c.distribcell_index_];
1,299,445,065✔
104
      auto& lat {*model::lattices[p.coord(i + 1).lattice()]};
1,299,445,065✔
105
      const auto& i_xyz {p.coord(i + 1).lattice_index()};
1,299,445,065✔
106
      if (lat.are_valid_indices(i_xyz)) {
1,299,445,065✔
107
        instance += lat.offset(c.distribcell_index_, i_xyz);
1,294,561,098✔
108
      }
109
    }
110
  }
111
  return instance;
112
}
113

114
//==============================================================================
115

116
bool find_cell_inner(
2,147,483,647✔
117
  GeometryState& p, const NeighborList* neighbor_list, bool verbose)
118
{
119
  // Find which cell of this universe the particle is in.  Use the neighbor list
120
  // to shorten the search if one was provided.
121
  bool found = false;
2,147,483,647✔
122
  int32_t i_cell = C_NONE;
2,147,483,647✔
123
  if (neighbor_list) {
2,147,483,647✔
124
    for (auto it = neighbor_list->cbegin(); it != neighbor_list->cend(); ++it) {
2,147,483,647✔
125
      i_cell = *it;
2,147,483,647!
126

127
      // Make sure the search cell is in the same universe.
128
      int i_universe = p.lowest_coord().universe();
2,147,483,647!
129
      if (model::cells[i_cell]->universe_ != i_universe)
2,147,483,647!
130
        continue;
×
131

132
      // Check if this cell contains the particle.
133
      Position r {p.r_local()};
2,147,483,647✔
134
      Direction u {p.u_local()};
2,147,483,647✔
135
      auto surf = p.surface();
2,147,483,647✔
136
      if (model::cells[i_cell]->contains(r, u, surf)) {
2,147,483,647✔
137
        p.lowest_coord().cell() = i_cell;
2,147,483,647✔
138
        found = true;
2,147,483,647✔
139
        break;
2,147,483,647✔
140
      }
141
    }
142

143
    // If we're attempting a neighbor list search and fail, we
144
    // now know we should return false. This will trigger an
145
    // exhaustive search from neighbor_list_find_cell and make
146
    // the result from that be appended to the neighbor list.
147
    if (!found) {
2,147,483,647✔
148
      return found;
149
    }
150
  }
151

152
  // Check successively lower coordinate levels until finding material fill
153
  for (;; ++p.n_coord()) {
2,147,483,647✔
154
    // If we did not attempt to use neighbor lists, i_cell is still C_NONE.  In
155
    // that case, we should now do an exhaustive search to find the right value
156
    // of i_cell.
157
    //
158
    // Alternatively, neighbor list searches could have succeeded, but we found
159
    // that the fill of the neighbor cell was another universe. As such, in the
160
    // code below this conditional, we set i_cell back to C_NONE to indicate
161
    // that.
162
    if (i_cell == C_NONE) {
2,147,483,647✔
163
      int i_universe = p.lowest_coord().universe();
2,147,483,647✔
164
      const auto& univ {model::universes[i_universe]};
2,147,483,647✔
165
      found = univ->find_cell(p);
2,147,483,647✔
166
    }
167

168
    if (!found) {
2,147,483,647✔
169
      return found;
170
    }
171
    i_cell = p.lowest_coord().cell();
2,147,483,647!
172

173
    // Announce the cell that the particle is entering.
174
    if (found && verbose) {
2,147,483,647!
175
      auto msg = fmt::format("    Entering cell {}", model::cells[i_cell]->id_);
×
176
      write_message(msg, 1);
×
177
    }
×
178

179
    Cell& c {*model::cells[i_cell]};
2,147,483,647✔
180
    if (c.type_ == Fill::MATERIAL) {
2,147,483,647✔
181
      // Found a material cell which means this is the lowest coord level.
182

183
      p.cell_instance() = 0;
2,147,483,647✔
184
      // Find the distribcell instance number.
185
      if (c.distribcell_index_ >= 0) {
2,147,483,647✔
186
        p.cell_instance() = cell_instance_at_level(p, p.n_coord() - 1);
2,147,483,647✔
187
      }
188

189
      // Set the material, temperature and density multiplier.
190
      p.material_last() = p.material();
2,147,483,647✔
191
      p.material() = c.material(p.cell_instance());
2,147,483,647✔
192
      p.sqrtkT_last() = p.sqrtkT();
2,147,483,647✔
193
      p.sqrtkT() = c.sqrtkT(p.cell_instance());
2,147,483,647✔
194
      p.density_mult_last() = p.density_mult();
2,147,483,647✔
195
      p.density_mult() = c.density_mult(p.cell_instance());
2,147,483,647✔
196

197
      return true;
2,147,483,647✔
198

199
    } else if (c.type_ == Fill::UNIVERSE) {
1,153,950,791✔
200
      //========================================================================
201
      //! Found a lower universe, update this coord level then search the next.
202

203
      // Set the lower coordinate level universe.
204
      auto& coord {p.coord(p.n_coord())};
1,001,796,492✔
205
      coord.universe() = c.fill_;
1,001,796,492✔
206

207
      // Set the position and direction.
208
      coord.r() = p.r_local();
1,001,796,492✔
209
      coord.u() = p.u_local();
1,001,796,492✔
210

211
      // Apply translation.
212
      coord.r() -= c.translation_;
1,001,796,492✔
213

214
      // Apply rotation.
215
      if (!c.rotation_.empty()) {
1,001,796,492✔
216
        coord.rotate(c.rotation_);
66,157,597✔
217
      }
218

219
    } else if (c.type_ == Fill::LATTICE) {
152,154,299!
220
      //========================================================================
221
      //! Found a lower lattice, update this coord level then search the next.
222

223
      Lattice& lat {*model::lattices[c.fill_]};
152,154,299✔
224

225
      // Set the position and direction.
226
      auto& coord {p.coord(p.n_coord())};
152,154,299✔
227
      coord.r() = p.r_local();
152,154,299✔
228
      coord.u() = p.u_local();
152,154,299✔
229

230
      // Apply translation.
231
      coord.r() -= c.translation_;
152,154,299✔
232

233
      // Apply rotation.
234
      if (!c.rotation_.empty()) {
152,154,299✔
235
        coord.rotate(c.rotation_);
358,336✔
236
      }
237

238
      // Determine lattice indices.
239
      auto& i_xyz {coord.lattice_index()};
152,154,299✔
240
      lat.get_indices(coord.r(), coord.u(), i_xyz);
152,154,299✔
241

242
      // Get local position in appropriate lattice cell
243
      coord.r() = lat.get_local_position(coord.r(), i_xyz);
152,154,299✔
244

245
      // Set lattice indices.
246
      coord.lattice() = c.fill_;
152,154,299✔
247

248
      // Set the lower coordinate level universe.
249
      if (lat.are_valid_indices(i_xyz)) {
152,154,299✔
250
        coord.universe() = lat[i_xyz];
147,270,332✔
251
      } else {
252
        if (lat.outer_ != NO_OUTER_UNIVERSE) {
4,883,967!
253
          coord.universe() = lat.outer_;
4,883,967✔
254
        } else {
255
          p.mark_as_lost(fmt::format(
×
256
            "Particle {} left lattice {}, but it has no outer definition.",
257
            p.id(), lat.id_));
×
258
        }
259
      }
260
    }
261
    i_cell = C_NONE; // trip non-neighbor cell search at next iteration
1,153,950,791✔
262
    found = false;
1,153,950,791✔
263
  }
264

265
  return found;
266
}
267

268
//==============================================================================
269

270
bool neighbor_list_find_cell(GeometryState& p, bool verbose)
2,147,483,647✔
271
{
272

273
  // Reset all the deeper coordinate levels.
274
  for (int i = p.n_coord(); i < model::n_coord_levels; i++) {
2,147,483,647✔
275
    p.coord(i).reset();
1,923,569,055✔
276
  }
277

278
  // Get the cell this particle was in previously.
279
  auto coord_lvl = p.n_coord() - 1;
2,147,483,647✔
280
  auto i_cell = p.coord(coord_lvl).cell();
2,147,483,647✔
281
  Cell& c {*model::cells[i_cell]};
2,147,483,647✔
282

283
  // Search for the particle in that cell's neighbor list.  Return if we
284
  // found the particle.
285
  bool found = find_cell_inner(p, &c.neighbors_, verbose);
2,147,483,647✔
286
  if (found)
2,147,483,647✔
287
    return found;
288

289
  // The particle could not be found in the neighbor list.  Try searching all
290
  // cells in this universe, and update the neighbor list if we find a new
291
  // neighboring cell.
292
  found = find_cell_inner(p, nullptr, verbose);
1,602,114✔
293
  if (found)
1,602,114✔
294
    c.neighbors_.push_back(p.coord(coord_lvl).cell());
30,377✔
295
  return found;
296
}
297

298
bool exhaustive_find_cell(GeometryState& p, bool verbose)
1,364,725,499✔
299
{
300
  int i_universe = p.lowest_coord().universe();
1,364,725,499✔
301
  if (i_universe == C_NONE) {
1,364,725,499✔
302
    p.coord(0).universe() = model::root_universe;
343,490,053✔
303
    p.n_coord() = 1;
343,490,053✔
304
    i_universe = model::root_universe;
343,490,053✔
305
  }
306
  // Reset all the deeper coordinate levels.
307
  for (int i = p.n_coord(); i < model::n_coord_levels; i++) {
1,452,554,727✔
308
    p.coord(i).reset();
87,829,228✔
309
  }
310
  return find_cell_inner(p, nullptr, verbose);
1,364,725,499✔
311
}
312

313
//==============================================================================
314

315
void cross_lattice(GeometryState& p, const BoundaryInfo& boundary, bool verbose)
801,265,642✔
316
{
317
  auto& coord {p.lowest_coord()};
801,265,642!
318
  auto& lat {*model::lattices[coord.lattice()]};
801,265,642!
319

320
  if (verbose) {
801,265,642!
321
    write_message(
×
322
      fmt::format("    Crossing lattice {}. Current position ({},{},{}). r={}",
×
323
        lat.id_, coord.lattice_index()[0], coord.lattice_index()[1],
×
324
        coord.lattice_index()[2], p.r()),
×
325
      1);
326
  }
327

328
  // Set the lattice indices.
329
  coord.lattice_index()[0] += boundary.lattice_translation()[0];
801,265,642✔
330
  coord.lattice_index()[1] += boundary.lattice_translation()[1];
801,265,642✔
331
  coord.lattice_index()[2] += boundary.lattice_translation()[2];
801,265,642✔
332

333
  // Set the new coordinate position.
334
  const auto& upper_coord {p.coord(p.n_coord() - 2)};
801,265,642✔
335
  const auto& cell {model::cells[upper_coord.cell()]};
801,265,642✔
336
  Position r = upper_coord.r();
801,265,642✔
337
  r -= cell->translation_;
801,265,642✔
338
  if (!cell->rotation_.empty()) {
801,265,642✔
339
    r = r.rotate(cell->rotation_);
471,647✔
340
  }
341
  p.r_local() = lat.get_local_position(r, coord.lattice_index());
801,265,642✔
342

343
  if (!lat.are_valid_indices(coord.lattice_index())) {
801,265,642✔
344
    // The particle is outside the lattice.  Search for it from the base coords.
345
    p.n_coord() = 1;
3,714,425✔
346
    bool found = exhaustive_find_cell(p);
3,714,425✔
347

348
    if (!found) {
3,714,425!
349
      p.mark_as_lost(fmt::format("Particle {} could not be located after "
×
350
                                 "crossing a boundary of lattice {}",
351
        p.id(), lat.id_));
×
352
    }
353

354
  } else {
355
    // Find cell in next lattice element.
356
    p.lowest_coord().universe() = lat[coord.lattice_index()];
797,551,217✔
357
    bool found = exhaustive_find_cell(p);
797,551,217✔
358

359
    if (!found) {
797,551,217!
360
      // A particle crossing the corner of a lattice tile may not be found.  In
361
      // this case, search for it from the base coords.
362
      p.n_coord() = 1;
×
363
      bool found = exhaustive_find_cell(p);
×
364
      if (!found) {
×
365
        p.mark_as_lost(fmt::format("Particle {} could not be located after "
×
366
                                   "crossing a boundary of lattice {}",
367
          p.id(), lat.id_));
×
368
      }
369
    }
370
  }
371
}
801,265,642✔
372

373
//==============================================================================
374

375
BoundaryInfo distance_to_boundary(GeometryState& p)
2,147,483,647✔
376
{
377
  BoundaryInfo info;
2,147,483,647✔
378
  double d_lat = INFINITY;
2,147,483,647✔
379
  double d_surf = INFINITY;
2,147,483,647✔
380
  int32_t level_surf_cross;
2,147,483,647✔
381
  array<int, 3> level_lat_trans {};
2,147,483,647✔
382

383
  // Loop over each coordinate level.
384
  for (int i = 0; i < p.n_coord(); i++) {
2,147,483,647✔
385
    const auto& coord {p.coord(i)};
2,147,483,647✔
386
    const Position& r {coord.r()};
2,147,483,647✔
387
    const Direction& u {coord.u()};
2,147,483,647✔
388
    Cell& c {*model::cells[coord.cell()]};
2,147,483,647✔
389

390
    // Find the oncoming surface in this cell and the distance to it.
391
    auto surface_distance = c.distance(r, u, p.surface(), &p);
2,147,483,647✔
392
    d_surf = surface_distance.first;
2,147,483,647✔
393
    level_surf_cross = surface_distance.second;
2,147,483,647✔
394

395
    // Find the distance to the next lattice tile crossing.
396
    if (coord.lattice() != C_NONE) {
2,147,483,647✔
397
      auto& lat {*model::lattices[coord.lattice()]};
1,381,476,825!
398
      // TODO: refactor so both lattice use the same position argument (which
399
      // also means the lat.type attribute can be removed)
400
      std::pair<double, array<int, 3>> lattice_distance;
1,381,476,825✔
401
      switch (lat.type_) {
1,381,476,825!
402
      case LatticeType::rect:
1,295,973,528✔
403
        lattice_distance = lat.distance(r, u, coord.lattice_index());
1,295,973,528✔
404
        break;
1,295,973,528✔
405
      case LatticeType::hex:
85,503,297✔
406
        auto& cell_above {model::cells[p.coord(i - 1).cell()]};
85,503,297✔
407
        Position r_hex {p.coord(i - 1).r()};
85,503,297✔
408
        r_hex -= cell_above->translation_;
85,503,297✔
409
        if (coord.rotated()) {
85,503,297✔
410
          r_hex = r_hex.rotate(cell_above->rotation_);
701,954✔
411
        }
412
        r_hex.z = coord.r().z;
85,503,297✔
413
        lattice_distance = lat.distance(r_hex, u, coord.lattice_index());
85,503,297✔
414
        break;
85,503,297✔
415
      }
416
      d_lat = lattice_distance.first;
1,381,476,825✔
417
      level_lat_trans = lattice_distance.second;
1,381,476,825✔
418

419
      if (d_lat < 0) {
1,381,476,825!
420
        p.mark_as_lost(fmt::format("Particle {} had a negative distance "
×
421
                                   "to a lattice boundary.",
422
          p.id()));
×
423
      }
424
    }
425

426
    // If the boundary on this coordinate level is coincident with a boundary on
427
    // a higher level then we need to make sure that the higher level boundary
428
    // is selected.  This logic must consider floating point precision.
429
    double& d = info.distance();
2,147,483,647✔
430
    if (d_surf < d_lat - FP_COINCIDENT) {
2,147,483,647✔
431
      if (d == INFINITY || (d - d_surf) / d >= FP_REL_PRECISION) {
2,147,483,647✔
432
        // Update closest distance
433
        d = d_surf;
2,147,483,647✔
434

435
        // If the cell is not simple, it is possible that both the negative and
436
        // positive half-space were given in the region specification. Thus, we
437
        // have to explicitly check which half-space the particle would be
438
        // traveling into if the surface is crossed
439
        if (c.is_simple() || d == INFTY) {
2,147,483,647✔
440
          info.surface() = level_surf_cross;
2,147,483,647✔
441
        } else {
442
          Position r_hit = r + d_surf * u;
22,141,426✔
443
          Surface& surf {*model::surfaces[std::abs(level_surf_cross) - 1]};
22,141,426✔
444
          Direction norm = surf.normal(r_hit);
22,141,426✔
445
          if (u.dot(norm) > 0) {
22,141,426✔
446
            info.surface() = std::abs(level_surf_cross);
11,069,060✔
447
          } else {
448
            info.surface() = -std::abs(level_surf_cross);
11,072,366✔
449
          }
450
        }
451

452
        info.lattice_translation()[0] = 0;
2,147,483,647✔
453
        info.lattice_translation()[1] = 0;
2,147,483,647✔
454
        info.lattice_translation()[2] = 0;
2,147,483,647✔
455
        info.coord_level() = i + 1;
2,147,483,647✔
456
      }
457
    } else {
458
      if (d == INFINITY || (d - d_lat) / d >= FP_REL_PRECISION) {
1,146,940,735!
459
        d = d_lat;
938,512,671✔
460
        info.surface() = SURFACE_NONE;
938,512,671✔
461
        info.lattice_translation() = level_lat_trans;
938,512,671✔
462
        info.coord_level() = i + 1;
938,512,671✔
463
      }
464
    }
465
  }
466
  return info;
2,147,483,647✔
467
}
468

469
//==============================================================================
470
// C API
471
//==============================================================================
472

473
extern "C" int openmc_find_cell(
600,308✔
474
  const double* xyz, int32_t* index, int32_t* instance)
475
{
476
  GeometryState geom_state;
600,308✔
477

478
  geom_state.r() = Position {xyz};
600,308✔
479
  geom_state.u() = {0.0, 0.0, 1.0};
600,308✔
480

481
  if (!exhaustive_find_cell(geom_state)) {
600,308✔
482
    set_errmsg(
11✔
483
      fmt::format("Could not find cell at position {}.", geom_state.r()));
11✔
484
    return OPENMC_E_GEOMETRY;
11✔
485
  }
486

487
  *index = geom_state.lowest_coord().cell();
600,297✔
488
  *instance = geom_state.cell_instance();
600,297✔
489
  return 0;
600,297✔
490
}
600,308✔
491

492
extern "C" int openmc_global_bounding_box(double* llc, double* urc)
11✔
493
{
494
  auto bbox = model::universes.at(model::root_universe)->bounding_box();
11✔
495

496
  // set lower left corner values
497
  llc[0] = bbox.min.x;
11✔
498
  llc[1] = bbox.min.y;
11✔
499
  llc[2] = bbox.min.z;
11✔
500

501
  // set upper right corner values
502
  urc[0] = bbox.max.x;
11✔
503
  urc[1] = bbox.max.y;
11✔
504
  urc[2] = bbox.max.z;
11✔
505

506
  return 0;
11✔
507
}
508

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