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

openmc-dev / openmc / 29945306817

22 Jul 2026 06:07PM UTC coverage: 80.939% (-0.5%) from 81.4%
29945306817

Pull #3934

github

web-flow
Merge 2f1f1b8a1 into 54b661d39
Pull Request #3934: Fix virtual surface crossing

17514 of 25109 branches covered (69.75%)

Branch coverage included in aggregate %.

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

651 existing lines in 36 files now uncovered.

58561 of 68881 relevant lines covered (85.02%)

37181215.7 hits per line

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

75.15
/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
8,640✔
44
{
45
  return model::universes[universe_]->n_instances_;
8,640✔
46
}
47

48
void Cell::set_rotation(const vector<double>& rot)
238✔
49
{
50
  if (fill_ == C_NONE) {
238!
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) {
238!
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();
238✔
62
  rotation_.reserve(rot.size() == 9 ? 9 : 12);
476!
63
  if (rot.size() == 3) {
238!
64
    double phi = -rot[0] * PI / 180.0;
238✔
65
    double theta = -rot[1] * PI / 180.0;
238✔
66
    double psi = -rot[2] * PI / 180.0;
238✔
67
    rotation_.push_back(std::cos(theta) * std::cos(psi));
238✔
68
    rotation_.push_back(-std::cos(phi) * std::sin(psi) +
238✔
69
                        std::sin(phi) * std::sin(theta) * std::cos(psi));
238✔
70
    rotation_.push_back(std::sin(phi) * std::sin(psi) +
238✔
71
                        std::cos(phi) * std::sin(theta) * std::cos(psi));
238✔
72
    rotation_.push_back(std::cos(theta) * std::sin(psi));
238✔
73
    rotation_.push_back(std::cos(phi) * std::cos(psi) +
238✔
74
                        std::sin(phi) * std::sin(theta) * std::sin(psi));
238✔
75
    rotation_.push_back(-std::sin(phi) * std::cos(psi) +
238✔
76
                        std::cos(phi) * std::sin(theta) * std::sin(psi));
238✔
77
    rotation_.push_back(-std::sin(theta));
238✔
78
    rotation_.push_back(std::sin(phi) * std::cos(theta));
238✔
79
    rotation_.push_back(std::cos(phi) * std::cos(theta));
238✔
80

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

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

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

104
double Cell::density_mult(int32_t instance) const
2,147,483,647✔
105
{
106
  if (instance >= 0) {
2,147,483,647✔
107
    return density_mult_.size() == 1 ? density_mult_.at(0)
2,147,483,647✔
108
                                     : density_mult_.at(instance);
2,746,350✔
109
  } else {
110
    return density_mult_[0];
42✔
111
  }
112
}
113

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

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

123
void Cell::set_temperature(double T, int32_t instance, bool set_contained)
5,458✔
124
{
125
  if (settings::temperature_method == TemperatureMethod::INTERPOLATION) {
5,458!
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) {
5,458✔
140
    if (instance >= 0) {
5,442✔
141
      // If temperature vector is not big enough, resize it first
142
      if (sqrtkT_.size() != n_instances())
5,400✔
143
        sqrtkT_.resize(n_instances(), sqrtkT_[0]);
24✔
144

145
      // Set temperature for the corresponding instance
146
      sqrtkT_.at(instance) = std::sqrt(K_BOLTZMANN * T);
5,400✔
147
    } else {
148
      // Set temperature for all instances
149
      for (auto& T_ : sqrtkT_) {
84✔
150
        T_ = std::sqrt(K_BOLTZMANN * T);
42✔
151
      }
152
    }
153
  } else {
154
    if (!set_contained) {
16!
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);
16✔
162
    for (const auto& entry : contained_cells) {
64✔
163
      auto& cell = model::cells[entry.first];
48!
164
      assert(cell->type_ == Fill::MATERIAL);
48!
165
      auto& instances = entry.second;
48✔
166
      for (auto instance : instances) {
168✔
167
        cell->set_temperature(T, instance);
120✔
168
      }
169
    }
170
  }
16✔
171
}
5,458✔
172

173
void Cell::set_density(double density, int32_t instance, bool set_contained)
188✔
174
{
175
  if (type_ != Fill::MATERIAL && !set_contained) {
188!
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) {
188✔
183
    const int32_t mat_index = material(instance);
180!
184
    if (mat_index == MATERIAL_VOID)
180!
185
      return;
186

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

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

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

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

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

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

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

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

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

245
  // Ensure number of temperatures makes sense
246
  auto n_temps = temps.size();
138✔
247
  if (n_temps > 1 && n_temps != n_instances()) {
138!
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();
138✔
255
  sqrtkT_.resize(temps.size());
138✔
256
  for (int64_t i = 0; i < temps.size(); ++i) {
5,412✔
257
    this->set_temperature(temps[i], i);
5,274✔
258
  }
259

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

265
    // Ensure number of densities makes sense
266
    auto n_density = density.size();
108!
267
    if (n_density > 1 && n_density != n_instances()) {
108!
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) {
216✔
275
      this->set_density(density[i], i);
108✔
276
    }
277
  }
108✔
278

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

282
void Cell::to_hdf5(hid_t cell_group) const
13,268✔
283
{
284

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

288
  if (!name_.empty()) {
13,268✔
289
    write_string(group, "name", name_, false);
3,936✔
290
  }
291

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

294
  to_hdf5_inner(group);
13,268✔
295

296
  // Write fill information.
297
  if (type_ == Fill::MATERIAL) {
13,268✔
298
    write_dataset(group, "fill_type", "material");
10,234✔
299
    std::vector<int32_t> mat_ids;
10,234✔
300
    for (auto i_mat : material_) {
21,180✔
301
      if (i_mat != MATERIAL_VOID) {
10,946✔
302
        mat_ids.push_back(model::materials[i_mat]->id_);
9,522✔
303
      } else {
304
        mat_ids.push_back(MATERIAL_VOID);
1,424✔
305
      }
306
    }
307
    if (mat_ids.size() == 1) {
10,234✔
308
      write_dataset(group, "material", mat_ids[0]);
10,130✔
309
    } else {
310
      write_dataset(group, "material", mat_ids);
104✔
311
    }
312

313
    std::vector<double> temps;
10,234✔
314
    for (auto sqrtkT_val : sqrtkT_)
26,832✔
315
      temps.push_back(sqrtkT_val * sqrtkT_val / K_BOLTZMANN);
16,598✔
316
    write_dataset(group, "temperature", temps);
10,234✔
317

318
    write_dataset(group, "density_mult", density_mult_);
10,234✔
319

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

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

340
  close_group(group);
13,268✔
341
}
13,268✔
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)
12,400✔
348
{
349
  vector<std::string> mats {
12,400✔
350
    get_node_array<std::string>(node, "material", true)};
12,400✔
351
  if (mats.empty()) {
12,400!
352
    fatal_error(fmt::format(
×
353
      "An empty material element was specified for cell {}", cell_id));
354
  }
355
  vector<int32_t> material;
12,400✔
356
  material.reserve(mats.size());
12,400✔
357
  for (const auto& mat : mats) {
25,530✔
358
    if (mat == "void") {
13,130✔
359
      material.push_back(MATERIAL_VOID);
1,692✔
360
    } else {
361
      material.push_back(std::stoi(mat));
11,438✔
362
    }
363
  }
364
  return material;
12,400✔
365
}
12,400✔
366

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

383
vector<double> parse_cell_density_xml(pugi::xml_node node, int32_t cell_id)
40✔
384
{
385
  auto densities = get_node_array<double>(node, "density");
40✔
386
  if (densities.empty()) {
40!
387
    fatal_error(fmt::format(
×
388
      "An empty density element was specified for cell {}", cell_id));
389
  }
390
  for (auto rho : densities) {
656✔
391
    if (rho <= 0) {
616!
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;
40✔
398
}
×
399

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

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

412
  if (check_for_node(cell_node, "name")) {
16,284✔
413
    name_ = get_node_value(cell_node, "name");
4,944✔
414
  }
415

416
  if (check_for_node(cell_node, "universe")) {
16,284✔
417
    universe_ = std::stoi(get_node_value(cell_node, "universe"));
31,276✔
418
  } else {
419
    universe_ = 0;
646✔
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");
16,284✔
424
  bool material_present = check_for_node(cell_node, "material");
16,284✔
425
  if (!(fill_present || material_present)) {
16,284!
426
    fatal_error(
×
427
      fmt::format("Neither material nor fill was specified for cell {}", id_));
×
428
  }
429
  if (fill_present && material_present) {
16,284!
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) {
16,284✔
436
    fill_ = std::stoi(get_node_value(cell_node, "fill"));
7,768✔
437
    if (fill_ == universe_) {
3,884!
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;
12,400✔
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) {
16,284✔
450
    material_ = parse_cell_material_xml(cell_node, id_);
12,400✔
451
  }
452

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

458
    // Make sure this is a material-filled cell.
459
    if (material_.size() == 0) {
230!
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,036✔
468
      T = std::sqrt(K_BOLTZMANN * T);
806✔
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")) {
16,284✔
478
    density_mult_ = parse_cell_density_xml(cell_node, id_);
40✔
479
    density_mult_.shrink_to_fit();
40✔
480

481
    // Make sure this is a material-filled cell.
482
    if (material_.size() == 0) {
40!
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_) {
80✔
491
      if (mat_id == MATERIAL_VOID) {
40!
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;
16,284✔
503
  if (check_for_node(cell_node, "region")) {
16,284✔
504
    region_spec = get_node_value(cell_node, "region");
14,580✔
505
  }
506

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

512
  // Read the translation vector.
513
  if (check_for_node(cell_node, "translation")) {
16,284✔
514
    if (fill_ == C_NONE) {
1,364!
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,364✔
521
    if (xyz.size() != 3) {
1,364!
522
      fatal_error(
×
523
        fmt::format("Non-3D translation vector applied to cell {}", id_));
×
524
    }
525
    translation_ = xyz;
1,364✔
526
  }
1,364✔
527

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

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

537
void CSGCell::to_hdf5_inner(hid_t group_id) const
13,268✔
538
{
539
  write_string(group_id, "geom_type", "csg", false);
13,268✔
540
  write_string(group_id, "region", region_.str(), false);
13,268✔
541
}
13,268✔
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)
16,338✔
583
{
584
  // Check if region_spec is not empty.
585
  if (!region_spec.empty()) {
16,338✔
586
    // Parse all halfspaces and operators except for intersection (whitespace).
587
    for (int i = 0; i < region_spec.size();) {
87,552✔
588
      if (region_spec[i] == '(') {
72,918✔
589
        expression_.push_back(OP_LEFT_PAREN);
898✔
590
        i++;
898✔
591

592
      } else if (region_spec[i] == ')') {
72,020✔
593
        expression_.push_back(OP_RIGHT_PAREN);
898✔
594
        i++;
898✔
595

596
      } else if (region_spec[i] == '|') {
71,122✔
597
        expression_.push_back(OP_UNION);
2,322✔
598
        i++;
2,322✔
599

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

604
      } else if (region_spec[i] == '-' || region_spec[i] == '+' ||
116,116!
605
                 std::isdigit(region_spec[i])) {
47,332✔
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;
40,150✔
609
        while (j < region_spec.size() && std::isdigit(region_spec[j])) {
80,562✔
610
          j++;
40,412✔
611
        }
612
        expression_.push_back(std::stoi(region_spec.substr(i, j - i)));
80,300✔
613
        i = j;
40,150✔
614

615
      } else if (std::isspace(region_spec[i])) {
28,634!
616
        i++;
28,634✔
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) {
67,478✔
629
      bool left_compat {
52,844✔
630
        (expression_[i] < OP_UNION) || (expression_[i] == OP_RIGHT_PAREN)};
52,844✔
631
      bool right_compat {(expression_[i + 1] < OP_UNION) ||
52,844✔
632
                         (expression_[i + 1] == OP_LEFT_PAREN) ||
52,844✔
633
                         (expression_[i + 1] == OP_COMPLEMENT)};
3,252✔
634
      if (left_compat && right_compat) {
52,844✔
635
        expression_.insert(expression_.begin() + i + 1, OP_INTERSECTION);
23,194✔
636
      }
637
      i++;
638
    }
639

640
    // Remove complement operators using DeMorgan's laws
641
    auto it = std::find(expression_.begin(), expression_.end(), OP_COMPLEMENT);
14,634✔
642
    while (it != expression_.end()) {
14,650✔
643
      // Erase complement
644
      expression_.erase(it);
16✔
645

646
      // Define stop given left parenthesis or not
647
      auto stop = it;
16✔
648
      if (*it == OP_LEFT_PAREN) {
16!
649
        int depth = 1;
650
        do {
128✔
651
          stop++;
128✔
652
          if (*stop > OP_COMPLEMENT) {
128✔
653
            if (*stop == OP_RIGHT_PAREN) {
16!
654
              depth--;
16✔
655
            } else {
656
              depth++;
×
657
            }
658
          }
659
        } while (depth > 0);
128✔
660
        it++;
16✔
661
      }
662

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

670
    // Convert user IDs to surface indices.
671
    for (auto& r : expression_) {
82,096✔
672
      if (r < OP_UNION) {
67,462✔
673
        const auto& it {model::surface_map.find(abs(r))};
40,150!
674
        if (it == model::surface_map.end()) {
40,150!
675
          throw std::runtime_error {
×
676
            "Invalid surface ID " + std::to_string(abs(r)) +
×
677
            " specified in region for cell " + std::to_string(cell_id) + "."};
×
678
        }
679
        r = (r > 0) ? it->second + 1 : -(it->second + 1);
40,150✔
680
      }
681
    }
682

683
    // Check if this is a simple cell.
684
    simple_ = true;
14,634✔
685
    for (int32_t token : expression_) {
73,904✔
686
      if (token == OP_UNION) {
59,932✔
687
        simple_ = false;
662✔
688
        // Ensure intersections have precedence over unions
689
        enforce_precedence();
662✔
690
        break;
691
      }
692
    }
693

694
    // If this cell is simple, remove all the superfluous operator tokens.
695
    if (simple_) {
14,634✔
696
      for (auto it = expression_.begin(); it != expression_.end(); it++) {
69,512✔
697
        if (*it == OP_INTERSECTION || *it > OP_COMPLEMENT) {
55,540!
698
          expression_.erase(it);
20,784✔
699
          it--;
55,540✔
700
        }
701
      }
702
    }
703
    expression_.shrink_to_fit();
14,634✔
704

705
  } else {
706
    simple_ = true;
1,704✔
707
  }
708
}
16,338✔
709

710
//==============================================================================
711

712
void Region::apply_demorgan(
16✔
713
  vector<int32_t>::iterator start, vector<int32_t>::iterator stop)
714
{
715
  do {
112✔
716
    if (*start < OP_UNION) {
112✔
717
      *start *= -1;
64✔
718
    } else if (*start == OP_UNION) {
48!
719
      *start = OP_INTERSECTION;
×
720
    } else if (*start == OP_INTERSECTION) {
48!
721
      *start = OP_UNION;
48✔
722
    }
723
    start++;
112✔
724
  } while (start < stop);
112✔
725
}
16✔
726

727
//==============================================================================
728
//! Add precedence for infix regions so intersections have higher
729
//! precedence than unions using parentheses.
730
//==============================================================================
731

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

741
  // Add right parenthesis
742
  // While the start iterator is within the bounds of infix
743
  while (start + 1 < expression_.size()) {
232✔
744
    start++;
220✔
745

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

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

792
void Region::enforce_precedence()
662✔
793
{
794
  // Stack tracking the operator type at each depth (0 = no operator seen yet)
795
  vector<int32_t> op_stack = {0};
662✔
796

797
  // Stack tracking where the operator sequence started at each depth
798
  vector<std::size_t> pos_stack = {0};
662✔
799

800
  for (int64_t i = 0; i < expression_.size(); ++i) {
13,134✔
801
    int32_t token = expression_[i];
12,472✔
802

803
    if (token == OP_LEFT_PAREN) {
12,472✔
804
      // Entering a new parenthesis level - push new tracking state
805
      op_stack.push_back(0);
1,004✔
806
      pos_stack.push_back(0);
1,004✔
807
      continue;
1,004✔
808
    } else if (token == OP_RIGHT_PAREN) {
11,468✔
809
      // Exiting a parenthesis level - pop tracking state (keep at least one)
810
      if (op_stack.size() > 1) {
982!
811
        op_stack.pop_back();
982✔
812
        pos_stack.pop_back();
982✔
813
      }
814
      continue;
982✔
815
    }
816

817
    if (token == OP_UNION || token == OP_INTERSECTION) {
10,486✔
818
      if (op_stack.back() == 0) {
4,912✔
819
        // First operator at this depth - record it and its position
820
        op_stack.back() = token;
1,690✔
821
        pos_stack.back() = i;
1,690✔
822
      } else if (token != op_stack.back()) {
3,222✔
823
        // Encountered a different operator at the same depth - need to add
824
        // parentheses to enforce precedence. Intersection has higher
825
        // precedence, so we parenthesize the intersection terms.
826
        if (op_stack.back() == OP_INTERSECTION) {
52✔
827
          add_parentheses(pos_stack.back());
26✔
828
        } else {
829
          add_parentheses(i);
26✔
830
        }
831

832
        // Restart the scan since we modified the expression
833
        i = -1; // Will be incremented to 0 by the for loop
52✔
834
        op_stack = {0};
52✔
835
        pos_stack = {0};
52✔
836
      }
837
    }
838
  }
839
}
662✔
840

841
//==============================================================================
842
//! Convert infix region specification to Reverse Polish Notation (RPN)
843
//!
844
//! This function uses the shunting-yard algorithm.
845
//==============================================================================
846

847
vector<int32_t> Region::generate_postfix(int32_t cell_id) const
24✔
848
{
849
  vector<int32_t> rpn;
24✔
850
  vector<int32_t> stack;
24✔
851

852
  for (int32_t token : expression_) {
540✔
853
    if (token < OP_UNION) {
516✔
854
      // If token is not an operator, add it to output
855
      rpn.push_back(token);
216✔
856
    } else if (token < OP_RIGHT_PAREN) {
300✔
857
      // Regular operators union, intersection, complement
858
      while (stack.size() > 0) {
306✔
859
        int32_t op = stack.back();
252✔
860

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

876
      stack.push_back(token);
192✔
877

878
    } else if (token == OP_LEFT_PAREN) {
108✔
879
      // If the token is a left parenthesis, push it onto the stack
880
      stack.push_back(token);
54✔
881

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

897
      // Pop the left parenthesis.
898
      stack.pop_back();
516✔
899
    }
900
  }
901

902
  while (stack.size() > 0) {
24✔
903
    int32_t op = stack.back();
24!
904

905
    // If the operator is a parenthesis it is mismatched.
906
    if (op >= OP_RIGHT_PAREN) {
24!
907
      fatal_error(fmt::format(
×
908
        "Mismatched parentheses in region specification for cell {}", cell_id));
909
    }
910

911
    rpn.push_back(stack.back());
24✔
912
    stack.pop_back();
48✔
913
  }
914

915
  return rpn;
24✔
916
}
24✔
917

918
//==============================================================================
919

920
std::string Region::str() const
13,322✔
921
{
922
  std::stringstream region_spec {};
13,322✔
923
  if (!expression_.empty()) {
13,322✔
924
    for (int32_t token : expression_) {
51,298✔
925
      if (token == OP_LEFT_PAREN) {
39,318✔
926
        region_spec << " (";
838✔
927
      } else if (token == OP_RIGHT_PAREN) {
38,480✔
928
        region_spec << " )";
838✔
929
      } else if (token == OP_COMPLEMENT) {
37,642!
930
        region_spec << " ~";
×
931
      } else if (token == OP_INTERSECTION) {
37,642✔
932
      } else if (token == OP_UNION) {
35,578✔
933
        region_spec << " |";
2,146✔
934
      } else {
935
        // Note the off-by-one indexing
936
        auto surf_id = model::surfaces[abs(token) - 1]->id_;
33,432✔
937
        region_spec << " " << ((token > 0) ? surf_id : -surf_id);
33,432✔
938
      }
939
    }
940
  }
941
  return region_spec.str();
26,644✔
942
}
13,322✔
943

944
//==============================================================================
945

946
std::pair<double, int32_t> Region::distance(
2,147,483,647✔
947
  Position r, Direction u, int32_t on_surface) const
948
{
949
  if (simple_) {
2,147,483,647✔
950
    return distance_simple(r, u, on_surface);
2,147,483,647✔
951
  } else {
952
    return distance_complex(r, u, on_surface);
99,917,208✔
953
  }
954
}
955

956
//==============================================================================
957

958
std::pair<double, int32_t> Region::distance_simple(
2,147,483,647✔
959
  Position r, Direction u, int32_t on_surface) const
960
{
961
  double min_dist {INFTY};
2,147,483,647✔
962
  int32_t i_surf {std::numeric_limits<int32_t>::max()};
2,147,483,647✔
963

964
  for (int32_t token : expression_) {
×
965
    // Ignore this token if it corresponds to an operator rather than a region.
UNCOV
966
    if (token >= OP_UNION)
✔
967
      continue;
2,147,483,647✔
968

969
    // Calculate the distance to this surface.
970
    // Note the off-by-one indexing
971
    bool coincident {std::abs(token) == std::abs(on_surface)};
2,147,483,647✔
972
    double d {model::surfaces[abs(token) - 1]->distance(r, u, coincident)};
2,147,483,647✔
973

974
    // Check if this distance is the new minimum.
975
    if (d < min_dist) {
2,147,483,647✔
976
      if (min_dist - d >= FP_PRECISION * min_dist) {
2,147,483,647!
977
        min_dist = d;
2,147,483,647✔
978
        i_surf = -token;
2,147,483,647✔
979
      }
980
    }
981
  }
982

983
  return {min_dist, i_surf};
2,147,483,647✔
984
}
985

986
//==============================================================================
987

988
std::pair<double, int32_t> Region::distance_complex(
99,917,208✔
989
  Position r, Direction u, int32_t on_surface) const
990
{
991
  double min_dist;
99,917,208✔
992
  int32_t i_surf;
99,917,208✔
993
  double atleast {-1.0};
99,917,208✔
994
  bool in_region = contains_complex(r, u, on_surface);
99,917,208✔
995

996
  while (true) {
571,008,760✔
997
    min_dist = INFTY;
335,462,984✔
998
    i_surf = std::numeric_limits<int32_t>::max();
335,462,984✔
999

1000
    for (int32_t token : expression_) {
2,147,483,647✔
1001
      // Ignore this token if it corresponds to an operator rather than a
1002
      // region.
1003
      if (token >= OP_UNION)
2,147,483,647✔
1004
        continue;
2,147,483,647✔
1005

1006
      // Calculate the distance to this surface.
1007
      // Note the off-by-one indexing
1008
      bool coincident {std::abs(token) == std::abs(on_surface)};
2,147,483,647✔
1009
      double d {model::surfaces[abs(token) - 1]->distance(r, u, coincident)};
2,147,483,647✔
1010

1011
      // Check if this distance is the new minimum.
1012
      if ((d > atleast) && (d < min_dist)) {
2,147,483,647✔
1013
        if (min_dist - d >= FP_PRECISION * min_dist) {
771,827,728!
1014
          min_dist = d;
771,827,728✔
1015
          i_surf = -token;
771,827,728✔
1016
        }
1017
      }
1018
    }
1019
    if (min_dist == INFTY)
335,462,984✔
1020
      break;
1021
    // Search for next closest intersection
1022
    auto [next_dist, i_surf_next] =
670,388,080✔
1023
      distance_simple(r + min_dist * u, u, i_surf);
335,194,040✔
1024
    // If no next intersection we are done
1025
    if (next_dist == INFTY)
335,194,040✔
1026
      break;
1027
    // If there is next intersection we will check point containment in the
1028
    // middle to avoid close to boundary numerical errors
1029
    auto r_mid = r + (min_dist + 0.5 * next_dist) * u;
333,967,718✔
1030
    if (contains_complex(r_mid, u, i_surf) != in_region)
333,967,718✔
1031
      break;
1032
    atleast = min_dist;
235,545,776✔
1033
  }
235,545,776✔
1034
  return {min_dist, i_surf};
99,917,208✔
1035
}
1036

1037
//==============================================================================
1038

1039
bool Region::contains(Position r, Direction u, int32_t on_surface) const
2,147,483,647✔
1040
{
1041
  if (simple_) {
2,147,483,647✔
1042
    return contains_simple(r, u, on_surface);
2,147,483,647✔
1043
  } else {
1044
    return contains_complex(r, u, on_surface);
13,177,031✔
1045
  }
1046
}
1047

1048
//==============================================================================
1049

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

1071
//==============================================================================
1072

1073
bool Region::contains_complex(Position r, Direction u, int32_t on_surface) const
447,061,957✔
1074
{
1075
  bool in_cell = true;
447,061,957✔
1076
  int total_depth = 0;
447,061,957✔
1077

1078
  // For each token
1079
  for (auto it = expression_.begin(); it != expression_.end(); it++) {
2,147,483,647✔
1080
    int32_t token = *it;
2,147,483,647✔
1081

1082
    // If the token is a surface evaluate the sense
1083
    // If the token is a union or intersection check to
1084
    // short circuit
1085
    if (token < OP_UNION) {
2,147,483,647✔
1086
      if (token == on_surface) {
1,739,727,951✔
1087
        in_cell = true;
1088
      } else if (-token == on_surface) {
1,726,215,410✔
1089
        in_cell = false;
1090
      } else {
1091
        // Note the off-by-one indexing
1092
        bool sense = model::surfaces[abs(token) - 1]->sense(r, u);
1,587,780,431✔
1093
        in_cell = (sense == (token > 0));
1,587,780,431✔
1094
      }
1095
    } else if ((token == OP_UNION && in_cell == true) ||
2,147,483,647✔
1096
               (token == OP_INTERSECTION && in_cell == false)) {
885,515,113✔
1097
      // If the total depth is zero return
1098
      if (total_depth == 0) {
798,940,771✔
1099
        return in_cell;
102,644,246✔
1100
      }
1101

1102
      total_depth--;
696,296,525✔
1103

1104
      // While the iterator is within the bounds of the vector
1105
      int depth = 1;
696,296,525✔
1106
      do {
2,147,483,647✔
1107
        // Get next token
1108
        it++;
2,147,483,647✔
1109
        int32_t next_token = *it;
2,147,483,647✔
1110

1111
        // If the token is an a parenthesis
1112
        if (next_token > OP_COMPLEMENT) {
2,147,483,647✔
1113
          // Adjust depth accordingly
1114
          if (next_token == OP_RIGHT_PAREN) {
861,034,845✔
1115
            depth--;
778,665,685✔
1116
          } else {
1117
            depth++;
82,369,160✔
1118
          }
1119
        }
1120
      } while (depth > 0);
2,147,483,647✔
1121
    } else if (token == OP_LEFT_PAREN) {
2,044,822,855✔
1122
      total_depth++;
724,226,693✔
1123
    } else if (token == OP_RIGHT_PAREN) {
1,320,596,162✔
1124
      total_depth--;
27,930,168✔
1125
    }
1126
  }
1127
  return in_cell;
1128
}
1129

1130
//==============================================================================
1131

1132
BoundingBox Region::bounding_box(int32_t cell_id) const
48✔
1133
{
1134
  if (simple_) {
48✔
1135
    return bounding_box_simple();
24✔
1136
  } else {
1137
    auto postfix = generate_postfix(cell_id);
24✔
1138
    return bounding_box_complex(postfix);
48✔
1139
  }
24✔
1140
}
1141

1142
//==============================================================================
1143

1144
BoundingBox Region::bounding_box_simple() const
24✔
1145
{
1146
  BoundingBox bbox;
24✔
1147
  for (int32_t token : expression_) {
96✔
1148
    bbox &= model::surfaces[abs(token) - 1]->bounding_box(token > 0);
72✔
1149
  }
1150
  return bbox;
24✔
1151
}
1152

1153
//==============================================================================
1154

1155
BoundingBox Region::bounding_box_complex(vector<int32_t> postfix) const
24✔
1156
{
1157
  vector<BoundingBox> stack(postfix.size());
24✔
1158
  int i_stack = -1;
24✔
1159

1160
  for (auto& token : postfix) {
432✔
1161
    if (token == OP_UNION) {
408✔
1162
      stack[i_stack - 1] = stack[i_stack - 1] | stack[i_stack];
84✔
1163
      i_stack--;
84✔
1164
    } else if (token == OP_INTERSECTION) {
324✔
1165
      stack[i_stack - 1] = stack[i_stack - 1] & stack[i_stack];
108✔
1166
      i_stack--;
108✔
1167
    } else {
1168
      i_stack++;
216✔
1169
      stack[i_stack] = model::surfaces[abs(token) - 1]->bounding_box(token > 0);
216✔
1170
    }
1171
  }
1172

1173
  assert(i_stack == 0);
24!
1174
  return stack.front();
24✔
1175
}
24✔
1176

1177
//==============================================================================
1178

1179
vector<int32_t> Region::surfaces() const
2,820✔
1180
{
1181
  if (simple_) {
2,820✔
1182
    return expression_;
2,808✔
1183
  }
1184

1185
  vector<int32_t> surfaces = expression_;
12✔
1186

1187
  auto it = std::find_if(surfaces.begin(), surfaces.end(),
12✔
1188
    [&](const auto& value) { return value >= OP_UNION; });
12!
1189

1190
  while (it != surfaces.end()) {
36✔
1191
    surfaces.erase(it);
24✔
1192

1193
    it = std::find_if(surfaces.begin(), surfaces.end(),
24✔
1194
      [&](const auto& value) { return value >= OP_UNION; });
48!
1195
  }
1196

1197
  return surfaces;
12✔
1198
}
2,820✔
1199

1200
//==============================================================================
1201
// Non-method functions
1202
//==============================================================================
1203

1204
void read_cells(pugi::xml_node node)
4,852✔
1205
{
1206
  // Count the number of cells.
1207
  int n_cells = 0;
4,852✔
1208
  for (pugi::xml_node cell_node : node.children("cell")) {
21,136✔
1209
    n_cells++;
16,284✔
1210
  }
1211

1212
  // Loop over XML cell elements and populate the array.
1213
  model::cells.reserve(n_cells);
4,852✔
1214
  for (pugi::xml_node cell_node : node.children("cell")) {
21,136✔
1215
    model::cells.push_back(make_unique<CSGCell>(cell_node));
16,284✔
1216
  }
1217

1218
  // Fill the cell map.
1219
  for (int i = 0; i < model::cells.size(); i++) {
21,136✔
1220
    int32_t id = model::cells[i]->id_;
16,284!
1221
    auto search = model::cell_map.find(id);
16,284!
1222
    if (search == model::cell_map.end()) {
16,284!
1223
      model::cell_map[id] = i;
16,284✔
1224
    } else {
1225
      fatal_error(
×
1226
        fmt::format("Two or more cells use the same unique ID: {}", id));
×
1227
    }
1228
  }
1229

1230
  read_dagmc_universes(node);
4,852✔
1231

1232
  populate_universes();
4,852✔
1233

1234
  // Allocate the cell overlap count if necessary.
1235
  if (settings::check_overlaps) {
4,852✔
1236
    model::overlap_check_count.resize(model::cells.size(), 0);
66✔
1237
  }
1238

1239
  if (model::cells.size() == 0) {
4,852!
1240
    fatal_error("No cells were found in the geometry.xml file");
×
1241
  }
1242
}
4,852✔
1243

1244
void populate_universes()
4,852✔
1245
{
1246
  // Used to map universe index to the index of an implicit complement cell for
1247
  // DAGMC universes
1248
  std::unordered_map<int, int> implicit_comp_cells;
4,852✔
1249

1250
  // Populate the Universe vector and map.
1251
  for (int index_cell = 0; index_cell < model::cells.size(); index_cell++) {
21,136✔
1252
    int32_t uid = model::cells[index_cell]->universe_;
16,284✔
1253
    auto it = model::universe_map.find(uid);
16,284✔
1254
    if (it == model::universe_map.end()) {
16,284✔
1255
      model::universes.push_back(make_unique<Universe>());
16,004✔
1256
      model::universes.back()->id_ = uid;
8,002✔
1257
      model::universes.back()->cells_.push_back(index_cell);
8,002✔
1258
      model::universe_map[uid] = model::universes.size() - 1;
8,002✔
1259
    } else {
1260
#ifdef OPENMC_DAGMC_ENABLED
1261
      // Skip implicit complement cells for now
1262
      Universe* univ = model::universes[it->second].get();
1263
      DAGUniverse* dag_univ = dynamic_cast<DAGUniverse*>(univ);
1264
      if (dag_univ && (dag_univ->implicit_complement_idx() == index_cell)) {
1265
        implicit_comp_cells[it->second] = index_cell;
1266
        continue;
1267
      }
1268
#endif
1269

1270
      model::universes[it->second]->cells_.push_back(index_cell);
8,282✔
1271
    }
1272
  }
1273

1274
  // Add DAGUniverse implicit complement cells last
1275
  for (const auto& it : implicit_comp_cells) {
4,852!
UNCOV
1276
    int index_univ = it.first;
×
UNCOV
1277
    int index_cell = it.second;
×
UNCOV
1278
    model::universes[index_univ]->cells_.push_back(index_cell);
×
1279
  }
1280

1281
  model::universes.shrink_to_fit();
4,852✔
1282
}
4,852✔
1283

1284
//==============================================================================
1285
// C-API functions
1286
//==============================================================================
1287

1288
extern "C" int openmc_cell_get_fill(
72✔
1289
  int32_t index, int* type, int32_t** indices, int32_t* n)
1290
{
1291
  if (index >= 0 && index < model::cells.size()) {
72!
1292
    Cell& c {*model::cells[index]};
72✔
1293
    *type = static_cast<int>(c.type_);
72✔
1294
    if (c.type_ == Fill::MATERIAL) {
72✔
1295
      *indices = c.material_.data();
66✔
1296
      *n = c.material_.size();
66✔
1297
    } else {
1298
      *indices = &c.fill_;
6✔
1299
      *n = 1;
6✔
1300
    }
1301
  } else {
1302
    set_errmsg("Index in cells array is out of bounds.");
×
1303
    return OPENMC_E_OUT_OF_BOUNDS;
×
1304
  }
1305
  return 0;
1306
}
1307

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

1341
extern "C" int openmc_cell_set_temperature(
48✔
1342
  int32_t index, double T, const int32_t* instance, bool set_contained)
1343
{
1344
  if (index < 0 || index >= model::cells.size()) {
48!
1345
    strcpy(openmc_err_msg, "Index in cells array is out of bounds.");
×
1346
    return OPENMC_E_OUT_OF_BOUNDS;
×
1347
  }
1348

1349
  int32_t instance_index = instance ? *instance : -1;
48✔
1350
  try {
48✔
1351
    model::cells[index]->set_temperature(T, instance_index, set_contained);
48✔
1352
  } catch (const std::exception& e) {
×
1353
    set_errmsg(e.what());
×
1354
    return OPENMC_E_UNASSIGNED;
×
1355
  }
×
1356
  return 0;
1357
}
1358

1359
extern "C" int openmc_cell_set_density(
48✔
1360
  int32_t index, double density, const int32_t* instance, bool set_contained)
1361
{
1362
  if (index < 0 || index >= model::cells.size()) {
48!
1363
    strcpy(openmc_err_msg, "Index in cells array is out of bounds.");
×
1364
    return OPENMC_E_OUT_OF_BOUNDS;
×
1365
  }
1366

1367
  int32_t instance_index = instance ? *instance : -1;
48✔
1368
  try {
48✔
1369
    model::cells[index]->set_density(density, instance_index, set_contained);
48✔
1370
  } catch (const std::exception& e) {
×
1371
    set_errmsg(e.what());
×
1372
    return OPENMC_E_UNASSIGNED;
×
1373
  }
×
1374
  return 0;
1375
}
1376

1377
extern "C" int openmc_cell_get_temperature(
5,250✔
1378
  int32_t index, const int32_t* instance, double* T)
1379
{
1380
  if (index < 0 || index >= model::cells.size()) {
5,250!
1381
    strcpy(openmc_err_msg, "Index in cells array is out of bounds.");
×
1382
    return OPENMC_E_OUT_OF_BOUNDS;
×
1383
  }
1384

1385
  int32_t instance_index = instance ? *instance : -1;
5,250✔
1386
  try {
5,250✔
1387
    *T = model::cells[index]->temperature(instance_index);
5,250✔
1388
  } catch (const std::exception& e) {
×
1389
    set_errmsg(e.what());
×
1390
    return OPENMC_E_UNASSIGNED;
×
1391
  }
×
1392
  return 0;
5,250✔
1393
}
1394

1395
extern "C" int openmc_cell_get_density(
48✔
1396
  int32_t index, const int32_t* instance, double* density)
1397
{
1398
  if (index < 0 || index >= model::cells.size()) {
48!
1399
    strcpy(openmc_err_msg, "Index in cells array is out of bounds.");
×
1400
    return OPENMC_E_OUT_OF_BOUNDS;
×
1401
  }
1402

1403
  int32_t instance_index = instance ? *instance : -1;
48✔
1404
  try {
48✔
1405
    if (model::cells[index]->type_ != Fill::MATERIAL) {
48!
1406
      fatal_error(
×
1407
        fmt::format("Cell {}, instance {} is not filled with a material.",
×
1408
          model::cells[index]->id_, instance_index));
×
1409
    }
1410

1411
    int32_t mat_index = model::cells[index]->material(instance_index);
48!
1412
    if (mat_index == MATERIAL_VOID) {
48!
1413
      *density = 0.0;
×
1414
    } else {
1415
      *density = model::cells[index]->density_mult(instance_index) *
48✔
1416
                 model::materials[mat_index]->density_gpcc();
96!
1417
    }
1418
  } catch (const std::exception& e) {
×
1419
    set_errmsg(e.what());
×
1420
    return OPENMC_E_UNASSIGNED;
×
1421
  }
×
1422
  return 0;
1423
}
1424

1425
//! Get the bounding box of a cell
1426
extern "C" int openmc_cell_bounding_box(
30✔
1427
  const int32_t index, double* llc, double* urc)
1428
{
1429

1430
  BoundingBox bbox;
30✔
1431

1432
  const auto& c = model::cells[index];
30✔
1433
  bbox = c->bounding_box();
30✔
1434

1435
  // set lower left corner values
1436
  llc[0] = bbox.min.x;
30✔
1437
  llc[1] = bbox.min.y;
30✔
1438
  llc[2] = bbox.min.z;
30✔
1439

1440
  // set upper right corner values
1441
  urc[0] = bbox.max.x;
30✔
1442
  urc[1] = bbox.max.y;
30✔
1443
  urc[2] = bbox.max.z;
30✔
1444

1445
  return 0;
30✔
1446
}
1447

1448
//! Get the name of a cell
1449
extern "C" int openmc_cell_get_name(int32_t index, const char** name)
204✔
1450
{
1451
  if (index < 0 || index >= model::cells.size()) {
204!
1452
    set_errmsg("Index in cells array is out of bounds.");
×
1453
    return OPENMC_E_OUT_OF_BOUNDS;
×
1454
  }
1455

1456
  *name = model::cells[index]->name().data();
204✔
1457

1458
  return 0;
204✔
1459
}
1460

1461
//! Set the name of a cell
1462
extern "C" int openmc_cell_set_name(int32_t index, const char* name)
6✔
1463
{
1464
  if (index < 0 || index >= model::cells.size()) {
6!
1465
    set_errmsg("Index in cells array is out of bounds.");
×
1466
    return OPENMC_E_OUT_OF_BOUNDS;
×
1467
  }
1468

1469
  model::cells[index]->set_name(name);
12✔
1470

1471
  return 0;
6✔
1472
}
1473

1474
//==============================================================================
1475
//! Define a containing (parent) cell
1476
//==============================================================================
1477

1478
//! Used to locate a universe fill in the geometry
1479
struct ParentCell {
1480
  bool operator==(const ParentCell& other) const
72✔
1481
  {
1482
    return cell_index == other.cell_index &&
72!
1483
           lattice_index == other.lattice_index;
72!
1484
  }
1485

1486
  bool operator<(const ParentCell& other) const
1487
  {
1488
    return cell_index < other.cell_index ||
1489
           (cell_index == other.cell_index &&
1490
             lattice_index < other.lattice_index);
1491
  }
1492

1493
  int64_t cell_index;
1494
  int64_t lattice_index;
1495
};
1496

1497
//! Structure used to insert ParentCell into hashed STL data structures
1498
struct ParentCellHash {
1499
  std::size_t operator()(const ParentCell& p) const
352✔
1500
  {
1501
    return 4096 * p.cell_index + p.lattice_index;
352!
1502
  }
1503
};
1504

1505
//! Used to manage a traversal stack when locating parent cells of a cell
1506
//! instance in the model
1507
struct ParentCellStack {
72✔
1508

1509
  //! push method that adds to the parent_cells visited cells for this search
1510
  //! universe
1511
  void push(int32_t search_universe, const ParentCell& pc)
56✔
1512
  {
1513
    parent_cells_.push_back(pc);
56✔
1514
    // add parent cell to the set of cells we've visited for this search
1515
    // universe
1516
    visited_cells_[search_universe].insert(pc);
56✔
1517
  }
56✔
1518

1519
  //! removes the last parent_cell and clears the visited cells for the popped
1520
  //! cell's universe
1521
  void pop()
40✔
1522
  {
1523
    visited_cells_[this->current_univ()].clear();
40✔
1524
    parent_cells_.pop_back();
40✔
1525
  }
40✔
1526

1527
  //! checks whether or not the parent cell has been visited already for this
1528
  //! search universe
1529
  bool visited(int32_t search_universe, const ParentCell& parent_cell)
296✔
1530
  {
1531
    return visited_cells_[search_universe].count(parent_cell) != 0;
296✔
1532
  }
1533

1534
  //! return the next universe to search for a parent cell
1535
  int32_t current_univ() const
40✔
1536
  {
1537
    return model::cells[parent_cells_.back().cell_index]->universe_;
40✔
1538
  }
1539

1540
  //! indicates whether nor not parent cells are present on the stack
1541
  bool empty() const { return parent_cells_.empty(); }
40✔
1542

1543
  //! compute an instance for the provided distribcell index
1544
  int32_t compute_instance(int32_t distribcell_index) const
112✔
1545
  {
1546
    if (distribcell_index == C_NONE)
112✔
1547
      return 0;
1548

1549
    int32_t instance = 0;
64✔
1550
    for (const auto& parent_cell : this->parent_cells_) {
120✔
1551
      auto& cell = model::cells[parent_cell.cell_index];
56!
1552
      if (cell->type_ == Fill::UNIVERSE) {
56!
1553
        instance += cell->offset_[distribcell_index];
×
1554
      } else if (cell->type_ == Fill::LATTICE) {
56!
1555
        auto& lattice = model::lattices[cell->fill_];
56✔
1556
        instance +=
56✔
1557
          lattice->offset(distribcell_index, parent_cell.lattice_index);
56✔
1558
      }
1559
    }
1560
    return instance;
1561
  }
1562

1563
  // Accessors
1564
  vector<ParentCell>& parent_cells() { return parent_cells_; }
72✔
1565
  const vector<ParentCell>& parent_cells() const { return parent_cells_; }
1566

1567
  // Data Members
1568
  vector<ParentCell> parent_cells_;
1569
  std::unordered_map<int32_t, std::unordered_set<ParentCell, ParentCellHash>>
1570
    visited_cells_;
1571
};
1572

1573
vector<ParentCell> Cell::find_parent_cells(
×
1574
  int32_t instance, const Position& r) const
1575
{
1576

1577
  // create a temporary particle
1578
  GeometryState dummy_particle {};
×
1579
  dummy_particle.r() = r;
×
1580
  dummy_particle.u() = {0., 0., 1.};
×
1581

1582
  return find_parent_cells(instance, dummy_particle);
×
1583
}
×
1584

1585
vector<ParentCell> Cell::find_parent_cells(
×
1586
  int32_t instance, GeometryState& p) const
1587
{
1588
  // look up the particle's location
1589
  exhaustive_find_cell(p);
×
1590
  const auto& coords = p.coord();
×
1591

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

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

1616
  // if this loop finished because the cell was found and
1617
  // the instance matches the one requested in the call
1618
  // we have the correct path and can return the stack
1619
  if (cell_found &&
×
1620
      stack.compute_instance(this->distribcell_index_) == instance) {
×
1621
    return stack.parent_cells();
×
1622
  }
1623

1624
  // fall back on an exhaustive search for the cell's parents
1625
  return exhaustive_find_parent_cells(instance);
×
1626
}
×
1627

1628
vector<ParentCell> Cell::exhaustive_find_parent_cells(int32_t instance) const
72✔
1629
{
1630
  ParentCellStack stack;
72✔
1631
  // start with this cell's universe
1632
  int32_t prev_univ_idx;
72✔
1633
  int32_t univ_idx = this->universe_;
72✔
1634

1635
  while (true) {
112✔
1636
    const auto& univ = model::universes[univ_idx];
112✔
1637
    prev_univ_idx = univ_idx;
112✔
1638

1639
    // search for a cell that is filled w/ this universe
1640
    for (const auto& cell : model::cells) {
760✔
1641
      // if this is a material-filled cell, move on
1642
      if (cell->type_ == Fill::MATERIAL)
704✔
1643
        continue;
416✔
1644

1645
      if (cell->type_ == Fill::UNIVERSE) {
288✔
1646
        // if this is in the set of cells previously visited for this universe,
1647
        // move on
1648
        if (stack.visited(univ_idx, {model::cell_map[cell->id_], C_NONE}))
168!
1649
          continue;
×
1650

1651
        // if this cell contains the universe we're searching for, add it to the
1652
        // stack
1653
        if (cell->fill_ == univ_idx) {
168!
1654
          stack.push(univ_idx, {model::cell_map[cell->id_], C_NONE});
×
1655
          univ_idx = cell->universe_;
×
1656
        }
1657
      } else if (cell->type_ == Fill::LATTICE) {
120!
1658
        // retrieve the lattice and lattice universes
1659
        const auto& lattice = model::lattices[cell->fill_];
120✔
1660
        const auto& lattice_univs = lattice->universes_;
120✔
1661

1662
        // start search for universe
1663
        auto lat_it = lattice_univs.begin();
120✔
1664
        while (true) {
264✔
1665
          // find the next lattice cell with this universe
1666
          lat_it = std::find(lat_it, lattice_univs.end(), univ_idx);
192✔
1667
          if (lat_it == lattice_univs.end())
192✔
1668
            break;
1669

1670
          int lattice_idx = lat_it - lattice_univs.begin();
128✔
1671

1672
          // move iterator forward one to avoid finding the same entry
1673
          lat_it++;
128✔
1674
          if (stack.visited(
256✔
1675
                univ_idx, {model::cell_map[cell->id_], lattice_idx}))
128✔
1676
            continue;
72✔
1677

1678
          // add this cell and lattice index to the stack and exit loop
1679
          stack.push(univ_idx, {model::cell_map[cell->id_], lattice_idx});
56✔
1680
          univ_idx = cell->universe_;
56✔
1681
          break;
56✔
1682
        }
72✔
1683
      }
1684
      // if we've updated the universe, break
1685
      if (prev_univ_idx != univ_idx)
288✔
1686
        break;
1687
    } // end cell loop search for universe
1688

1689
    // if we're at the top of the geometry and the instance matches, we're done
1690
    if (univ_idx == model::root_universe &&
112!
1691
        stack.compute_instance(this->distribcell_index_) == instance)
112✔
1692
      break;
1693

1694
    // if there is no match on the original cell's universe, report an error
1695
    if (univ_idx == this->universe_) {
40!
1696
      fatal_error(
×
1697
        fmt::format("Could not find the parent cells for cell {}, instance {}.",
×
1698
          this->id_, instance));
×
1699
    }
1700

1701
    // if we don't find a suitable update, adjust the stack and continue
1702
    if (univ_idx == model::root_universe || univ_idx == prev_univ_idx) {
40!
1703
      stack.pop();
40✔
1704
      univ_idx = stack.empty() ? this->universe_ : stack.current_univ();
40!
1705
    }
1706

1707
  } // end while
1708

1709
  // reverse the stack so the highest cell comes first
1710
  std::reverse(stack.parent_cells().begin(), stack.parent_cells().end());
72✔
1711
  return stack.parent_cells();
144✔
1712
}
72✔
1713

1714
std::unordered_map<int32_t, vector<int32_t>> Cell::get_contained_cells(
96✔
1715
  int32_t instance, Position* hint) const
1716
{
1717
  std::unordered_map<int32_t, vector<int32_t>> contained_cells;
96✔
1718

1719
  // if this is a material-filled cell it has no contained cells
1720
  if (this->type_ == Fill::MATERIAL)
96✔
1721
    return contained_cells;
1722

1723
  // find the pathway through the geometry to this cell
1724
  vector<ParentCell> parent_cells;
72!
1725

1726
  // if a positional hint is provided, attempt to do a fast lookup
1727
  // of the parent cells
1728
  parent_cells = hint ? find_parent_cells(instance, *hint)
72!
1729
                      : exhaustive_find_parent_cells(instance);
72✔
1730

1731
  // if this cell is filled w/ a material, it contains no other cells
1732
  if (type_ != Fill::MATERIAL) {
72!
1733
    this->get_contained_cells_inner(contained_cells, parent_cells);
72✔
1734
  }
1735

1736
  return contained_cells;
72✔
1737
}
96✔
1738

1739
//! Get all cells within this cell
1740
void Cell::get_contained_cells_inner(
71,560✔
1741
  std::unordered_map<int32_t, vector<int32_t>>& contained_cells,
1742
  vector<ParentCell>& parent_cells) const
1743
{
1744

1745
  // filled by material, determine instance based on parent cells
1746
  if (type_ == Fill::MATERIAL) {
71,560✔
1747
    int instance = 0;
71,216✔
1748
    if (this->distribcell_index_ >= 0) {
71,216!
1749
      for (auto& parent_cell : parent_cells) {
213,648✔
1750
        auto& cell = model::cells[parent_cell.cell_index];
142,432✔
1751
        if (cell->type_ == Fill::UNIVERSE) {
142,432✔
1752
          instance += cell->offset_[distribcell_index_];
70,416✔
1753
        } else if (cell->type_ == Fill::LATTICE) {
72,016!
1754
          auto& lattice = model::lattices[cell->fill_];
72,016✔
1755
          instance += lattice->offset(
72,016✔
1756
            this->distribcell_index_, parent_cell.lattice_index);
72,016✔
1757
        }
1758
      }
1759
    }
1760
    // add entry to contained cells
1761
    contained_cells[model::cell_map[id_]].push_back(instance);
71,216✔
1762
    // filled with universe, add the containing cell to the parent cells
1763
    // and recurse
1764
  } else if (type_ == Fill::UNIVERSE) {
344✔
1765
    parent_cells.push_back({model::cell_map[id_], -1});
280✔
1766
    auto& univ = model::universes[fill_];
280✔
1767
    for (auto cell_index : univ->cells_) {
1,616✔
1768
      auto& cell = model::cells[cell_index];
1,336✔
1769
      cell->get_contained_cells_inner(contained_cells, parent_cells);
1,336✔
1770
    }
1771
    parent_cells.pop_back();
280✔
1772
    // filled with a lattice, visit each universe in the lattice
1773
    // with a recursive call to collect the cell instances
1774
  } else if (type_ == Fill::LATTICE) {
64!
1775
    auto& lattice = model::lattices[fill_];
64✔
1776
    for (auto i = lattice->begin(); i != lattice->end(); ++i) {
70,048✔
1777
      auto& univ = model::universes[*i];
69,984✔
1778
      parent_cells.push_back({model::cell_map[id_], i.indx_});
69,984✔
1779
      for (auto cell_index : univ->cells_) {
140,136✔
1780
        auto& cell = model::cells[cell_index];
70,152✔
1781
        cell->get_contained_cells_inner(contained_cells, parent_cells);
70,152✔
1782
      }
1783
      parent_cells.pop_back();
69,984✔
1784
    }
1785
  }
1786
}
71,560✔
1787

1788
//! Return the index in the cells array of a cell with a given ID
1789
extern "C" int openmc_get_cell_index(int32_t id, int32_t* index)
534✔
1790
{
1791
  auto it = model::cell_map.find(id);
534✔
1792
  if (it != model::cell_map.end()) {
534✔
1793
    *index = it->second;
528✔
1794
    return 0;
528✔
1795
  } else {
1796
    set_errmsg("No cell exists with ID=" + std::to_string(id) + ".");
6✔
1797
    return OPENMC_E_INVALID_ID;
6✔
1798
  }
1799
}
1800

1801
//! Return the ID of a cell
1802
extern "C" int openmc_cell_get_id(int32_t index, int32_t* id)
1,290✔
1803
{
1804
  if (index >= 0 && index < model::cells.size()) {
1,290!
1805
    *id = model::cells[index]->id_;
1,290✔
1806
    return 0;
1,290✔
1807
  } else {
1808
    set_errmsg("Index in cells array is out of bounds.");
×
1809
    return OPENMC_E_OUT_OF_BOUNDS;
×
1810
  }
1811
}
1812

1813
//! Set the ID of a cell
1814
extern "C" int openmc_cell_set_id(int32_t index, int32_t id)
12✔
1815
{
1816
  if (index >= 0 && index < model::cells.size()) {
12!
1817
    model::cells[index]->id_ = id;
12✔
1818
    model::cell_map[id] = index;
12✔
1819
    return 0;
12✔
1820
  } else {
1821
    set_errmsg("Index in cells array is out of bounds.");
×
1822
    return OPENMC_E_OUT_OF_BOUNDS;
×
1823
  }
1824
}
1825

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

1841
//! Set the translation vector of a cell
1842
extern "C" int openmc_cell_set_translation(int32_t index, const double xyz[])
30✔
1843
{
1844
  if (index >= 0 && index < model::cells.size()) {
30!
1845
    if (model::cells[index]->fill_ == C_NONE) {
30✔
1846
      set_errmsg(fmt::format("Cannot apply a translation to cell {}"
6✔
1847
                             " because it is not filled with another universe",
1848
        index));
1849
      return OPENMC_E_GEOMETRY;
6✔
1850
    }
1851
    model::cells[index]->translation_ = Position(xyz);
24✔
1852
    return 0;
24✔
1853
  } else {
1854
    set_errmsg("Index in cells array is out of bounds.");
×
1855
    return OPENMC_E_OUT_OF_BOUNDS;
×
1856
  }
1857
}
1858

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

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

1893
//! Get the number of instances of the requested cell
1894
extern "C" int openmc_cell_get_num_instances(
42✔
1895
  int32_t index, int32_t* num_instances)
1896
{
1897
  if (index < 0 || index >= model::cells.size()) {
42!
1898
    set_errmsg("Index in cells array is out of bounds.");
×
1899
    return OPENMC_E_OUT_OF_BOUNDS;
×
1900
  }
1901
  *num_instances = model::cells[index]->n_instances();
42✔
1902
  return 0;
42✔
1903
}
1904

1905
//! Extend the cells array by n elements
1906
extern "C" int openmc_extend_cells(
12✔
1907
  int32_t n, int32_t* index_start, int32_t* index_end)
1908
{
1909
  if (index_start)
12!
1910
    *index_start = model::cells.size();
12✔
1911
  if (index_end)
12!
1912
    *index_end = model::cells.size() + n - 1;
×
1913
  for (int32_t i = 0; i < n; i++) {
24✔
1914
    model::cells.push_back(make_unique<CSGCell>());
12✔
1915
  }
1916
  return 0;
12✔
1917
}
1918

1919
extern "C" int cells_size()
54✔
1920
{
1921
  return model::cells.size();
54✔
1922
}
1923

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