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

openmc-dev / openmc / 38247006718

12 Oct 2026 12:07PM UTC coverage: 81.447% (+0.007%) from 81.44%
38247006718

Pull #4189

github

web-flow
Merge 5983e8d44 into aa4afc41e
Pull Request #4189: fixing source plotting scale bug

20120 of 29244 branches covered (68.8%)

Branch coverage included in aggregate %.

2 of 2 new or added lines in 1 file covered. (100.0%)

721 existing lines in 40 files now uncovered.

62723 of 72470 relevant lines covered (86.55%)

57685680.03 hits per line

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

74.61
/src/cell.cpp
1

2
#include "openmc/cell.h"
3

4
#include <algorithm>
5
#include <cassert>
6
#include <cctype>
7
#include <cmath>
8
#include <iterator>
9
#include <set>
10
#include <sstream>
11
#include <string>
12

13
#include <fmt/core.h>
14

15
#include "openmc/capi.h"
16
#include "openmc/constants.h"
17
#include "openmc/dagmc.h"
18
#include "openmc/error.h"
19
#include "openmc/geometry.h"
20
#include "openmc/hdf5_interface.h"
21
#include "openmc/lattice.h"
22
#include "openmc/material.h"
23
#include "openmc/nuclide.h"
24
#include "openmc/settings.h"
25
#include "openmc/xml_interface.h"
26

27
namespace openmc {
28

29
//==============================================================================
30
// Global variables
31
//==============================================================================
32

33
namespace model {
34
std::unordered_map<int32_t, int32_t> cell_map;
35
vector<unique_ptr<Cell>> cells;
36

37
} // namespace model
38

39
//==============================================================================
40
// Cell implementation
41
//==============================================================================
42

43
int32_t Cell::n_instances() const
13,137 ✔
44
{
45
  return model::universes[universe_]->n_instances_;
13,137 ✔
46
}
47

48
void Cell::set_rotation(const vector<double>& rot)
300 ✔
49
{
50
  if (fill_ == C_NONE) {
300 !
51
    fatal_error(fmt::format("Cannot apply a rotation to cell {}"
×
52
                            " because it is not filled with another universe",
53
      id_));
×
54
  }
55

56
  if (rot.size() != 3 && rot.size() != 9) {
300 !
57
    fatal_error(fmt::format("Non-3D rotation vector applied to cell {}", id_));
×
58
  }
59

60
  // Compute and store the inverse rotation matrix for the angles given.
61
  rotation_.clear();
300 ✔
62
  rotation_.reserve(rot.size() == 9 ? 9 : 12);
600 !
63
  if (rot.size() == 3) {
300 !
64
    double phi = -rot[0] * PI / 180.0;
300 ✔
65
    double theta = -rot[1] * PI / 180.0;
300 ✔
66
    double psi = -rot[2] * PI / 180.0;
300 ✔
67
    rotation_.push_back(std::cos(theta) * std::cos(psi));
300 ✔
68
    rotation_.push_back(-std::cos(phi) * std::sin(psi) +
300 ✔
69
                        std::sin(phi) * std::sin(theta) * std::cos(psi));
300 ✔
70
    rotation_.push_back(std::sin(phi) * std::sin(psi) +
300 ✔
71
                        std::cos(phi) * std::sin(theta) * std::cos(psi));
300 ✔
72
    rotation_.push_back(std::cos(theta) * std::sin(psi));
300 ✔
73
    rotation_.push_back(std::cos(phi) * std::cos(psi) +
300 ✔
74
                        std::sin(phi) * std::sin(theta) * std::sin(psi));
300 ✔
75
    rotation_.push_back(-std::sin(phi) * std::cos(psi) +
300 ✔
76
                        std::cos(phi) * std::sin(theta) * std::sin(psi));
300 ✔
77
    rotation_.push_back(-std::sin(theta));
300 ✔
78
    rotation_.push_back(std::sin(phi) * std::cos(theta));
300 ✔
79
    rotation_.push_back(std::cos(phi) * std::cos(theta));
300 ✔
80

81
    // When user specifies angles, write them at end of vector
82
    rotation_.push_back(rot[0]);
300 ✔
83
    rotation_.push_back(rot[1]);
300 ✔
84
    rotation_.push_back(rot[2]);
300 ✔
85
  } else {
86
    std::copy(rot.begin(), rot.end(), std::back_inserter(rotation_));
×
87
  }
88
}
300 ✔
89

90
double Cell::temperature(int32_t instance) const
7,006 ✔
91
{
92
  if (sqrtkT_.size() < 1) {
7,006 !
93
    throw std::runtime_error {"Cell temperature has not yet been set."};
×
94
  }
95

96
  if (instance >= 0) {
7,006 ✔
97
    double sqrtkT = sqrtkT_.size() == 1 ? sqrtkT_.at(0) : sqrtkT_.at(instance);
6,944 ✔
98
    return sqrtkT * sqrtkT / K_BOLTZMANN;
6,944 ✔
99
  } else {
100
    return sqrtkT_[0] * sqrtkT_[0] / K_BOLTZMANN;
62 ✔
101
  }
102
}
103

104
double Cell::density_mult(int32_t instance) const
2,913,228,452 ✔
105
{
106
  if (instance >= 0) {
2,913,228,452 ✔
107
    return density_mult_.size() == 1 ? density_mult_.at(0)
2,913,228,396 ✔
108
                                     : density_mult_.at(instance);
3,678,980 ✔
109
  } else {
110
    return density_mult_[0];
56 ✔
111
  }
112
}
113

114
double Cell::density(int32_t instance) const
880,348 ✔
115
{
116
  const int32_t mat_index = material(instance);
880,348 ✔
117
  if (mat_index == MATERIAL_VOID)
880,348 !
118
    return 0.0;
119

120
  return density_mult(instance) * model::materials[mat_index]->density_gpcc();
1,760,696 ✔
121
}
122

123
void Cell::set_temperature(double T, int32_t instance, bool set_contained)
7,266 ✔
124
{
125
  if (settings::temperature_method == TemperatureMethod::INTERPOLATION) {
7,266 !
126
    if (T < (data::temperature_min - settings::temperature_tolerance)) {
×
127
      throw std::runtime_error {
×
128
        fmt::format("Temperature of {} K is below minimum temperature at "
×
129
                    "which data is available of {} K.",
130
          T, data::temperature_min)};
×
131
    } else if (T > (data::temperature_max + settings::temperature_tolerance)) {
×
132
      throw std::runtime_error {
×
133
        fmt::format("Temperature of {} K is above maximum temperature at "
×
134
                    "which data is available of {} K.",
135
          T, data::temperature_max)};
×
136
    }
137
  }
138

139
  if (type_ == Fill::MATERIAL) {
7,266 ✔
140
    if (instance >= 0) {
7,246 ✔
141
      // If temperature vector is not big enough, resize it first
142
      if (sqrtkT_.size() != n_instances())
7,190 ✔
143
        sqrtkT_.resize(n_instances(), sqrtkT_[0]);
30 ✔
144

145
      // Set temperature for the corresponding instance
146
      sqrtkT_.at(instance) = std::sqrt(K_BOLTZMANN * T);
7,190 ✔
147
    } else {
148
      // Set temperature for all instances
149
      for (auto& T_ : sqrtkT_) {
112 ✔
150
        T_ = std::sqrt(K_BOLTZMANN * T);
56 ✔
151
      }
152
    }
153
  } else {
154
    if (!set_contained) {
20 !
155
      throw std::runtime_error {
×
156
        fmt::format("Attempted to set the temperature of cell {} "
×
157
                    "which is not filled by a material.",
158
          id_)};
×
159
    }
160

161
    auto contained_cells = this->get_contained_cells(instance);
20 ✔
162
    for (const auto& entry : contained_cells) {
80 ✔
163
      auto& cell = model::cells[entry.first];
60 !
164
      assert(cell->type_ == Fill::MATERIAL);
60 !
165
      auto& instances = entry.second;
60 ✔
166
      for (auto instance : instances) {
210 ✔
167
        cell->set_temperature(T, instance);
150 ✔
168
      }
169
    }
170
  }
20 ✔
171
}
7,266 ✔
172

173
void Cell::set_density(double density, int32_t instance, bool set_contained)
248 ✔
174
{
175
  if (type_ != Fill::MATERIAL && !set_contained) {
248 !
176
    fatal_error(
×
177
      fmt::format("Attempted to set the density multiplier of cell {} "
×
178
                  "which is not filled by a material.",
179
        id_));
×
180
  }
181

182
  if (type_ == Fill::MATERIAL) {
248 ✔
183
    const int32_t mat_index = material(instance);
238 !
184
    if (mat_index == MATERIAL_VOID)
238 !
185
      return;
186

187
    if (instance >= 0) {
238 ✔
188
      // If density multiplier vector is not big enough, resize it first
189
      if (density_mult_.size() != n_instances())
182 ✔
190
        density_mult_.resize(n_instances(), density_mult_[0]);
78 ✔
191

192
      // Set density multiplier for the corresponding instance
193
      density_mult_.at(instance) =
182 ✔
194
        density / model::materials[mat_index]->density_gpcc();
364 !
195
    } else {
196
      // Set density multiplier for all instances
197
      for (auto& x : density_mult_) {
112 ✔
198
        x = density / model::materials[mat_index]->density_gpcc();
112 !
199
      }
200
    }
201
  } else {
202
    auto contained_cells = this->get_contained_cells(instance);
10 ✔
203
    for (const auto& entry : contained_cells) {
40 ✔
204
      auto& cell = model::cells[entry.first];
30 !
205
      assert(cell->type_ == Fill::MATERIAL);
30 !
206
      auto& instances = entry.second;
30 ✔
207
      for (auto instance : instances) {
60 ✔
208
        cell->set_density(density, instance);
30 ✔
209
      }
210
    }
211
  }
10 ✔
212
}
213

214
void Cell::export_properties_hdf5(hid_t group) const
168 ✔
215
{
216
  // Create a group for this cell.
217
  auto cell_group = create_group(group, fmt::format("cell {}", id_));
168 ✔
218

219
  // Write temperature in [K] for one or more cell instances
220
  vector<double> temps;
168 ✔
221
  for (auto sqrtkT_val : sqrtkT_)
312 ✔
222
    temps.push_back(sqrtkT_val * sqrtkT_val / K_BOLTZMANN);
144 ✔
223
  write_dataset(cell_group, "temperature", temps);
168 ✔
224

225
  // Write density for one or more cell instances
226
  if (type_ == Fill::MATERIAL && material_.size() > 0) {
168 ✔
227
    vector<double> density;
144 ✔
228
    for (int32_t i = 0; i < density_mult_.size(); ++i)
288 ✔
229
      density.push_back(this->density(i));
144 ✔
230

231
    write_dataset(cell_group, "density", density);
144 ✔
232
  }
144 ✔
233

234
  close_group(cell_group);
168 ✔
235
}
168 ✔
236

237
void Cell::import_properties_hdf5(hid_t group)
184 ✔
238
{
239
  auto cell_group = open_group(group, fmt::format("cell {}", id_));
184 ✔
240

241
  // Read temperatures from file
242
  vector<double> temps;
184 ✔
243
  read_dataset(cell_group, "temperature", temps);
184 ✔
244

245
  // Ensure number of temperatures makes sense
246
  auto n_temps = temps.size();
184 ✔
247
  if (n_temps > 1 && n_temps != n_instances()) {
184 !
248
    fatal_error(fmt::format(
×
249
      "Number of temperatures for cell {} doesn't match number of instances",
250
      id_));
×
251
  }
252

253
  // Modify temperatures for the cell
254
  sqrtkT_.clear();
184 ✔
255
  sqrtkT_.resize(temps.size());
184 ✔
256
  for (int64_t i = 0; i < temps.size(); ++i) {
7,216 ✔
257
    this->set_temperature(temps[i], i);
7,032 ✔
258
  }
259

260
  // Read densities
261
  if (object_exists(cell_group, "density")) {
184 ✔
262
    vector<double> density;
144 ✔
263
    read_dataset(cell_group, "density", density);
144 ✔
264

265
    // Ensure number of densities makes sense
266
    auto n_density = density.size();
144 !
267
    if (n_density > 1 && n_density != n_instances()) {
144 !
268
      fatal_error(fmt::format("Number of densities for cell {} "
×
269
                              "doesn't match number of instances",
270
        id_));
×
271
    }
272

273
    // Set densities.
274
    for (int32_t i = 0; i < n_density; ++i) {
288 ✔
275
      this->set_density(density[i], i);
144 ✔
276
    }
277
  }
144 ✔
278

279
  close_group(cell_group);
184 ✔
280
}
184 ✔
281

282
void Cell::to_hdf5(hid_t cell_group) const
26,969 ✔
283
{
284

285
  // Create a group for this cell.
286
  auto group = create_group(cell_group, fmt::format("cell {}", id_));
26,969 ✔
287

288
  if (!name_.empty()) {
26,969 ✔
289
    write_string(group, "name", name_, false);
6,006 ✔
290
  }
291

292
  write_dataset(group, "universe", model::universes[universe_]->id_);
26,969 ✔
293

294
  to_hdf5_inner(group);
26,969 ✔
295

296
  // Write fill information.
297
  if (type_ == Fill::MATERIAL) {
26,969 ✔
298
    write_dataset(group, "fill_type", "material");
22,657 ✔
299
    std::vector<int32_t> mat_ids;
22,657 ✔
300
    for (auto i_mat : material_) {
46,315 ✔
301
      if (i_mat != MATERIAL_VOID) {
23,658 ✔
302
        mat_ids.push_back(model::materials[i_mat]->id_);
13,569 ✔
303
      } else {
304
        mat_ids.push_back(MATERIAL_VOID);
10,089 ✔
305
      }
306
    }
307
    if (mat_ids.size() == 1) {
22,657 ✔
308
      write_dataset(group, "material", mat_ids[0]);
22,510 ✔
309
    } else {
310
      write_dataset(group, "material", mat_ids);
147 ✔
311
    }
312

313
    std::vector<double> temps;
22,657 ✔
314
    for (auto sqrtkT_val : sqrtkT_)
53,842 ✔
315
      temps.push_back(sqrtkT_val * sqrtkT_val / K_BOLTZMANN);
31,185 ✔
316
    write_dataset(group, "temperature", temps);
22,657 ✔
317

318
    write_dataset(group, "density_mult", density_mult_);
22,657 ✔
319

320
  } else if (type_ == Fill::UNIVERSE) {
26,969 ✔
321
    write_dataset(group, "fill_type", "universe");
3,082 ✔
322
    write_dataset(group, "fill", model::universes[fill_]->id_);
3,082 ✔
323
    if (translation_ != Position(0, 0, 0)) {
3,082 ✔
324
      write_dataset(group, "translation", translation_);
1,336 ✔
325
    }
326
    if (!rotation_.empty()) {
3,082 ✔
327
      if (rotation_.size() == 12) {
192 !
328
        std::array<double, 3> rot {rotation_[9], rotation_[10], rotation_[11]};
192 ✔
329
        write_dataset(group, "rotation", rot);
192 ✔
330
      } else {
331
        write_dataset(group, "rotation", rotation_);
×
332
      }
333
    }
334

335
  } else if (type_ == Fill::LATTICE) {
1,230 !
336
    write_dataset(group, "fill_type", "lattice");
1,230 ✔
337
    write_dataset(group, "lattice", model::lattices[fill_]->id_);
1,230 ✔
338
  }
339

340
  close_group(group);
26,969 ✔
341
}
26,969 ✔
342

343
//==============================================================================
344
// XML parsing helpers for <cell> nodes
345
//==============================================================================
346

347
vector<int32_t> parse_cell_material_xml(pugi::xml_node node, int32_t cell_id)
24,977 ✔
348
{
349
  vector<std::string> mats {
24,977 ✔
350
    get_node_array<std::string>(node, "material", true)};
24,977 ✔
351
  if (mats.empty()) {
24,977 !
352
    fatal_error(fmt::format(
×
353
      "An empty material element was specified for cell {}", cell_id));
354
  }
355
  vector<int32_t> material;
24,977 ✔
356
  material.reserve(mats.size());
24,977 ✔
357
  for (const auto& mat : mats) {
50,973 ✔
358
    if (mat == "void") {
25,996 ✔
359
      material.push_back(MATERIAL_VOID);
10,383 ✔
360
    } else {
361
      material.push_back(std::stoi(mat));
15,613 ✔
362
    }
363
  }
364
  return material;
24,977 ✔
365
}
24,977 ✔
366

367
vector<double> parse_cell_temperature_xml(pugi::xml_node node, int32_t cell_id)
288 ✔
368
{
369
  auto temperatures = get_node_array<double>(node, "temperature");
288 ✔
370
  if (temperatures.empty()) {
288 !
371
    fatal_error(fmt::format(
×
372
      "An empty temperature element was specified for cell {}", cell_id));
373
  }
374
  for (auto T : temperatures) {
1,296 ✔
375
    if (T < 0) {
1,008 !
376
      fatal_error(fmt::format(
×
377
        "Cell {} was specified with a negative temperature", cell_id));
378
    }
379
  }
380
  return temperatures;
288 ✔
381
}
×
382

383
vector<double> parse_cell_density_xml(pugi::xml_node node, int32_t cell_id)
50 ✔
384
{
385
  auto densities = get_node_array<double>(node, "density");
50 ✔
386
  if (densities.empty()) {
50 !
387
    fatal_error(fmt::format(
×
388
      "An empty density element was specified for cell {}", cell_id));
389
  }
390
  for (auto rho : densities) {
820 ✔
391
    if (rho <= 0) {
770 !
392
      fatal_error(fmt::format(
×
393
        "Cell {} was specified with a density less than or equal to zero",
394
        cell_id));
395
    }
396
  }
397
  return densities;
50 ✔
398
}
×
399

400
//==============================================================================
401
// CSGCell implementation
402
//==============================================================================
403

404
CSGCell::CSGCell(pugi::xml_node cell_node)
30,196 ✔
405
{
406
  if (check_for_node(cell_node, "id")) {
30,196 !
407
    id_ = std::stoi(get_node_value(cell_node, "id"));
60,392 ✔
408
  } else {
409
    fatal_error("Must specify id of cell in geometry XML file.");
×
410
  }
411

412
  if (check_for_node(cell_node, "name")) {
30,196 ✔
413
    name_ = get_node_value(cell_node, "name");
7,143 ✔
414
  }
415

416
  if (check_for_node(cell_node, "universe")) {
30,196 ✔
417
    universe_ = std::stoi(get_node_value(cell_node, "universe"));
58,768 ✔
418
  } else {
419
    universe_ = 0;
812 ✔
420
  }
421

422
  // Make sure that either material or fill was specified, but not both.
423
  bool fill_present = check_for_node(cell_node, "fill");
30,196 ✔
424
  bool material_present = check_for_node(cell_node, "material");
30,196 ✔
425
  if (!(fill_present || material_present)) {
30,196 !
426
    fatal_error(
×
427
      fmt::format("Neither material nor fill was specified for cell {}", id_));
×
428
  }
429
  if (fill_present && material_present) {
30,196 !
430
    fatal_error(fmt::format("Cell {} has both a material and a fill specified; "
×
431
                            "only one can be specified per cell",
432
      id_));
×
433
  }
434

435
  if (fill_present) {
30,196 ✔
436
    fill_ = std::stoi(get_node_value(cell_node, "fill"));
10,456 ✔
437
    if (fill_ == universe_) {
5,228 !
438
      fatal_error(fmt::format("Cell {} is filled with the same universe that "
×
439
                              "it is contained in.",
440
        id_));
×
441
    }
442
  } else {
443
    fill_ = C_NONE;
24,968 ✔
444
  }
445

446
  // Read the material element.  There can be zero materials (filled with a
447
  // universe), more than one material (distribmats), and some materials may
448
  // be "void".
449
  if (material_present) {
30,196 ✔
450
    material_ = parse_cell_material_xml(cell_node, id_);
24,968 ✔
451
  }
452

453
  // Read the temperature element which may be distributed like materials.
454
  if (check_for_node(cell_node, "temperature")) {
30,196 ✔
455
    sqrtkT_ = parse_cell_temperature_xml(cell_node, id_);
288 ✔
456
    sqrtkT_.shrink_to_fit();
288 ✔
457

458
    // Make sure this is a material-filled cell.
459
    if (material_.size() == 0) {
288 !
460
      fatal_error(fmt::format(
×
461
        "Cell {} was specified with a temperature but no material. Temperature"
462
        "specification is only valid for cells filled with a material.",
463
        id_));
×
464
    }
465

466
    // Convert to sqrt(k*T).
467
    for (auto& T : sqrtkT_) {
1,296 ✔
468
      T = std::sqrt(K_BOLTZMANN * T);
1,008 ✔
469
    }
470
  }
471

472
  // Read the density element which can be distributed similar to temperature.
473
  // These get assigned to the density multiplier, requiring a division by
474
  // the material density.
475
  // Note: calculating the actual density multiplier is deferred until materials
476
  // are finalized. density_mult_ contains the true density in the meantime.
477
  if (check_for_node(cell_node, "density")) {
30,196 ✔
478
    density_mult_ = parse_cell_density_xml(cell_node, id_);
50 ✔
479
    density_mult_.shrink_to_fit();
50 ✔
480

481
    // Make sure this is a material-filled cell.
482
    if (material_.size() == 0) {
50 !
483
      fatal_error(fmt::format(
×
484
        "Cell {} was specified with a density but no material. Density"
485
        "specification is only valid for cells filled with a material.",
486
        id_));
×
487
    }
488

489
    // Make sure this is a non-void material.
490
    for (auto mat_id : material_) {
100 ✔
491
      if (mat_id == MATERIAL_VOID) {
50 !
492
        fatal_error(fmt::format(
×
493
          "Cell {} was specified with a density, but contains a void "
494
          "material. Density specification is only valid for cells "
495
          "filled with a non-void material.",
496
          id_));
×
497
      }
498
    }
499
  }
500

501
  // Read the region specification.
502
  std::string region_spec;
30,196 ✔
503
  if (check_for_node(cell_node, "region")) {
30,196 ✔
504
    region_spec = get_node_value(cell_node, "region");
19,589 ✔
505
  }
506

507
  // Get a tokenized representation of the region specification and apply De
508
  // Morgans law
509
  Region region(region_spec, id_);
30,196 ✔
510
  region_ = region;
30,196 ✔
511

512
  // Read the translation vector.
513
  if (check_for_node(cell_node, "translation")) {
30,196 ✔
514
    if (fill_ == C_NONE) {
1,706 !
515
      fatal_error(fmt::format("Cannot apply a translation to cell {}"
×
516
                              " because it is not filled with another universe",
517
        id_));
×
518
    }
519

520
    auto xyz {get_node_array<double>(cell_node, "translation")};
1,706 ✔
521
    if (xyz.size() != 3) {
1,706 !
522
      fatal_error(
×
523
        fmt::format("Non-3D translation vector applied to cell {}", id_));
×
524
    }
525
    translation_ = xyz;
1,706 ✔
526
  }
1,706 ✔
527

528
  // Read the rotation transform.
529
  if (check_for_node(cell_node, "rotation")) {
30,196 ✔
530
    auto rot {get_node_array<double>(cell_node, "rotation")};
260 ✔
531
    set_rotation(rot);
260 ✔
532
  }
260 ✔
533
}
30,196 ✔
534

535
//==============================================================================
536

537
void CSGCell::to_hdf5_inner(hid_t group_id) const
26,809 ✔
538
{
539
  write_string(group_id, "geom_type", "csg", false);
26,809 ✔
540
  write_string(group_id, "region", region_.str(), false);
26,809 ✔
541
}
26,809 ✔
542

543
//==============================================================================
544

545
vector<int32_t>::iterator CSGCell::find_left_parenthesis(
×
546
  vector<int32_t>::iterator start, const vector<int32_t>& infix)
547
{
548
  // start search at zero
549
  int parenthesis_level = 0;
×
550
  auto it = start;
×
551
  while (it != infix.begin()) {
×
552
    // look at two tokens at a time
553
    int32_t one = *it;
×
554
    int32_t two = *(it - 1);
×
555

556
    // decrement parenthesis level if there are two adjacent surfaces
557
    if (one < OP_UNION && two < OP_UNION) {
×
558
      parenthesis_level--;
×
559
      // increment if there are two adjacent operators
560
    } else if (one >= OP_UNION && two >= OP_UNION) {
×
561
      parenthesis_level++;
×
562
    }
563

564
    // if the level gets to zero, return the position
565
    if (parenthesis_level == 0) {
×
566
      // move the iterator back one before leaving the loop
567
      // so that all tokens in the parenthesis block are included
568
      it--;
×
569
      break;
570
    }
571

572
    // continue loop, one token at a time
573
    it--;
574
  }
575
  return it;
×
576
}
577

578
//==============================================================================
579
// Region implementation
580
//==============================================================================
581

582
Region::Region(std::string region_spec, int32_t cell_id)
30,300 ✔
583
{
584
  // Check if region_spec is not empty.
585
  if (!region_spec.empty()) {
30,300 ✔
586
    // Parse all halfspaces and operators except for intersection (whitespace).
587
    for (int i = 0; i < region_spec.size();) {
121,988 ✔
588
      if (region_spec[i] == '(') {
102,295 ✔
589
        expression_.push_back(OP_LEFT_PAREN);
1,276 ✔
590
        i++;
1,276 ✔
591

592
      } else if (region_spec[i] == ')') {
101,019 ✔
593
        expression_.push_back(OP_RIGHT_PAREN);
1,276 ✔
594
        i++;
1,276 ✔
595

596
      } else if (region_spec[i] == '|') {
99,743 ✔
597
        expression_.push_back(OP_UNION);
3,591 ✔
598
        i++;
3,591 ✔
599

600
      } else if (region_spec[i] == '~') {
96,152 ✔
601
        expression_.push_back(OP_COMPLEMENT);
20 ✔
602
        i++;
20 ✔
603

604
      } else if (region_spec[i] == '-' || region_spec[i] == '+' ||
162,532 ✔
605
                 std::isdigit(region_spec[i])) {
66,400 ✔
606
        // This is the start of a halfspace specification.  Iterate j until we
607
        // find the end, then push-back everything between i and j.
608
        int j = i + 1;
55,619 ✔
609
        while (j < region_spec.size() && std::isdigit(region_spec[j])) {
112,082 ✔
610
          j++;
56,463 ✔
611
        }
612
        expression_.push_back(std::stoi(region_spec.substr(i, j - i)));
111,238 ✔
613
        i = j;
55,619 ✔
614

615
      } else if (std::isspace(region_spec[i])) {
40,513 !
616
        i++;
40,513 ✔
617

618
      } else {
619
        auto err_msg =
×
620
          fmt::format("Region specification contains invalid character, \"{}\"",
621
            region_spec[i]);
×
622
        fatal_error(err_msg);
×
623
      }
×
624
    }
625

626
    // Add in intersection operators where a missing operator is needed.
627
    int i = 0;
628
    while (i < expression_.size() - 1) {
94,117 ✔
629
      bool left_compat {
74,424 ✔
630
        (expression_[i] < OP_UNION) || (expression_[i] == OP_RIGHT_PAREN)};
74,424 ✔
631
      bool right_compat {(expression_[i + 1] < OP_UNION) ||
74,424 ✔
632
                         (expression_[i + 1] == OP_LEFT_PAREN) ||
74,424 ✔
633
                         (expression_[i + 1] == OP_COMPLEMENT)};
4,907 ✔
634
      if (left_compat && right_compat) {
74,424 ✔
635
        expression_.insert(expression_.begin() + i + 1, OP_INTERSECTION);
32,335 ✔
636
      }
637
      i++;
638
    }
639

640
    // Remove complement operators using DeMorgan's laws
641
    auto it = std::find(expression_.begin(), expression_.end(), OP_COMPLEMENT);
19,693 ✔
642
    while (it != expression_.end()) {
19,713 ✔
643
      // Erase complement. Note that erase invalidates the iterator, so we have
644
      // to use the iterator it returns, which points to the token that
645
      // followed the complement operator.
646
      it = expression_.erase(it);
20 ✔
647
      if (it == expression_.end())
20 !
648
        break;
649

650
      // Define stop given left parenthesis or not
651
      auto stop = it;
20 ✔
652
      if (*it == OP_LEFT_PAREN) {
20 !
653
        int depth = 1;
654
        do {
160 ✔
655
          stop++;
160 ✔
656
          if (*stop > OP_COMPLEMENT) {
160 ✔
657
            if (*stop == OP_RIGHT_PAREN) {
20 !
658
              depth--;
20 ✔
659
            } else {
660
              depth++;
×
661
            }
662
          }
663
        } while (depth > 0);
160 ✔
664
        it++;
20 ✔
665
      }
666

667
      // apply DeMorgan's law to any surfaces/operators between these
668
      // positions in the RPN
669
      apply_demorgan(it, stop);
20 ✔
670
      // update iterator position
671
      it = std::find(expression_.begin(), expression_.end(), OP_COMPLEMENT);
20 ✔
672
    }
673

674
    // Convert user IDs to surface indices.
675
    for (auto& r : expression_) {
113,790 ✔
676
      if (r < OP_UNION) {
94,097 ✔
677
        const auto& it {model::surface_map.find(abs(r))};
55,619 !
678
        if (it == model::surface_map.end()) {
55,619 !
679
          throw std::runtime_error {
×
680
            "Invalid surface ID " + std::to_string(abs(r)) +
×
681
            " specified in region for cell " + std::to_string(cell_id) + "."};
×
682
        }
683
        r = (r > 0) ? it->second + 1 : -(it->second + 1);
55,619 ✔
684
      }
685
    }
686

687
    // Check if this is a simple cell.
688
    simple_ = true;
19,693 ✔
689
    for (int32_t token : expression_) {
101,901 ✔
690
      if (token == OP_UNION) {
83,190 ✔
691
        simple_ = false;
982 ✔
692
        // Ensure intersections have precedence over unions
693
        enforce_precedence();
982 ✔
694
        break;
695
      }
696
    }
697

698
    // If this cell is simple, remove all the superfluous operator tokens.
699
    if (simple_) {
19,693 ✔
700
      expression_.erase(std::remove_if(expression_.begin(), expression_.end(),
18,711 ✔
701
                          [](int32_t token) {
75,869 ✔
702
                            return token == OP_INTERSECTION ||
75,869 ✔
703
                                   token > OP_COMPLEMENT;
69,911 !
704
                          }),
705
        expression_.end());
18,711 ✔
706
    }
707
    expression_.shrink_to_fit();
19,693 ✔
708

709
  } else {
710
    simple_ = true;
10,607 ✔
711
  }
712
}
30,300 ✔
713

714
//==============================================================================
715

716
void Region::apply_demorgan(
20 ✔
717
  vector<int32_t>::iterator start, vector<int32_t>::iterator stop)
718
{
719
  do {
140 ✔
720
    if (*start < OP_UNION) {
140 ✔
721
      *start *= -1;
80 ✔
722
    } else if (*start == OP_UNION) {
60 !
723
      *start = OP_INTERSECTION;
×
724
    } else if (*start == OP_INTERSECTION) {
60 !
725
      *start = OP_UNION;
60 ✔
726
    }
727
    start++;
140 ✔
728
  } while (start < stop);
140 ✔
729
}
20 ✔
730

731
//==============================================================================
732
//! Add precedence for infix regions so intersections have higher
733
//! precedence than unions using parentheses.
734
//==============================================================================
735

736
void Region::add_parentheses(int64_t start)
68 ✔
737
{
738
  int32_t start_token = expression_[start];
68 !
739
  // Add left parenthesis and set new position to be after parenthesis
740
  if (start_token == OP_UNION) {
68 !
741
    start += 2;
×
742
  }
743
  expression_.insert(expression_.begin() + start - 1, OP_LEFT_PAREN);
68 ✔
744

745
  // Add right parenthesis
746
  // While the start iterator is within the bounds of infix
747
  while (start + 1 < expression_.size()) {
300 ✔
748
    start++;
284 ✔
749

750
    // If the current token is an operator and is different than the start token
751
    if (expression_[start] >= OP_UNION && expression_[start] != start_token) {
284 ✔
752
      // Skip wrapped regions but save iterator position to check precedence and
753
      // add right parenthesis, right parenthesis position depends on the
754
      // operator, when the operator is a union then do not include the operator
755
      // in the region, when the operator is an intersection then include the
756
      // operator and next surface
757
      if (expression_[start] == OP_LEFT_PAREN) {
60 ✔
758
        int depth = 1;
759
        do {
32 ✔
760
          start++;
32 ✔
761
          if (expression_[start] > OP_COMPLEMENT) {
32 ✔
762
            if (expression_[start] == OP_RIGHT_PAREN) {
8 !
763
              depth--;
8 ✔
764
            } else {
765
              depth++;
×
766
            }
767
          }
768
        } while (depth > 0);
32 ✔
769
      } else {
770
        if (start_token == OP_UNION) {
52 !
771
          --start;
×
772
        }
773
        expression_.insert(expression_.begin() + start, OP_RIGHT_PAREN);
52 ✔
774
        return;
52 ✔
775
      }
776
    }
777
  }
778
  // If we get here a right parenthesis hasn't been placed
779
  expression_.push_back(OP_RIGHT_PAREN);
16 ✔
780
}
781

782
//==============================================================================
783
//! Add parentheses to enforce operator precedence in region expressions
784
//!
785
//! This function ensures that intersection operators have higher precedence
786
//! than union operators by adding parentheses where needed. For example:
787
//!   "1 2 | 3" becomes "(1 2) | 3"
788
//!   "1 | 2 3" becomes "1 | (2 3)"
789
//!
790
//! The algorithm uses stacks to track the current operator type and its
791
//! position at each parenthesis depth level. When it encounters a different
792
//! operator at the same depth, it adds parentheses to group the
793
//! higher-precedence operations.
794
//==============================================================================
795

796
void Region::enforce_precedence()
982 ✔
797
{
798
  // Stack tracking the operator type at each depth (0 = no operator seen yet)
799
  vector<int32_t> op_stack = {0};
982 ✔
800

801
  // Stack tracking where the operator sequence started at each depth
802
  vector<std::size_t> pos_stack = {0};
982 ✔
803

804
  for (int64_t i = 0; i < expression_.size(); ++i) {
19,926 ✔
805
    int32_t token = expression_[i];
18,944 ✔
806

807
    if (token == OP_LEFT_PAREN) {
18,944 ✔
808
      // Entering a new parenthesis level - push new tracking state
809
      op_stack.push_back(0);
1,414 ✔
810
      pos_stack.push_back(0);
1,414 ✔
811
      continue;
1,414 ✔
812
    } else if (token == OP_RIGHT_PAREN) {
17,530 ✔
813
      // Exiting a parenthesis level - pop tracking state (keep at least one)
814
      if (op_stack.size() > 1) {
1,386 !
815
        op_stack.pop_back();
1,386 ✔
816
        pos_stack.pop_back();
1,386 ✔
817
      }
818
      continue;
1,386 ✔
819
    }
820

821
    if (token == OP_UNION || token == OP_INTERSECTION) {
16,144 ✔
822
      if (op_stack.back() == 0) {
7,581 ✔
823
        // First operator at this depth - record it and its position
824
        op_stack.back() = token;
2,428 ✔
825
        pos_stack.back() = i;
2,428 ✔
826
      } else if (token != op_stack.back()) {
5,153 ✔
827
        // Encountered a different operator at the same depth - need to add
828
        // parentheses to enforce precedence. Intersection has higher
829
        // precedence, so we parenthesize the intersection terms.
830
        if (op_stack.back() == OP_INTERSECTION) {
68 ✔
831
          add_parentheses(pos_stack.back());
34 ✔
832
        } else {
833
          add_parentheses(i);
34 ✔
834
        }
835

836
        // Restart the scan since we modified the expression
837
        i = -1; // Will be incremented to 0 by the for loop
68 ✔
838
        op_stack = {0};
68 ✔
839
        pos_stack = {0};
68 ✔
840
      }
841
    }
842
  }
843
}
982 ✔
844

845
//==============================================================================
846
//! Convert infix region specification to Reverse Polish Notation (RPN)
847
//!
848
//! This function uses the shunting-yard algorithm.
849
//==============================================================================
850

851
vector<int32_t> Region::generate_postfix(int32_t cell_id) const
32 ✔
852
{
853
  vector<int32_t> rpn;
32 ✔
854
  vector<int32_t> stack;
32 ✔
855

856
  for (int32_t token : expression_) {
720 ✔
857
    if (token < OP_UNION) {
688 ✔
858
      // If token is not an operator, add it to output
859
      rpn.push_back(token);
288 ✔
860
    } else if (token < OP_RIGHT_PAREN) {
400 ✔
861
      // Regular operators union, intersection, complement
862
      while (stack.size() > 0) {
408 ✔
863
        int32_t op = stack.back();
336 ✔
864

865
        if (op < OP_RIGHT_PAREN && ((token == OP_COMPLEMENT && token < op) ||
336 !
866
                                     (token != OP_COMPLEMENT && token <= op))) {
152 !
867
          // While there is an operator, op, on top of the stack, if the token
868
          // is left-associative and its precedence is less than or equal to
869
          // that of op or if the token is right-associative and its precedence
870
          // is less than that of op, move op to the output queue and push the
871
          // token on to the stack. Note that only complement is
872
          // right-associative.
873
          rpn.push_back(op);
152 ✔
874
          stack.pop_back();
152 ✔
875
        } else {
876
          break;
877
        }
878
      }
879

880
      stack.push_back(token);
256 ✔
881

882
    } else if (token == OP_LEFT_PAREN) {
144 ✔
883
      // If the token is a left parenthesis, push it onto the stack
884
      stack.push_back(token);
72 ✔
885

886
    } else {
887
      // If the token is a right parenthesis, move operators from the stack to
888
      // the output queue until reaching the left parenthesis.
889
      for (auto it = stack.rbegin(); *it != OP_LEFT_PAREN; it++) {
144 ✔
890
        // If we run out of operators without finding a left parenthesis, it
891
        // means there are mismatched parentheses.
892
        if (it == stack.rend()) {
72 !
893
          fatal_error(fmt::format(
×
894
            "Mismatched parentheses in region specification for cell {}",
895
            cell_id));
896
        }
897
        rpn.push_back(stack.back());
72 ✔
898
        stack.pop_back();
72 ✔
899
      }
900

901
      // Pop the left parenthesis.
902
      stack.pop_back();
688 ✔
903
    }
904
  }
905

906
  while (stack.size() > 0) {
32 ✔
907
    int32_t op = stack.back();
32 !
908

909
    // If the operator is a parenthesis it is mismatched.
910
    if (op >= OP_RIGHT_PAREN) {
32 !
911
      fatal_error(fmt::format(
×
912
        "Mismatched parentheses in region specification for cell {}", cell_id));
913
    }
914

915
    rpn.push_back(stack.back());
32 ✔
916
    stack.pop_back();
64 ✔
917
  }
918

919
  return rpn;
32 ✔
920
}
32 ✔
921

922
//==============================================================================
923

924
std::string Region::str() const
26,881 ✔
925
{
926
  std::stringstream region_spec {};
26,881 ✔
927
  if (!expression_.empty()) {
26,881 ✔
928
    for (int32_t token : expression_) {
74,098 ✔
929
      if (token == OP_LEFT_PAREN) {
57,376 ✔
930
        region_spec << " (";
1,232 ✔
931
      } else if (token == OP_RIGHT_PAREN) {
56,144 ✔
932
        region_spec << " )";
1,232 ✔
933
      } else if (token == OP_COMPLEMENT) {
54,912 !
934
        region_spec << " ~";
×
935
      } else if (token == OP_INTERSECTION) {
54,912 ✔
936
      } else if (token == OP_UNION) {
51,514 ✔
937
        region_spec << " |";
3,395 ✔
938
      } else {
939
        // Note the off-by-one indexing
940
        auto surf_id = model::surfaces[abs(token) - 1]->id_;
48,119 ✔
941
        region_spec << " " << ((token > 0) ? surf_id : -surf_id);
48,119 ✔
942
      }
943
    }
944
  }
945
  return region_spec.str();
53,762 ✔
946
}
26,881 ✔
947

948
//==============================================================================
949

950
std::pair<double, int32_t> Region::distance(
4,294,967,294 ✔
951
  Position r, Direction u, int32_t on_surface) const
952
{
953
  if (simple_) {
4,294,967,294 ✔
954
    return distance_to_nearest_surface(r, u, on_surface, false);
4,294,967,294 ✔
955
  } else {
956
    return distance_complex(r, u, on_surface);
141,724,413 ✔
957
  }
958
}
959

960
//==============================================================================
961

962
std::pair<double, int32_t> Region::distance_to_nearest_surface(Position r,
4,294,967,294 ✔
963
  Direction u, int32_t on_surface, bool ignore_coincident_surfaces) const
964
{
965
  double min_dist {INFTY};
4,294,967,294 ✔
966
  int32_t i_surf {std::numeric_limits<int32_t>::max()};
4,294,967,294 ✔
967

968
  for (int32_t token : expression_) {
×
969
    // Ignore this token if it corresponds to an operator rather than a region.
970
    if (token >= OP_UNION)
✔
971
      continue;
4,294,967,294 ✔
972

973
    // Calculate the distance to this surface.
974
    // Note the off-by-one indexing
975
    bool coincident {std::abs(token) == std::abs(on_surface)};
4,294,967,294 ✔
976
    double d {model::surfaces[abs(token) - 1]->distance(r, u, coincident)};
4,294,967,294 ✔
977

978
    // Different surface definitions can represent the same geometric surface.
979
    // When the ray is already known to be on a surface, ignore intersections
980
    // with other surfaces at the same location to avoid repeatedly crossing
981
    // between them due to roundoff.
982
    if (ignore_coincident_surfaces && d < FP_COINCIDENT)
4,294,967,294 ✔
983
      continue;
8 ✔
984

985
    // Check if this distance is the new minimum.
986
    if (d < min_dist) {
4,294,967,294 ✔
987
      if (min_dist - d >= FP_PRECISION * min_dist) {
4,294,967,294 !
988
        min_dist = d;
4,294,967,294 ✔
989
        i_surf = -token;
4,294,967,294 ✔
990
      }
991
    }
992
  }
993

994
  return {min_dist, i_surf};
4,294,967,294 ✔
995
}
996

997
//==============================================================================
998

999
std::pair<double, int32_t> Region::distance_complex(
141,724,413 ✔
1000
  Position r, Direction u, int32_t on_surface) const
1001
{
1002
  const bool in_region = contains_complex(r, u, on_surface);
141,724,413 ✔
1003
  double total_distance {0.0};
1004

1005
  while (true) {
783,834,665 ✔
1006
    auto [distance, i_surf] =
925,559,078 ✔
1007
      distance_to_nearest_surface(r, u, on_surface, on_surface != 0);
462,779,539 ✔
1008
    if (distance == INFTY) {
462,779,539 ✔
1009
      return {INFTY, std::numeric_limits<int32_t>::max()};
1,236,322 ✔
1010
    }
1011

1012
    // Move to the candidate surface and determine which side of it the ray is
1013
    // entering. The surface normal is used instead of evaluating the surface
1014
    // equation because accumulated roundoff may place the point slightly to
1015
    // the wrong side of a curved surface.
1016
    r += distance * u;
461,543,217 ✔
1017
    total_distance += distance;
461,543,217 ✔
1018
    i_surf = std::abs(i_surf);
461,543,217 ✔
1019
    const auto& surf {*model::surfaces[i_surf - 1]};
461,543,217 ✔
1020
    if (u.dot(surf.normal(r)) <= 0.0) {
461,543,217 ✔
1021
      i_surf = -i_surf;
161,729,696 ✔
1022
    }
1023

1024
    // If crossing the candidate changes the region membership, it is a true
1025
    // boundary. Otherwise, continue the search from the virtual crossing.
1026
    if (contains_complex(r, u, i_surf) != in_region) {
461,543,217 ✔
1027
      return {total_distance, i_surf};
140,488,091 ✔
1028
    }
1029
    on_surface = i_surf;
321,055,126 ✔
1030
  }
321,055,126 ✔
1031
}
1032

1033
//==============================================================================
1034

1035
bool Region::contains(Position r, Direction u, int32_t on_surface) const
4,294,967,294 ✔
1036
{
1037
  if (simple_) {
4,294,967,294 ✔
1038
    return contains_simple(r, u, on_surface);
4,294,967,294 ✔
1039
  } else {
1040
    return contains_complex(r, u, on_surface);
20,916,753 ✔
1041
  }
1042
}
1043

1044
//==============================================================================
1045

1046
bool Region::contains_simple(Position r, Direction u, int32_t on_surface) const
4,294,967,294 ✔
1047
{
1048
  for (int32_t token : expression_) {
4,294,967,294 ✔
1049
    // Assume that no tokens are operators. Evaluate the sense of particle with
1050
    // respect to the surface and see if the token matches the sense. If the
1051
    // particle's surface attribute is set and matches the token, that
1052
    // overrides the determination based on sense().
1053
    if (token == on_surface) {
4,294,967,294 ✔
1054
    } else if (-token == on_surface) {
4,294,967,294 ✔
1055
      return false;
1056
    } else {
1057
      // Note the off-by-one indexing
1058
      bool sense = model::surfaces[abs(token) - 1]->sense(r, u);
4,294,967,294 ✔
1059
      if (sense != (token > 0)) {
4,294,967,294 ✔
1060
        return false;
1061
      }
1062
    }
1063
  }
1064
  return true;
1065
}
1066

1067
//==============================================================================
1068

1069
bool Region::contains_complex(Position r, Direction u, int32_t on_surface) const
624,184,383 ✔
1070
{
1071
  bool in_cell = true;
624,184,383 ✔
1072
  int total_depth = 0;
624,184,383 ✔
1073

1074
  // For each token
1075
  for (auto it = expression_.begin(); it != expression_.end(); it++) {
4,294,967,294 ✔
1076
    int32_t token = *it;
4,294,967,294 ✔
1077

1078
    // If the token is a surface evaluate the sense
1079
    // If the token is a union or intersection check to
1080
    // short circuit
1081
    if (token < OP_UNION) {
4,294,967,294 ✔
1082
      if (token == on_surface) {
2,490,728,210 ✔
1083
        in_cell = true;
1084
      } else if (-token == on_surface) {
2,452,698,549 ✔
1085
        in_cell = false;
1086
      } else {
1087
        // Note the off-by-one indexing
1088
        bool sense = model::surfaces[abs(token) - 1]->sense(r, u);
2,272,198,685 ✔
1089
        in_cell = (sense == (token > 0));
2,272,198,685 ✔
1090
      }
1091
    } else if ((token == OP_UNION && in_cell == true) ||
3,979,915,976 ✔
1092
               (token == OP_INTERSECTION && in_cell == false)) {
1,323,698,461 ✔
1093
      // If the total depth is zero return
1094
      if (total_depth == 0) {
1,091,253,412 ✔
1095
        return in_cell;
145,128,715 ✔
1096
      }
1097

1098
      total_depth--;
946,124,697 ✔
1099

1100
      // While the iterator is within the bounds of the vector
1101
      int depth = 1;
946,124,697 ✔
1102
      do {
4,294,967,294 ✔
1103
        // Get next token
1104
        it++;
4,294,967,294 ✔
1105
        int32_t next_token = *it;
4,294,967,294 ✔
1106

1107
        // If the token is an a parenthesis
1108
        if (next_token > OP_COMPLEMENT) {
4,294,967,294 ✔
1109
          // Adjust depth accordingly
1110
          if (next_token == OP_RIGHT_PAREN) {
1,173,583,683 ✔
1111
            depth--;
1,059,854,190 ✔
1112
          } else {
1113
            depth++;
113,729,493 ✔
1114
          }
1115
        }
1116
      } while (depth > 0);
4,294,967,294 ✔
1117
    } else if (token == OP_LEFT_PAREN) {
2,888,662,564 ✔
1118
      total_depth++;
984,121,717 ✔
1119
    } else if (token == OP_RIGHT_PAREN) {
1,904,540,847 ✔
1120
      total_depth--;
37,997,020 ✔
1121
    }
1122
  }
1123
  return in_cell;
1124
}
1125

1126
//==============================================================================
1127

1128
BoundingBox Region::bounding_box(int32_t cell_id) const
72 ✔
1129
{
1130
  if (simple_) {
72 ✔
1131
    return bounding_box_simple();
40 ✔
1132
  } else {
1133
    auto postfix = generate_postfix(cell_id);
32 ✔
1134
    return bounding_box_complex(postfix);
64 ✔
1135
  }
32 ✔
1136
}
1137

1138
//==============================================================================
1139

1140
BoundingBox Region::bounding_box_simple() const
40 ✔
1141
{
1142
  BoundingBox bbox;
40 ✔
1143
  for (int32_t token : expression_) {
160 ✔
1144
    bbox &= model::surfaces[abs(token) - 1]->bounding_box(token > 0);
120 ✔
1145
  }
1146
  return bbox;
40 ✔
1147
}
1148

1149
//==============================================================================
1150

1151
BoundingBox Region::bounding_box_complex(vector<int32_t> postfix) const
32 ✔
1152
{
1153
  vector<BoundingBox> stack(postfix.size());
32 ✔
1154
  int i_stack = -1;
32 ✔
1155

1156
  for (auto& token : postfix) {
576 ✔
1157
    if (token == OP_UNION) {
544 ✔
1158
      stack[i_stack - 1] = stack[i_stack - 1] | stack[i_stack];
112 ✔
1159
      i_stack--;
112 ✔
1160
    } else if (token == OP_INTERSECTION) {
432 ✔
1161
      stack[i_stack - 1] = stack[i_stack - 1] & stack[i_stack];
144 ✔
1162
      i_stack--;
144 ✔
1163
    } else {
1164
      i_stack++;
288 ✔
1165
      stack[i_stack] = model::surfaces[abs(token) - 1]->bounding_box(token > 0);
288 ✔
1166
    }
1167
  }
1168

1169
  assert(i_stack == 0);
32 !
1170
  return stack.front();
32 ✔
1171
}
32 ✔
1172

1173
//==============================================================================
1174

1175
vector<int32_t> Region::surfaces() const
3,542 ✔
1176
{
1177
  if (simple_) {
3,542 ✔
1178
    return expression_;
3,528 ✔
1179
  }
1180

1181
  vector<int32_t> surfaces = expression_;
14 ✔
1182

1183
  auto it = std::find_if(surfaces.begin(), surfaces.end(),
14 ✔
1184
    [&](const auto& value) { return value >= OP_UNION; });
14 !
1185

1186
  while (it != surfaces.end()) {
42 ✔
1187
    surfaces.erase(it);
28 ✔
1188

1189
    it = std::find_if(surfaces.begin(), surfaces.end(),
28 ✔
1190
      [&](const auto& value) { return value >= OP_UNION; });
56 !
1191
  }
1192

1193
  return surfaces;
14 ✔
1194
}
3,542 ✔
1195

1196
//==============================================================================
1197
// Non-method functions
1198
//==============================================================================
1199

1200
void read_cells(pugi::xml_node node)
6,840 ✔
1201
{
1202
  // Count the number of cells.
1203
  auto cell_nodes = node.children("cell");
6,840 ✔
1204
  int n_cells = std::distance(cell_nodes.begin(), cell_nodes.end());
6,840 ✔
1205

1206
  // Loop over XML cell elements and populate the array.
1207
  model::cells.reserve(n_cells);
6,840 ✔
1208
  for (pugi::xml_node cell_node : node.children("cell")) {
37,012 ✔
1209
    model::cells.push_back(make_unique<CSGCell>(cell_node));
30,172 ✔
1210
  }
1211

1212
  // Fill the cell map.
1213
  for (int i = 0; i < model::cells.size(); i++) {
37,012 ✔
1214
    int32_t id = model::cells[i]->id_;
30,172 !
1215
    auto search = model::cell_map.find(id);
30,172 !
1216
    if (search == model::cell_map.end()) {
30,172 !
1217
      model::cell_map[id] = i;
30,172 ✔
1218
    } else {
1219
      fatal_error(
×
1220
        fmt::format("Two or more cells use the same unique ID: {}", id));
×
1221
    }
1222
  }
1223

1224
  read_dagmc_universes(node);
6,840 ✔
1225

1226
  populate_universes();
6,840 ✔
1227

1228
  // Allocate the cell overlap count if necessary.
1229
  if (settings::check_overlaps) {
6,840 ✔
1230
    model::overlap_check_count.resize(model::cells.size(), 0);
87 ✔
1231
  }
1232

1233
  if (model::cells.size() == 0) {
6,840 !
1234
    fatal_error("No cells were found in the geometry.xml file");
×
1235
  }
1236
}
6,840 ✔
1237

1238
void populate_universes()
6,841 ✔
1239
{
1240
  // Used to map universe index to the index of an implicit complement cell for
1241
  // DAGMC universes
1242
  std::unordered_map<int, int> implicit_comp_cells;
6,841 ✔
1243

1244
  // Populate the Universe vector and map.
1245
  for (int index_cell = 0; index_cell < model::cells.size(); index_cell++) {
37,189 ✔
1246
    int32_t uid = model::cells[index_cell]->universe_;
30,348 ✔
1247
    auto it = model::universe_map.find(uid);
30,348 ✔
1248
    if (it == model::universe_map.end()) {
30,348 ✔
1249
      model::universes.push_back(make_unique<Universe>());
38,510 ✔
1250
      model::universes.back()->id_ = uid;
19,255 ✔
1251
      model::universes.back()->cells_.push_back(index_cell);
19,255 ✔
1252
      model::universe_map[uid] = model::universes.size() - 1;
19,255 ✔
1253
    } else {
1254
#ifdef OPENMC_DAGMC_ENABLED
1255
      // Skip implicit complement cells for now
1256
      Universe* univ = model::universes[it->second].get();
1,452 !
1257
      DAGUniverse* dag_univ = dynamic_cast<DAGUniverse*>(univ);
1,452 !
1258
      if (dag_univ && (dag_univ->implicit_complement_idx() == index_cell)) {
1,452 ✔
1259
        implicit_comp_cells[it->second] = index_cell;
40 ✔
1260
        continue;
40 ✔
1261
      }
1262
#endif
1263

1264
      model::universes[it->second]->cells_.push_back(index_cell);
11,053 ✔
1265
    }
1266
  }
1267

1268
  // Add DAGUniverse implicit complement cells last
1269
  for (const auto& it : implicit_comp_cells) {
6,881 ✔
UNCOV
1270
    int index_univ = it.first;
40 ✔
UNCOV
1271
    int index_cell = it.second;
40 ✔
UNCOV
1272
    model::universes[index_univ]->cells_.push_back(index_cell);
40 !
1273
  }
1274

1275
  model::universes.shrink_to_fit();
6,841 ✔
1276
}
6,841 ✔
1277

1278
//==============================================================================
1279
// C-API functions
1280
//==============================================================================
1281

1282
extern "C" int openmc_cell_get_fill(
199 ✔
1283
  int32_t index, int* type, int32_t** indices, int32_t* n)
1284
{
1285
  if (index >= 0 && index < model::cells.size()) {
199 !
1286
    Cell& c {*model::cells[index]};
199 ✔
1287
    *type = static_cast<int>(c.type_);
199 ✔
1288
    if (c.type_ == Fill::MATERIAL) {
199 ✔
1289
      *indices = c.material_.data();
191 ✔
1290
      *n = c.material_.size();
191 ✔
1291
    } else {
1292
      *indices = &c.fill_;
8 ✔
1293
      *n = 1;
8 ✔
1294
    }
1295
  } else {
1296
    set_errmsg("Index in cells array is out of bounds.");
×
1297
    return OPENMC_E_OUT_OF_BOUNDS;
×
1298
  }
1299
  return 0;
1300
}
1301

1302
extern "C" int openmc_cell_set_fill(
8 ✔
1303
  int32_t index, int type, int32_t n, const int32_t* indices)
1304
{
1305
  Fill filltype = static_cast<Fill>(type);
8 ✔
1306
  if (index >= 0 && index < model::cells.size()) {
8 !
1307
    Cell& c {*model::cells[index]};
8 !
1308
    if (filltype == Fill::MATERIAL) {
8 !
1309
      c.type_ = Fill::MATERIAL;
8 ✔
1310
      c.material_.clear();
8 !
1311
      for (int i = 0; i < n; i++) {
16 ✔
1312
        int i_mat = indices[i];
8 ✔
1313
        if (i_mat == MATERIAL_VOID) {
8 !
1314
          c.material_.push_back(MATERIAL_VOID);
×
1315
        } else if (i_mat >= 0 && i_mat < model::materials.size()) {
8 !
1316
          c.material_.push_back(i_mat);
8 ✔
1317
        } else {
1318
          set_errmsg("Index in materials array is out of bounds.");
×
1319
          return OPENMC_E_OUT_OF_BOUNDS;
×
1320
        }
1321
      }
1322
      c.material_.shrink_to_fit();
8 ✔
1323
    } else if (filltype == Fill::UNIVERSE) {
×
1324
      c.type_ = Fill::UNIVERSE;
×
1325
    } else {
1326
      c.type_ = Fill::LATTICE;
×
1327
    }
1328
  } else {
1329
    set_errmsg("Index in cells array is out of bounds.");
×
1330
    return OPENMC_E_OUT_OF_BOUNDS;
×
1331
  }
1332
  return 0;
1333
}
1334

1335
extern "C" int openmc_cell_set_temperature(
64 ✔
1336
  int32_t index, double T, const int32_t* instance, bool set_contained)
1337
{
1338
  if (index < 0 || index >= model::cells.size()) {
64 !
1339
    set_errmsg("Index in cells array is out of bounds.");
×
1340
    return OPENMC_E_OUT_OF_BOUNDS;
×
1341
  }
1342

1343
  int32_t instance_index = instance ? *instance : -1;
64 ✔
1344
  try {
64 ✔
1345
    model::cells[index]->set_temperature(T, instance_index, set_contained);
64 ✔
1346
  } catch (const std::exception& e) {
×
1347
    set_errmsg(e.what());
×
1348
    return OPENMC_E_UNASSIGNED;
×
1349
  }
×
1350
  return 0;
1351
}
1352

1353
extern "C" int openmc_cell_set_density(
64 ✔
1354
  int32_t index, double density, const int32_t* instance, bool set_contained)
1355
{
1356
  if (index < 0 || index >= model::cells.size()) {
64 !
1357
    set_errmsg("Index in cells array is out of bounds.");
×
1358
    return OPENMC_E_OUT_OF_BOUNDS;
×
1359
  }
1360

1361
  int32_t instance_index = instance ? *instance : -1;
64 ✔
1362
  try {
64 ✔
1363
    model::cells[index]->set_density(density, instance_index, set_contained);
64 ✔
1364
  } catch (const std::exception& e) {
×
1365
    set_errmsg(e.what());
×
1366
    return OPENMC_E_UNASSIGNED;
×
1367
  }
×
1368
  return 0;
1369
}
1370

1371
extern "C" int openmc_cell_get_temperature(
7,003 ✔
1372
  int32_t index, const int32_t* instance, double* T)
1373
{
1374
  if (index < 0 || index >= model::cells.size()) {
7,003 !
1375
    set_errmsg("Index in cells array is out of bounds.");
×
1376
    return OPENMC_E_OUT_OF_BOUNDS;
×
1377
  }
1378

1379
  int32_t instance_index = instance ? *instance : -1;
7,003 ✔
1380
  try {
7,003 ✔
1381
    *T = model::cells[index]->temperature(instance_index);
7,003 ✔
1382
  } catch (const std::exception& e) {
×
1383
    set_errmsg(e.what());
×
1384
    return OPENMC_E_UNASSIGNED;
×
1385
  }
×
1386
  return 0;
7,003 ✔
1387
}
1388

1389
extern "C" int openmc_cell_get_density(
64 ✔
1390
  int32_t index, const int32_t* instance, double* density)
1391
{
1392
  if (index < 0 || index >= model::cells.size()) {
64 !
1393
    set_errmsg("Index in cells array is out of bounds.");
×
1394
    return OPENMC_E_OUT_OF_BOUNDS;
×
1395
  }
1396

1397
  int32_t instance_index = instance ? *instance : -1;
64 ✔
1398
  try {
64 ✔
1399
    if (model::cells[index]->type_ != Fill::MATERIAL) {
64 !
1400
      fatal_error(
×
1401
        fmt::format("Cell {}, instance {} is not filled with a material.",
×
1402
          model::cells[index]->id_, instance_index));
×
1403
    }
1404

1405
    int32_t mat_index = model::cells[index]->material(instance_index);
64 !
1406
    if (mat_index == MATERIAL_VOID) {
64 !
1407
      *density = 0.0;
×
1408
    } else {
1409
      *density = model::cells[index]->density_mult(instance_index) *
64 ✔
1410
                 model::materials[mat_index]->density_gpcc();
128 !
1411
    }
1412
  } catch (const std::exception& e) {
×
1413
    set_errmsg(e.what());
×
1414
    return OPENMC_E_UNASSIGNED;
×
1415
  }
×
1416
  return 0;
1417
}
1418

1419
//! Get the bounding box of a cell
1420
extern "C" int openmc_cell_bounding_box(
48 ✔
1421
  const int32_t index, double* llc, double* urc)
1422
{
1423

1424
  BoundingBox bbox;
48 ✔
1425

1426
  const auto& c = model::cells[index];
48 ✔
1427
  bbox = c->bounding_box();
48 ✔
1428

1429
  // set lower left corner values
1430
  llc[0] = bbox.min.x;
48 ✔
1431
  llc[1] = bbox.min.y;
48 ✔
1432
  llc[2] = bbox.min.z;
48 ✔
1433

1434
  // set upper right corner values
1435
  urc[0] = bbox.max.x;
48 ✔
1436
  urc[1] = bbox.max.y;
48 ✔
1437
  urc[2] = bbox.max.z;
48 ✔
1438

1439
  return 0;
48 ✔
1440
}
1441

1442
//! Get the name of a cell
1443
extern "C" int openmc_cell_get_name(int32_t index, const char** name)
317 ✔
1444
{
1445
  if (index < 0 || index >= model::cells.size()) {
317 !
1446
    set_errmsg("Index in cells array is out of bounds.");
×
1447
    return OPENMC_E_OUT_OF_BOUNDS;
×
1448
  }
1449

1450
  *name = model::cells[index]->name().data();
317 ✔
1451

1452
  return 0;
317 ✔
1453
}
1454

1455
//! Set the name of a cell
1456
extern "C" int openmc_cell_set_name(int32_t index, const char* name)
8 ✔
1457
{
1458
  if (index < 0 || index >= model::cells.size()) {
8 !
1459
    set_errmsg("Index in cells array is out of bounds.");
×
1460
    return OPENMC_E_OUT_OF_BOUNDS;
×
1461
  }
1462

1463
  model::cells[index]->set_name(name);
16 ✔
1464

1465
  return 0;
8 ✔
1466
}
1467

1468
//==============================================================================
1469
//! Define a containing (parent) cell
1470
//==============================================================================
1471

1472
//! Used to locate a universe fill in the geometry
1473
struct ParentCell {
1474
  bool operator==(const ParentCell& other) const
90 ✔
1475
  {
1476
    return cell_index == other.cell_index &&
90 !
1477
           lattice_index == other.lattice_index;
90 !
1478
  }
1479

1480
  bool operator<(const ParentCell& other) const
1481
  {
1482
    return cell_index < other.cell_index ||
1483
           (cell_index == other.cell_index &&
1484
             lattice_index < other.lattice_index);
1485
  }
1486

1487
  int64_t cell_index;
1488
  int64_t lattice_index;
1489
};
1490

1491
//! Structure used to insert ParentCell into hashed STL data structures
1492
struct ParentCellHash {
1493
  std::size_t operator()(const ParentCell& p) const
521 ✔
1494
  {
1495
    return 4096 * p.cell_index + p.lattice_index;
521 !
1496
  }
1497
};
1498

1499
//! Used to manage a traversal stack when locating parent cells of a cell
1500
//! instance in the model
1501
struct ParentCellStack {
101 ✔
1502

1503
  //! push method that adds to the parent_cells visited cells for this search
1504
  //! universe
1505
  void push(int32_t search_universe, const ParentCell& pc)
70 ✔
1506
  {
1507
    parent_cells_.push_back(pc);
70 ✔
1508
    // add parent cell to the set of cells we've visited for this search
1509
    // universe
1510
    visited_cells_[search_universe].insert(pc);
70 ✔
1511
  }
70 ✔
1512

1513
  //! removes the last parent_cell and clears the visited cells for the popped
1514
  //! cell's universe
1515
  void pop()
50 ✔
1516
  {
1517
    visited_cells_[this->current_univ()].clear();
50 ✔
1518
    parent_cells_.pop_back();
50 ✔
1519
  }
50 ✔
1520

1521
  //! checks whether or not the parent cell has been visited already for this
1522
  //! search universe
1523
  bool visited(int32_t search_universe, const ParentCell& parent_cell)
451 ✔
1524
  {
1525
    return visited_cells_[search_universe].count(parent_cell) != 0;
451 ✔
1526
  }
1527

1528
  //! return the next universe to search for a parent cell
1529
  int32_t current_univ() const
50 ✔
1530
  {
1531
    return model::cells[parent_cells_.back().cell_index]->universe_;
50 ✔
1532
  }
1533

1534
  //! indicates whether nor not parent cells are present on the stack
1535
  bool empty() const { return parent_cells_.empty(); }
50 ✔
1536

1537
  //! compute an instance for the provided distribcell index
1538
  int32_t compute_instance(int32_t distribcell_index) const
151 ✔
1539
  {
1540
    if (distribcell_index == C_NONE)
151 ✔
1541
      return 0;
1542

1543
    int32_t instance = 0;
80 ✔
1544
    for (const auto& parent_cell : this->parent_cells_) {
150 ✔
1545
      auto& cell = model::cells[parent_cell.cell_index];
70 !
1546
      if (cell->type_ == Fill::UNIVERSE) {
70 !
1547
        instance += cell->offset_[distribcell_index];
×
1548
      } else if (cell->type_ == Fill::LATTICE) {
70 !
1549
        auto& lattice = model::lattices[cell->fill_];
70 ✔
1550
        instance +=
70 ✔
1551
          lattice->offset(distribcell_index, parent_cell.lattice_index);
70 ✔
1552
      }
1553
    }
1554
    return instance;
1555
  }
1556

1557
  // Accessors
1558
  vector<ParentCell>& parent_cells() { return parent_cells_; }
101 ✔
1559
  const vector<ParentCell>& parent_cells() const { return parent_cells_; }
1560

1561
  // Data Members
1562
  vector<ParentCell> parent_cells_;
1563
  std::unordered_map<int32_t, std::unordered_set<ParentCell, ParentCellHash>>
1564
    visited_cells_;
1565
};
1566

1567
vector<ParentCell> Cell::find_parent_cells(
×
1568
  int32_t instance, const Position& r) const
1569
{
1570

1571
  // create a temporary particle
1572
  GeometryState dummy_particle {};
×
1573
  dummy_particle.r() = r;
×
1574
  dummy_particle.u() = {0., 0., 1.};
×
1575

1576
  return find_parent_cells(instance, dummy_particle);
×
1577
}
×
1578

1579
vector<ParentCell> Cell::find_parent_cells(
×
1580
  int32_t instance, GeometryState& p) const
1581
{
1582
  // look up the particle's location
1583
  exhaustive_find_cell(p);
×
1584
  const auto& coords = p.coord();
×
1585

1586
  // build a parent cell stack from the particle coordinates
1587
  ParentCellStack stack;
×
1588
  bool cell_found = false;
×
1589
  for (auto it = coords.begin(); it != coords.end(); it++) {
×
1590
    const auto& coord = *it;
×
1591
    const auto& cell = model::cells[coord.cell()];
×
1592
    // if the cell at this level matches the current cell, stop adding to the
1593
    // stack
1594
    if (coord.cell() == model::cell_map[this->id_]) {
×
1595
      cell_found = true;
1596
      break;
1597
    }
1598

1599
    // if filled with a lattice, get the lattice index from the next
1600
    // level in the coordinates to push to the stack
1601
    int lattice_idx = C_NONE;
×
1602
    if (cell->type_ == Fill::LATTICE) {
×
1603
      const auto& next_coord = *(it + 1);
×
1604
      lattice_idx = model::lattices[next_coord.lattice()]->get_flat_index(
×
1605
        next_coord.lattice_index());
1606
    }
1607
    stack.push(coord.universe(), {coord.cell(), lattice_idx});
×
1608
  }
1609

1610
  // if this loop finished because the cell was found and
1611
  // the instance matches the one requested in the call
1612
  // we have the correct path and can return the stack
1613
  if (cell_found &&
×
1614
      stack.compute_instance(this->distribcell_index_) == instance) {
×
1615
    return stack.parent_cells();
×
1616
  }
1617

1618
  // fall back on an exhaustive search for the cell's parents
1619
  return exhaustive_find_parent_cells(instance);
×
1620
}
×
1621

1622
vector<ParentCell> Cell::exhaustive_find_parent_cells(int32_t instance) const
101 ✔
1623
{
1624
  ParentCellStack stack;
101 ✔
1625
  // start with this cell's universe
1626
  int32_t prev_univ_idx;
101 ✔
1627
  int32_t univ_idx = this->universe_;
101 ✔
1628

1629
  while (true) {
151 ✔
1630
    prev_univ_idx = univ_idx;
151 ✔
1631

1632
    // search for a cell that is filled w/ this universe
1633
    for (const auto& cell : model::cells) {
1,134 ✔
1634
      // if this is a material-filled cell, move on
1635
      if (cell->type_ == Fill::MATERIAL)
1,053 ✔
1636
        continue;
592 ✔
1637

1638
      if (cell->type_ == Fill::UNIVERSE) {
461 ✔
1639
        // if this is in the set of cells previously visited for this universe,
1640
        // move on
1641
        if (stack.visited(univ_idx, {model::cell_map[cell->id_], C_NONE}))
291 !
1642
          continue;
×
1643

1644
        // if this cell contains the universe we're searching for, add it to the
1645
        // stack
1646
        if (cell->fill_ == univ_idx) {
291 !
1647
          stack.push(univ_idx, {model::cell_map[cell->id_], C_NONE});
×
1648
          univ_idx = cell->universe_;
×
1649
        }
1650
      } else if (cell->type_ == Fill::LATTICE) {
170 !
1651
        // retrieve the lattice and lattice universes
1652
        const auto& lattice = model::lattices[cell->fill_];
170 ✔
1653
        const auto& lattice_univs = lattice->universes_;
170 ✔
1654

1655
        // start search for universe
1656
        auto lat_it = lattice_univs.begin();
170 ✔
1657
        while (true) {
350 ✔
1658
          // find the next lattice cell with this universe
1659
          lat_it = std::find(lat_it, lattice_univs.end(), univ_idx);
260 ✔
1660
          if (lat_it == lattice_univs.end())
260 ✔
1661
            break;
1662

1663
          int lattice_idx = lat_it - lattice_univs.begin();
160 ✔
1664

1665
          // move iterator forward one to avoid finding the same entry
1666
          lat_it++;
160 ✔
1667
          if (stack.visited(
320 ✔
1668
                univ_idx, {model::cell_map[cell->id_], lattice_idx}))
160 ✔
1669
            continue;
90 ✔
1670

1671
          // add this cell and lattice index to the stack and exit loop
1672
          stack.push(univ_idx, {model::cell_map[cell->id_], lattice_idx});
70 ✔
1673
          univ_idx = cell->universe_;
70 ✔
1674
          break;
70 ✔
1675
        }
90 ✔
1676
      }
1677
      // if we've updated the universe, break
1678
      if (prev_univ_idx != univ_idx)
461 ✔
1679
        break;
1680
    } // end cell loop search for universe
1681

1682
    // if we're at the top of the geometry and the instance matches, we're done
1683
    if (univ_idx == model::root_universe &&
167 !
1684
        stack.compute_instance(this->distribcell_index_) == instance)
151 ✔
1685
      break;
1686

1687
    // if there is no match on the original cell's universe, report an error
1688
    if (univ_idx == this->universe_) {
50 !
1689
      fatal_error(
×
1690
        fmt::format("Could not find the parent cells for cell {}, instance {}.",
×
1691
          this->id_, instance));
×
1692
    }
1693

1694
    // if we don't find a suitable update, adjust the stack and continue
1695
    if (univ_idx == model::root_universe || univ_idx == prev_univ_idx) {
50 !
1696
      stack.pop();
50 ✔
1697
      univ_idx = stack.empty() ? this->universe_ : stack.current_univ();
50 !
1698
    }
1699

1700
  } // end while
1701

1702
  // reverse the stack so the highest cell comes first
1703
  std::reverse(stack.parent_cells().begin(), stack.parent_cells().end());
101 ✔
1704
  return stack.parent_cells();
202 ✔
1705
}
101 ✔
1706

1707
std::unordered_map<int32_t, vector<int32_t>> Cell::get_contained_cells(
131 ✔
1708
  int32_t instance, Position* hint) const
1709
{
1710
  std::unordered_map<int32_t, vector<int32_t>> contained_cells;
131 ✔
1711

1712
  // if this is a material-filled cell it has no contained cells
1713
  if (this->type_ == Fill::MATERIAL)
131 ✔
1714
    return contained_cells;
1715

1716
  // find the pathway through the geometry to this cell
1717
  vector<ParentCell> parent_cells;
101 !
1718

1719
  // if a positional hint is provided, attempt to do a fast lookup
1720
  // of the parent cells
1721
  parent_cells = hint ? find_parent_cells(instance, *hint)
101 !
1722
                      : exhaustive_find_parent_cells(instance);
101 ✔
1723

1724
  // if this cell is filled w/ a material, it contains no other cells
1725
  if (type_ != Fill::MATERIAL) {
101 !
1726
    this->get_contained_cells_inner(contained_cells, parent_cells);
101 ✔
1727
  }
1728

1729
  return contained_cells;
101 ✔
1730
}
131 ✔
1731

1732
//! Get all cells within this cell
1733
void Cell::get_contained_cells_inner(
92,153 ✔
1734
  std::unordered_map<int32_t, vector<int32_t>>& contained_cells,
1735
  vector<ParentCell>& parent_cells) const
1736
{
1737

1738
  // filled by material, determine instance based on parent cells
1739
  if (type_ == Fill::MATERIAL) {
92,153 ✔
1740
    int instance = 0;
91,462 ✔
1741
    if (this->distribcell_index_ >= 0) {
91,462 !
1742
      for (auto& parent_cell : parent_cells) {
274,384 ✔
1743
        auto& cell = model::cells[parent_cell.cell_index];
182,922 ✔
1744
        if (cell->type_ == Fill::UNIVERSE) {
182,922 ✔
1745
          instance += cell->offset_[distribcell_index_];
89,462 ✔
1746
        } else if (cell->type_ == Fill::LATTICE) {
93,460 !
1747
          auto& lattice = model::lattices[cell->fill_];
93,460 ✔
1748
          instance += lattice->offset(
93,460 ✔
1749
            this->distribcell_index_, parent_cell.lattice_index);
93,460 ✔
1750
        }
1751
      }
1752
    }
1753
    // add entry to contained cells
1754
    contained_cells[model::cell_map[id_]].push_back(instance);
91,462 ✔
1755
    // filled with universe, add the containing cell to the parent cells
1756
    // and recurse
1757
  } else if (type_ == Fill::UNIVERSE) {
691 ✔
1758
    parent_cells.push_back({model::cell_map[id_], -1});
591 ✔
1759
    auto& univ = model::universes[fill_];
591 ✔
1760
    for (auto cell_index : univ->cells_) {
3,703 ✔
1761
      auto& cell = model::cells[cell_index];
3,112 ✔
1762
      cell->get_contained_cells_inner(contained_cells, parent_cells);
3,112 ✔
1763
    }
1764
    parent_cells.pop_back();
591 ✔
1765
    // filled with a lattice, visit each universe in the lattice
1766
    // with a recursive call to collect the cell instances
1767
  } else if (type_ == Fill::LATTICE) {
100 !
1768
    auto& lattice = model::lattices[fill_];
100 ✔
1769
    for (auto i = lattice->begin(); i != lattice->end(); ++i) {
88,620 ✔
1770
      auto& univ = model::universes[*i];
88,520 ✔
1771
      parent_cells.push_back({model::cell_map[id_], i.indx_});
88,520 ✔
1772
      for (auto cell_index : univ->cells_) {
177,460 ✔
1773
        auto& cell = model::cells[cell_index];
88,940 ✔
1774
        cell->get_contained_cells_inner(contained_cells, parent_cells);
88,940 ✔
1775
      }
1776
      parent_cells.pop_back();
88,520 ✔
1777
    }
1778
  }
1779
}
92,153 ✔
1780

1781
//! Return the index in the cells array of a cell with a given ID
1782
extern "C" int openmc_get_cell_index(int32_t id, int32_t* index)
768 ✔
1783
{
1784
  auto it = model::cell_map.find(id);
768 ✔
1785
  if (it != model::cell_map.end()) {
768 ✔
1786
    *index = it->second;
760 ✔
1787
    return 0;
760 ✔
1788
  } else {
1789
    set_errmsg("No cell exists with ID=" + std::to_string(id) + ".");
16 ✔
1790
    return OPENMC_E_INVALID_ID;
8 ✔
1791
  }
1792
}
1793

1794
//! Return the ID of a cell
1795
extern "C" int openmc_cell_get_id(int32_t index, int32_t* id)
801,776 ✔
1796
{
1797
  if (index >= 0 && index < model::cells.size()) {
801,776 !
1798
    *id = model::cells[index]->id_;
801,776 ✔
1799
    return 0;
801,776 ✔
1800
  } else {
1801
    set_errmsg("Index in cells array is out of bounds.");
×
1802
    return OPENMC_E_OUT_OF_BOUNDS;
×
1803
  }
1804
}
1805

1806
//! Set the ID of a cell
1807
extern "C" int openmc_cell_set_id(int32_t index, int32_t id)
16 ✔
1808
{
1809
  if (index >= 0 && index < model::cells.size()) {
16 !
1810
    model::cells[index]->id_ = id;
16 ✔
1811
    model::cell_map[id] = index;
16 ✔
1812
    return 0;
16 ✔
1813
  } else {
1814
    set_errmsg("Index in cells array is out of bounds.");
×
1815
    return OPENMC_E_OUT_OF_BOUNDS;
×
1816
  }
1817
}
1818

1819
//! Return the translation vector of a cell
1820
extern "C" int openmc_cell_get_translation(int32_t index, double xyz[])
40 ✔
1821
{
1822
  if (index >= 0 && index < model::cells.size()) {
40 !
1823
    auto& cell = model::cells[index];
40 ✔
1824
    xyz[0] = cell->translation_.x;
40 ✔
1825
    xyz[1] = cell->translation_.y;
40 ✔
1826
    xyz[2] = cell->translation_.z;
40 ✔
1827
    return 0;
40 ✔
1828
  } else {
1829
    set_errmsg("Index in cells array is out of bounds.");
×
1830
    return OPENMC_E_OUT_OF_BOUNDS;
×
1831
  }
1832
}
1833

1834
//! Set the translation vector of a cell
1835
extern "C" int openmc_cell_set_translation(int32_t index, const double xyz[])
40 ✔
1836
{
1837
  if (index >= 0 && index < model::cells.size()) {
40 !
1838
    if (model::cells[index]->fill_ == C_NONE) {
40 ✔
1839
      set_errmsg(fmt::format("Cannot apply a translation to cell {}"
8 ✔
1840
                             " because it is not filled with another universe",
1841
        index));
1842
      return OPENMC_E_GEOMETRY;
8 ✔
1843
    }
1844
    model::cells[index]->translation_ = Position(xyz);
32 ✔
1845
    return 0;
32 ✔
1846
  } else {
1847
    set_errmsg("Index in cells array is out of bounds.");
×
1848
    return OPENMC_E_OUT_OF_BOUNDS;
×
1849
  }
1850
}
1851

1852
//! Return the rotation matrix of a cell
1853
extern "C" int openmc_cell_get_rotation(int32_t index, double rot[], size_t* n)
40 ✔
1854
{
1855
  if (index >= 0 && index < model::cells.size()) {
40 !
1856
    auto& cell = model::cells[index];
40 ✔
1857
    *n = cell->rotation_.size();
40 ✔
1858
    std::memcpy(rot, cell->rotation_.data(), *n * sizeof(cell->rotation_[0]));
40 ✔
1859
    return 0;
40 ✔
1860
  } else {
1861
    set_errmsg("Index in cells array is out of bounds.");
×
1862
    return OPENMC_E_OUT_OF_BOUNDS;
×
1863
  }
1864
}
1865

1866
//! Set the flattened rotation matrix of a cell
1867
extern "C" int openmc_cell_set_rotation(
48 ✔
1868
  int32_t index, const double rot[], size_t rot_len)
1869
{
1870
  if (index >= 0 && index < model::cells.size()) {
48 !
1871
    if (model::cells[index]->fill_ == C_NONE) {
48 ✔
1872
      set_errmsg(fmt::format("Cannot apply a rotation to cell {}"
8 ✔
1873
                             " because it is not filled with another universe",
1874
        index));
1875
      return OPENMC_E_GEOMETRY;
8 ✔
1876
    }
1877
    std::vector<double> vec_rot(rot, rot + rot_len);
40 ✔
1878
    model::cells[index]->set_rotation(vec_rot);
40 ✔
1879
    return 0;
40 ✔
1880
  } else {
48 ✔
1881
    set_errmsg("Index in cells array is out of bounds.");
×
1882
    return OPENMC_E_OUT_OF_BOUNDS;
×
1883
  }
1884
}
1885

1886
//! Get the number of instances of the requested cell
1887
extern "C" int openmc_cell_get_num_instances(
56 ✔
1888
  int32_t index, int32_t* num_instances)
1889
{
1890
  if (index < 0 || index >= model::cells.size()) {
56 !
1891
    set_errmsg("Index in cells array is out of bounds.");
×
1892
    return OPENMC_E_OUT_OF_BOUNDS;
×
1893
  }
1894
  *num_instances = model::cells[index]->n_instances();
56 ✔
1895
  return 0;
56 ✔
1896
}
1897

1898
//! Extend the cells array by n elements
1899
extern "C" int openmc_extend_cells(
16 ✔
1900
  int32_t n, int32_t* index_start, int32_t* index_end)
1901
{
1902
  if (index_start)
16 !
1903
    *index_start = model::cells.size();
16 ✔
1904
  if (index_end)
16 !
1905
    *index_end = model::cells.size() + n - 1;
×
1906
  for (int32_t i = 0; i < n; i++) {
32 ✔
1907
    model::cells.push_back(make_unique<CSGCell>());
16 ✔
1908
  }
1909
  return 0;
16 ✔
1910
}
1911

1912
extern "C" int cells_size()
72 ✔
1913
{
1914
  return model::cells.size();
72 ✔
1915
}
1916

1917
} // namespace openmc
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc