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

openmc-dev / openmc / 29167401541

11 Jul 2026 08:38PM UTC coverage: 80.762% (-0.5%) from 81.295%
29167401541

Pull #4010

github

web-flow
Merge 612540872 into e783e0147
Pull Request #4010: Implementation of surfaces with custom implicit function

18489 of 27369 branches covered (67.55%)

Branch coverage included in aggregate %.

1041 of 1330 new or added lines in 13 files covered. (78.27%)

76 existing lines in 3 files now uncovered.

60486 of 70418 relevant lines covered (85.9%)

53343422.58 hits per line

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

72.41
/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/surface.h"
26
#include "openmc/xml_interface.h"
27

28
namespace openmc {
29

30
//==============================================================================
31
// Global variables
32
//==============================================================================
33

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

38
} // namespace model
39

40
//==============================================================================
41
// Cell implementation
42
//==============================================================================
43

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

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

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

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

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

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

97
  if (instance >= 0) {
9,634✔
98
    double sqrtkT = sqrtkT_.size() == 1 ? sqrtkT_.at(0) : sqrtkT_.at(instance);
9,548✔
99
    return sqrtkT * sqrtkT / K_BOLTZMANN;
9,548✔
100
  } else {
101
    return sqrtkT_[0] * sqrtkT_[0] / K_BOLTZMANN;
86✔
102
  }
103
}
104

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

115
double Cell::density(int32_t instance) const
1,199,827✔
116
{
117
  const int32_t mat_index = material(instance);
1,199,827✔
118
  if (mat_index == MATERIAL_VOID)
1,199,827!
119
    return 0.0;
120

121
  return density_mult(instance) * model::materials[mat_index]->density_gpcc();
2,399,654✔
122
}
123

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

140
  if (type_ == Fill::MATERIAL) {
10,012✔
141
    if (instance >= 0) {
9,982✔
142
      // If temperature vector is not big enough, resize it first
143
      if (sqrtkT_.size() != n_instances())
9,905✔
144
        sqrtkT_.resize(n_instances(), sqrtkT_[0]);
45✔
145

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

162
    auto contained_cells = this->get_contained_cells(instance);
30✔
163
    for (const auto& entry : contained_cells) {
120✔
164
      auto& cell = model::cells[entry.first];
90!
165
      assert(cell->type_ == Fill::MATERIAL);
90!
166
      auto& instances = entry.second;
90✔
167
      for (auto instance : instances) {
315✔
168
        cell->set_temperature(T, instance);
225✔
169
      }
170
    }
171
  }
30✔
172
}
10,012✔
173

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

183
  if (type_ == Fill::MATERIAL) {
346✔
184
    const int32_t mat_index = material(instance);
331!
185
    if (mat_index == MATERIAL_VOID)
331!
186
      return;
187

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

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

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

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

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

232
    write_dataset(cell_group, "density", density);
198✔
233
  }
198✔
234

235
  close_group(cell_group);
231✔
236
}
231✔
237

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

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

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

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

261
  // Read densities
262
  if (object_exists(cell_group, "density")) {
253✔
263
    vector<double> density;
198✔
264
    read_dataset(cell_group, "density", density);
198✔
265

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

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

280
  close_group(cell_group);
253✔
281
}
253✔
282

283
void Cell::to_hdf5(hid_t cell_group) const
30,391✔
284
{
285

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

289
  if (!name_.empty()) {
30,391✔
290
    write_string(group, "name", name_, false);
7,100✔
291
  }
292

293
  write_dataset(group, "universe", model::universes[universe_]->id_);
30,391✔
294

295
  to_hdf5_inner(group);
30,391✔
296

297
  // Write fill information.
298
  if (type_ == Fill::MATERIAL) {
30,391✔
299
    write_dataset(group, "fill_type", "material");
24,835✔
300
    std::vector<int32_t> mat_ids;
24,835✔
301
    for (auto i_mat : material_) {
50,963✔
302
      if (i_mat != MATERIAL_VOID) {
26,128✔
303
        mat_ids.push_back(model::materials[i_mat]->id_);
17,443✔
304
      } else {
305
        mat_ids.push_back(MATERIAL_VOID);
8,685✔
306
      }
307
    }
308
    if (mat_ids.size() == 1) {
24,835✔
309
      write_dataset(group, "material", mat_ids[0]);
24,644✔
310
    } else {
311
      write_dataset(group, "material", mat_ids);
191✔
312
    }
313

314
    std::vector<double> temps;
24,835✔
315
    for (auto sqrtkT_val : sqrtkT_)
61,316✔
316
      temps.push_back(sqrtkT_val * sqrtkT_val / K_BOLTZMANN);
36,481✔
317
    write_dataset(group, "temperature", temps);
24,835✔
318

319
    write_dataset(group, "density_mult", density_mult_);
24,835✔
320

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

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

341
  close_group(group);
30,391✔
342
}
30,391✔
343

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

348
vector<int32_t> parse_cell_material_xml(pugi::xml_node node, int32_t cell_id)
28,996✔
349
{
350
  vector<std::string> mats {
28,996✔
351
    get_node_array<std::string>(node, "material", true)};
28,996✔
352
  if (mats.empty()) {
28,996!
353
    fatal_error(fmt::format(
×
354
      "An empty material element was specified for cell {}", cell_id));
355
  }
356
  vector<int32_t> material;
28,996✔
357
  material.reserve(mats.size());
28,996✔
358
  for (const auto& mat : mats) {
59,321✔
359
    if (mat == "void") {
30,325✔
360
      material.push_back(MATERIAL_VOID);
9,137✔
361
    } else {
362
      material.push_back(std::stoi(mat));
21,188✔
363
    }
364
  }
365
  return material;
28,996✔
366
}
28,996✔
367

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

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

401
//==============================================================================
402
// CSGCell implementation
403
//==============================================================================
404

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

413
  if (check_for_node(cell_node, "name")) {
36,246✔
414
    name_ = get_node_value(cell_node, "name");
9,075✔
415
  }
416

417
  if (check_for_node(cell_node, "universe")) {
36,246✔
418
    universe_ = std::stoi(get_node_value(cell_node, "universe"));
70,162✔
419
  } else {
420
    universe_ = 0;
1,165✔
421
  }
422

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

436
  if (fill_present) {
36,246✔
437
    fill_ = std::stoi(get_node_value(cell_node, "fill"));
14,518✔
438
    if (fill_ == universe_) {
7,259!
439
      fatal_error(fmt::format("Cell {} is filled with the same universe that "
×
440
                              "it is contained in.",
441
        id_));
×
442
    }
443
  } else {
444
    fill_ = C_NONE;
28,987✔
445
  }
446

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

454
  // Read the temperature element which may be distributed like materials.
455
  if (check_for_node(cell_node, "temperature")) {
36,246✔
456
    sqrtkT_ = parse_cell_temperature_xml(cell_node, id_);
431✔
457
    sqrtkT_.shrink_to_fit();
431✔
458

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

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

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

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

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

502
  // Read the region specification.
503
  std::string region_spec;
36,246✔
504
  if (check_for_node(cell_node, "region")) {
36,246✔
505
    region_spec = get_node_value(cell_node, "region");
27,095✔
506
  }
507

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

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

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

529
  // Read the rotation transform.
530
  if (check_for_node(cell_node, "rotation")) {
36,246✔
531
    auto rot {get_node_array<double>(cell_node, "rotation")};
390✔
532
    set_rotation(rot);
390✔
533
  }
390✔
534
}
36,246✔
535

536
//==============================================================================
537

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

544
//==============================================================================
545

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

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

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

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

579
//==============================================================================
580
// Region implementation
581
//==============================================================================
582

583
Region::Region(std::string region_spec, int32_t cell_id)
36,345✔
584
{
585
  // Check if region_spec is not empty.
586
  if (!region_spec.empty()) {
36,345✔
587
    // Parse all halfspaces and operators except for intersection (whitespace).
588
    for (int i = 0; i < region_spec.size();) {
162,274✔
589
      if (region_spec[i] == '(') {
135,080✔
590
        expression_.push_back(OP_LEFT_PAREN);
1,670✔
591
        i++;
1,670✔
592

593
      } else if (region_spec[i] == ')') {
133,410✔
594
        expression_.push_back(OP_RIGHT_PAREN);
1,670✔
595
        i++;
1,670✔
596

597
      } else if (region_spec[i] == '|') {
131,740✔
598
        expression_.push_back(OP_UNION);
3,933✔
599
        i++;
3,933✔
600

601
      } else if (region_spec[i] == '~') {
127,807✔
602
        expression_.push_back(OP_COMPLEMENT);
30✔
603
        i++;
30✔
604

605
      } else if (region_spec[i] == '-' || region_spec[i] == '+' ||
215,631!
606
                 std::isdigit(region_spec[i])) {
87,854✔
607
        // This is the start of a halfspace specification.  Iterate j until we
608
        // find the end, then push-back everything between i and j.
609
        int j = i + 1;
74,773✔
610
        while (j < region_spec.size() && std::isdigit(region_spec[j])) {
150,981✔
611
          j++;
76,208✔
612
        }
613
        expression_.push_back(std::stoi(region_spec.substr(i, j - i)));
149,546✔
614
        i = j;
74,773✔
615

616
      } else if (std::isspace(region_spec[i])) {
53,004!
617
        i++;
53,004✔
618

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

627
    // Add in intersection operators where a missing operator is needed.
628
    int i = 0;
629
    while (i < expression_.size() - 1) {
125,722✔
630
      bool left_compat {
98,528✔
631
        (expression_[i] < OP_UNION) || (expression_[i] == OP_RIGHT_PAREN)};
98,528✔
632
      bool right_compat {(expression_[i + 1] < OP_UNION) ||
98,528✔
633
                         (expression_[i + 1] == OP_LEFT_PAREN) ||
98,528✔
634
                         (expression_[i + 1] == OP_COMPLEMENT)};
5,663✔
635
      if (left_compat && right_compat) {
98,528✔
636
        expression_.insert(expression_.begin() + i + 1, OP_INTERSECTION);
43,646✔
637
      }
638
      i++;
639
    }
640

641
    // Remove complement operators using DeMorgan's laws
642
    auto it = std::find(expression_.begin(), expression_.end(), OP_COMPLEMENT);
27,194✔
643
    while (it != expression_.end()) {
27,224✔
644
      // Erase complement
645
      expression_.erase(it);
30✔
646

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

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

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

684
    // Check if this is a simple cell.
685
    simple_ = true;
27,194✔
686
    for (int32_t token : expression_) {
137,786✔
687
      if (token == OP_UNION) {
111,790✔
688
        simple_ = false;
1,198✔
689
        // Ensure intersections have precedence over unions
690
        enforce_precedence();
1,198✔
691
        break;
692
      }
693
    }
694

695
    // If this cell is simple, remove all the superfluous operator tokens.
696
    if (simple_) {
27,194✔
697
      for (auto it = expression_.begin(); it != expression_.end(); it++) {
129,930✔
698
        if (*it == OP_INTERSECTION || *it > OP_COMPLEMENT) {
103,934!
699
          expression_.erase(it);
38,969✔
700
          it--;
103,934✔
701
        }
702
      }
703
    }
704
    expression_.shrink_to_fit();
27,194✔
705

706
  } else {
707
    simple_ = true;
9,151✔
708
  }
709
}
36,345✔
710

711
//==============================================================================
712

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

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

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

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

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

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

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

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

801
  for (int64_t i = 0; i < expression_.size(); ++i) {
23,973✔
802
    int32_t token = expression_[i];
22,775✔
803

804
    if (token == OP_LEFT_PAREN) {
22,775✔
805
      // Entering a new parenthesis level - push new tracking state
806
      op_stack.push_back(0);
1,866✔
807
      pos_stack.push_back(0);
1,866✔
808
      continue;
1,866✔
809
    } else if (token == OP_RIGHT_PAREN) {
20,909✔
810
      // Exiting a parenthesis level - pop tracking state (keep at least one)
811
      if (op_stack.size() > 1) {
1,825!
812
        op_stack.pop_back();
1,825✔
813
        pos_stack.pop_back();
1,825✔
814
      }
815
      continue;
1,825✔
816
    }
817

818
    if (token == OP_UNION || token == OP_INTERSECTION) {
19,084✔
819
      if (op_stack.back() == 0) {
8,943✔
820
        // First operator at this depth - record it and its position
821
        op_stack.back() = token;
3,108✔
822
        pos_stack.back() = i;
3,108✔
823
      } else if (token != op_stack.back()) {
5,835✔
824
        // Encountered a different operator at the same depth - need to add
825
        // parentheses to enforce precedence. Intersection has higher
826
        // precedence, so we parenthesize the intersection terms.
827
        if (op_stack.back() == OP_INTERSECTION) {
96✔
828
          add_parentheses(pos_stack.back());
48✔
829
        } else {
830
          add_parentheses(i);
48✔
831
        }
832

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

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

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

853
  for (int32_t token : expression_) {
990✔
854
    if (token < OP_UNION) {
946✔
855
      // If token is not an operator, add it to output
856
      rpn.push_back(token);
396✔
857
    } else if (token < OP_RIGHT_PAREN) {
550✔
858
      // Regular operators union, intersection, complement
859
      while (stack.size() > 0) {
561✔
860
        int32_t op = stack.back();
462✔
861

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

877
      stack.push_back(token);
352✔
878

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

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

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

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

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

912
    rpn.push_back(stack.back());
44✔
913
    stack.pop_back();
88✔
914
  }
915

916
  return rpn;
44✔
917
}
44✔
918

919
//==============================================================================
920

921
std::string Region::str() const
30,341✔
922
{
923
  std::stringstream region_spec {};
30,341✔
924
  if (!expression_.empty()) {
30,341✔
925
    for (int32_t token : expression_) {
93,129✔
926
      if (token == OP_LEFT_PAREN) {
71,216✔
927
        region_spec << " (";
1,494✔
928
      } else if (token == OP_RIGHT_PAREN) {
69,722✔
929
        region_spec << " )";
1,494✔
930
      } else if (token == OP_COMPLEMENT) {
68,228!
931
        region_spec << " ~";
×
932
      } else if (token == OP_INTERSECTION) {
68,228✔
933
      } else if (token == OP_UNION) {
64,397✔
934
        region_spec << " |";
3,479✔
935
      } else {
936
        // Note the off-by-one indexing
937
        auto surf_id = model::surfaces[abs(token) - 1]->id_;
60,918✔
938
        region_spec << " " << ((token > 0) ? surf_id : -surf_id);
60,918✔
939
      }
940
    }
941
  }
942
  return region_spec.str();
60,682✔
943
}
30,341✔
944

945
//==============================================================================
946

947
std::pair<double, int32_t> Region::distance(
2,147,483,647✔
948
  Position r, Direction u, int32_t on_surface) const
949
{
950
  double min_dist {INFTY};
2,147,483,647✔
951
  int32_t i_surf {std::numeric_limits<int32_t>::max()};
2,147,483,647✔
952

953
  // Implicit surfaces are deferred until we know min_dist from the
954
  // analytical surfaces, which bounds the solver interval.
955
  std::vector<int32_t> implicit_tokens;
2,147,483,647✔
956
  for (int32_t token : expression_) {
2,147,483,647✔
957
    // Ignore this token if it corresponds to an operator rather than a region.
958
    if (token >= OP_UNION)
2,147,483,647✔
959
      continue;
686,281,145✔
960

961
    // Calculate the distance to this surface.
962
    // Note the off-by-one indexing
963
    Surface* surf = model::surfaces[std::abs(token) - 1].get();
2,147,483,647✔
964

965
    // Defer implicit surfaces to pass 2.
966
    if (surf->geom_type() == GeometryType::IMP) {
2,147,483,647✔
967
      implicit_tokens.push_back(token);
29,493,563✔
968
      continue;
29,493,563✔
969
    }
970

971
    bool coincident {std::abs(token) == std::abs(on_surface)};
2,147,483,647✔
972
    double d {surf->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
  // Finite region check
984
  if (!implicit_tokens.empty() && min_dist == INFTY) {
2,147,483,647!
985
    // Ensure we are actually in the region: False errors sometimes comes with
986
    // SolidRayTracing
NEW
987
    bool isInCell = true;
×
NEW
988
    for (int32_t token : expression_) {
×
NEW
989
      if (token >= OP_UNION)
×
NEW
990
        continue;
×
NEW
991
      Surface* surf = model::surfaces[std::abs(token) - 1].get();
×
NEW
992
      if (surf->geom_type() != GeometryType::CSG)
×
NEW
993
        continue;
×
994

NEW
995
      double f = surf->evaluate(r);
×
NEW
996
      bool in_halfspace = (token < 0) ? (f < 0.) : (f > 0.);
×
NEW
997
      if (!in_halfspace) {
×
998
        isInCell = false;
999
        break;
1000
      }
1001
    }
1002
    // If we are actually in the region: throw error, if not, return min dist.
NEW
1003
    if (isInCell) {
×
NEW
1004
      fatal_error(
×
1005
        "An implicit surface belongs to a region with no finite analytical "
1006
        "boundary. Implicit surfaces must be enclosed in a finite region "
1007
        "defined by standard surfaces (planes, spheres, cylinders, etc.)."
NEW
1008
        "r=(" +
×
NEW
1009
        std::to_string(r.x) + ", " + std::to_string(r.y) + ", " +
×
NEW
1010
        std::to_string(r.z) +
×
1011
        ")"
NEW
1012
        "u=(" +
×
NEW
1013
        std::to_string(u.x) + ", " + std::to_string(u.y) + ", " +
×
NEW
1014
        std::to_string(u.z) + ")");
×
1015
    } else {
NEW
1016
      return {min_dist, i_surf};
×
1017
    }
1018
  }
1019

1020
  // Implicit surfaces treatment
1021
  for (int32_t token : implicit_tokens) {
2,147,483,647✔
1022
    auto* surf =
29,493,563✔
1023
      static_cast<SurfaceImplicit*>(model::surfaces[std::abs(token) - 1].get());
29,493,563✔
1024

1025
    bool coincident {std::abs(token) == std::abs(on_surface)};
29,493,563✔
1026
    double d {surf->distance_finite(r, u, coincident, min_dist)};
29,493,563✔
1027

1028
    if (d < min_dist) {
29,493,563✔
1029
      if (min_dist - d >= FP_PRECISION * min_dist) {
27,615,335!
1030
        min_dist = d;
27,615,335✔
1031
        i_surf = -token;
27,615,335✔
1032
      }
1033
    }
1034
  }
1035

1036
  return {min_dist, i_surf};
2,147,483,647✔
1037
}
2,147,483,647✔
1038

1039
//==============================================================================
1040

1041
bool Region::contains(Position r, Direction u, int32_t on_surface) const
2,147,483,647✔
1042
{
1043
  if (simple_) {
2,147,483,647✔
1044
    return contains_simple(r, u, on_surface);
2,147,483,647✔
1045
  } else {
1046
    return contains_complex(r, u, on_surface);
19,321,118✔
1047
  }
1048
}
1049

1050
//==============================================================================
1051

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

1073
//==============================================================================
1074

1075
bool Region::contains_complex(Position r, Direction u, int32_t on_surface) const
19,321,118✔
1076
{
1077
  bool in_cell = true;
19,321,118✔
1078
  int total_depth = 0;
19,321,118✔
1079

1080
  // For each token
1081
  for (auto it = expression_.begin(); it != expression_.end(); it++) {
373,032,482✔
1082
    int32_t token = *it;
362,334,761✔
1083

1084
    // If the token is a surface evaluate the sense
1085
    // If the token is a union or intersection check to
1086
    // short circuit
1087
    if (token < OP_UNION) {
362,334,761✔
1088
      if (token == on_surface) {
130,235,676✔
1089
        in_cell = true;
1090
      } else if (-token == on_surface) {
118,635,689✔
1091
        in_cell = false;
1092
      } else {
1093
        // Note the off-by-one indexing
1094
        bool sense = model::surfaces[abs(token) - 1]->sense(r, u);
114,744,718✔
1095
        in_cell = (sense == (token > 0));
114,744,718✔
1096
      }
1097
    } else if ((token == OP_UNION && in_cell == true) ||
232,099,085✔
1098
               (token == OP_INTERSECTION && in_cell == false)) {
101,837,399✔
1099
      // If the total depth is zero return
1100
      if (total_depth == 0) {
39,255,835✔
1101
        return in_cell;
8,623,397✔
1102
      }
1103

1104
      total_depth--;
30,632,438✔
1105

1106
      // While the iterator is within the bounds of the vector
1107
      int depth = 1;
30,632,438✔
1108
      do {
537,808,024✔
1109
        // Get next token
1110
        it++;
537,808,024✔
1111
        int32_t next_token = *it;
537,808,024✔
1112

1113
        // If the token is an a parenthesis
1114
        if (next_token > OP_COMPLEMENT) {
537,808,024✔
1115
          // Adjust depth accordingly
1116
          if (next_token == OP_RIGHT_PAREN) {
158,975,774✔
1117
            depth--;
94,804,106✔
1118
          } else {
1119
            depth++;
64,171,668✔
1120
          }
1121
        }
1122
      } while (depth > 0);
537,808,024✔
1123
    } else if (token == OP_LEFT_PAREN) {
192,843,250✔
1124
      total_depth++;
56,280,565✔
1125
    } else if (token == OP_RIGHT_PAREN) {
136,562,685✔
1126
      total_depth--;
25,648,127✔
1127
    }
1128
  }
1129
  return in_cell;
1130
}
1131

1132
//==============================================================================
1133

1134
BoundingBox Region::bounding_box(int32_t cell_id) const
88✔
1135
{
1136
  if (simple_) {
88✔
1137
    return bounding_box_simple();
44✔
1138
  } else {
1139
    auto postfix = generate_postfix(cell_id);
44✔
1140
    return bounding_box_complex(postfix);
88✔
1141
  }
44✔
1142
}
1143

1144
//==============================================================================
1145

1146
BoundingBox Region::bounding_box_simple() const
44✔
1147
{
1148
  BoundingBox bbox;
44✔
1149
  for (int32_t token : expression_) {
176✔
1150
    bbox &= model::surfaces[abs(token) - 1]->bounding_box(token > 0);
132✔
1151
  }
1152
  return bbox;
44✔
1153
}
1154

1155
//==============================================================================
1156

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

1162
  for (auto& token : postfix) {
792✔
1163
    if (token == OP_UNION) {
748✔
1164
      stack[i_stack - 1] = stack[i_stack - 1] | stack[i_stack];
154✔
1165
      i_stack--;
154✔
1166
    } else if (token == OP_INTERSECTION) {
594✔
1167
      stack[i_stack - 1] = stack[i_stack - 1] & stack[i_stack];
198✔
1168
      i_stack--;
198✔
1169
    } else {
1170
      i_stack++;
396✔
1171
      stack[i_stack] = model::surfaces[abs(token) - 1]->bounding_box(token > 0);
396✔
1172
    }
1173
  }
1174

1175
  assert(i_stack == 0);
44!
1176
  return stack.front();
44✔
1177
}
44✔
1178

1179
//==============================================================================
1180

1181
vector<int32_t> Region::surfaces() const
5,270✔
1182
{
1183
  if (simple_) {
5,270✔
1184
    return expression_;
5,250✔
1185
  }
1186

1187
  vector<int32_t> surfaces = expression_;
20✔
1188

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

1192
  while (it != surfaces.end()) {
60✔
1193
    surfaces.erase(it);
40✔
1194

1195
    it = std::find_if(surfaces.begin(), surfaces.end(),
40✔
1196
      [&](const auto& value) { return value >= OP_UNION; });
80!
1197
  }
1198

1199
  return surfaces;
20✔
1200
}
5,270✔
1201

1202
//==============================================================================
1203
// Non-method functions
1204
//==============================================================================
1205

1206
void read_cells(pugi::xml_node node)
9,021✔
1207
{
1208
  // Count the number of cells.
1209
  int n_cells = 0;
9,021✔
1210
  for (pugi::xml_node cell_node : node.children("cell")) {
45,267✔
1211
    n_cells++;
36,246✔
1212
  }
1213

1214
  // Loop over XML cell elements and populate the array.
1215
  model::cells.reserve(n_cells);
9,021✔
1216
  for (pugi::xml_node cell_node : node.children("cell")) {
45,267✔
1217
    model::cells.push_back(make_unique<CSGCell>(cell_node));
36,246✔
1218
  }
1219

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

1232
  read_dagmc_universes(node);
9,021✔
1233

1234
  populate_universes();
9,019✔
1235

1236
  // Allocate the cell overlap count if necessary.
1237
  if (settings::check_overlaps) {
9,019✔
1238
    model::overlap_check_count.resize(model::cells.size(), 0);
119✔
1239
  }
1240

1241
  if (model::cells.size() == 0) {
9,019!
1242
    fatal_error("No cells were found in the geometry.xml file");
×
1243
  }
1244
}
9,019✔
1245

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

1252
  // Populate the Universe vector and map.
1253
  for (int index_cell = 0; index_cell < model::cells.size(); index_cell++) {
45,468✔
1254
    int32_t uid = model::cells[index_cell]->universe_;
36,447✔
1255
    auto it = model::universe_map.find(uid);
36,447✔
1256
    if (it == model::universe_map.end()) {
36,447✔
1257
      model::universes.push_back(make_unique<Universe>());
41,658✔
1258
      model::universes.back()->id_ = uid;
20,829✔
1259
      model::universes.back()->cells_.push_back(index_cell);
20,829✔
1260
      model::universe_map[uid] = model::universes.size() - 1;
20,829✔
1261
    } else {
1262
#ifdef OPENMC_DAGMC_ENABLED
1263
      // Skip implicit complement cells for now
1264
      Universe* univ = model::universes[it->second].get();
2,072!
1265
      DAGUniverse* dag_univ = dynamic_cast<DAGUniverse*>(univ);
2,072!
1266
      if (dag_univ && (dag_univ->implicit_complement_idx() == index_cell)) {
2,072✔
1267
        implicit_comp_cells[it->second] = index_cell;
43✔
1268
        continue;
43✔
1269
      }
1270
#endif
1271

1272
      model::universes[it->second]->cells_.push_back(index_cell);
15,575✔
1273
    }
1274
  }
1275

1276
  // Add DAGUniverse implicit complement cells last
1277
  for (const auto& it : implicit_comp_cells) {
9,064✔
1278
    int index_univ = it.first;
43✔
1279
    int index_cell = it.second;
43✔
1280
    model::universes[index_univ]->cells_.push_back(index_cell);
43!
1281
  }
1282

1283
  model::universes.shrink_to_fit();
9,021✔
1284
}
9,021✔
1285

1286
//==============================================================================
1287
// C-API functions
1288
//==============================================================================
1289

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

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

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

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

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

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

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

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

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

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

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

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

1432
  BoundingBox bbox;
55✔
1433

1434
  const auto& c = model::cells[index];
55✔
1435
  bbox = c->bounding_box();
55✔
1436

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

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

1447
  return 0;
55✔
1448
}
1449

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

1458
  *name = model::cells[index]->name().data();
413✔
1459

1460
  return 0;
413✔
1461
}
1462

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

1471
  model::cells[index]->set_name(name);
22✔
1472

1473
  return 0;
11✔
1474
}
1475

1476
//==============================================================================
1477
//! Define a containing (parent) cell
1478
//==============================================================================
1479

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

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

1495
  int64_t cell_index;
1496
  int64_t lattice_index;
1497
};
1498

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

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

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

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

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

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

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

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

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

1565
  // Accessors
1566
  vector<ParentCell>& parent_cells() { return parent_cells_; }
136✔
1567
  const vector<ParentCell>& parent_cells() const { return parent_cells_; }
1568

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

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

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

1584
  return find_parent_cells(instance, dummy_particle);
×
1585
}
×
1586

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

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

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

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

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

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

1637
  while (true) {
211✔
1638
    const auto& univ = model::universes[univ_idx];
211✔
1639
    prev_univ_idx = univ_idx;
211✔
1640

1641
    // search for a cell that is filled w/ this universe
1642
    for (const auto& cell : model::cells) {
1,429✔
1643
      // if this is a material-filled cell, move on
1644
      if (cell->type_ == Fill::MATERIAL)
1,323✔
1645
        continue;
782✔
1646

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

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

1664
        // start search for universe
1665
        auto lat_it = lattice_univs.begin();
225✔
1666
        while (true) {
495✔
1667
          // find the next lattice cell with this universe
1668
          lat_it = std::find(lat_it, lattice_univs.end(), univ_idx);
360✔
1669
          if (lat_it == lattice_univs.end())
360✔
1670
            break;
1671

1672
          int lattice_idx = lat_it - lattice_univs.begin();
240✔
1673

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

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

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

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

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

1709
  } // end while
1710

1711
  // reverse the stack so the highest cell comes first
1712
  std::reverse(stack.parent_cells().begin(), stack.parent_cells().end());
136✔
1713
  return stack.parent_cells();
272✔
1714
}
136✔
1715

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

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

1725
  // find the pathway through the geometry to this cell
1726
  vector<ParentCell> parent_cells;
136!
1727

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

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

1738
  return contained_cells;
136✔
1739
}
181✔
1740

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

1747
  // filled by material, determine instance based on parent cells
1748
  if (type_ == Fill::MATERIAL) {
134,178✔
1749
    int instance = 0;
133,532✔
1750
    if (this->distribcell_index_ >= 0) {
133,532!
1751
      for (auto& parent_cell : parent_cells) {
400,594✔
1752
        auto& cell = model::cells[parent_cell.cell_index];
267,062✔
1753
        if (cell->type_ == Fill::UNIVERSE) {
267,062✔
1754
          instance += cell->offset_[distribcell_index_];
132,032✔
1755
        } else if (cell->type_ == Fill::LATTICE) {
135,030!
1756
          auto& lattice = model::lattices[cell->fill_];
135,030✔
1757
          instance += lattice->offset(
135,030✔
1758
            this->distribcell_index_, parent_cell.lattice_index);
135,030✔
1759
        }
1760
      }
1761
    }
1762
    // add entry to contained cells
1763
    contained_cells[model::cell_map[id_]].push_back(instance);
133,532✔
1764
    // filled with universe, add the containing cell to the parent cells
1765
    // and recurse
1766
  } else if (type_ == Fill::UNIVERSE) {
646✔
1767
    parent_cells.push_back({model::cell_map[id_], -1});
526✔
1768
    auto& univ = model::universes[fill_];
526✔
1769
    for (auto cell_index : univ->cells_) {
3,033✔
1770
      auto& cell = model::cells[cell_index];
2,507✔
1771
      cell->get_contained_cells_inner(contained_cells, parent_cells);
2,507✔
1772
    }
1773
    parent_cells.pop_back();
526✔
1774
    // filled with a lattice, visit each universe in the lattice
1775
    // with a recursive call to collect the cell instances
1776
  } else if (type_ == Fill::LATTICE) {
120!
1777
    auto& lattice = model::lattices[fill_];
120✔
1778
    for (auto i = lattice->begin(); i != lattice->end(); ++i) {
131,340✔
1779
      auto& univ = model::universes[*i];
131,220✔
1780
      parent_cells.push_back({model::cell_map[id_], i.indx_});
131,220✔
1781
      for (auto cell_index : univ->cells_) {
262,755✔
1782
        auto& cell = model::cells[cell_index];
131,535✔
1783
        cell->get_contained_cells_inner(contained_cells, parent_cells);
131,535✔
1784
      }
1785
      parent_cells.pop_back();
131,220✔
1786
    }
1787
  }
1788
}
134,178✔
1789

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

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

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

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

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

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

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

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

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

1921
extern "C" int cells_size()
99✔
1922
{
1923
  return model::cells.size();
99✔
1924
}
1925

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

© 2026 Coveralls, Inc