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

openmc-dev / openmc / 35364501500

18 Sep 2026 03:46PM UTC coverage: 81.641% (+0.2%) from 81.477%
35364501500

Pull #4141

github

web-flow
Merge 315debd0e into c66d748ca
Pull Request #4141: Adding multigroup photon transport capability in MC mode

19450 of 27913 branches covered (69.68%)

Branch coverage included in aggregate %.

175 of 194 new or added lines in 8 files covered. (90.21%)

173 existing lines in 23 files now uncovered.

61794 of 71601 relevant lines covered (86.3%)

47807573.87 hits per line

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

81.93
/src/surface.cpp
1
#include "openmc/surface.h"
2

3
#include <cmath>
4
#include <complex>
5
#include <initializer_list>
6
#include <set>
7
#include <utility>
8

9
#include <fmt/core.h>
10

11
#include "openmc/array.h"
12
#include "openmc/cell.h"
13
#include "openmc/container_util.h"
14
#include "openmc/error.h"
15
#include "openmc/external/quartic_solver.h"
16
#include "openmc/hdf5_interface.h"
17
#include "openmc/math_functions.h"
18
#include "openmc/random_lcg.h"
19
#include "openmc/settings.h"
20
#include "openmc/string_utils.h"
21
#include "openmc/xml_interface.h"
22

23
namespace openmc {
24

25
//==============================================================================
26
// Global variables
27
//==============================================================================
28

29
namespace model {
30
std::unordered_map<int, int> surface_map;
31
vector<unique_ptr<Surface>> surfaces;
32
} // namespace model
33

34
//==============================================================================
35
// Helper functions for reading the "coeffs" node of an XML surface element
36
//==============================================================================
37

38
void read_coeffs(
45,293 ✔
39
  pugi::xml_node surf_node, int surf_id, std::initializer_list<double*> coeffs)
40
{
41
  // Check the given number of coefficients.
42
  auto coeffs_file = get_node_array<double>(surf_node, "coeffs");
45,293 ✔
43
  if (coeffs_file.size() != coeffs.size()) {
45,293 !
44
    fatal_error(
×
45
      fmt::format("Surface {} expects {} coefficient but was given {}", surf_id,
×
46
        coeffs.size(), coeffs_file.size()));
×
47
  }
48

49
  // Copy the coefficients
50
  int i = 0;
45,293 ✔
51
  for (auto c : coeffs) {
133,165 ✔
52
    *c = coeffs_file[i++];
87,872 ✔
53
  }
54
}
45,293 ✔
55

56
//==============================================================================
57
// Surface implementation
58
//==============================================================================
59

UNCOV
60
Surface::Surface() {} // empty constructor
×
61

62
Surface::Surface(pugi::xml_node surf_node)
45,293 ✔
63
{
64
  if (check_for_node(surf_node, "id")) {
45,293 !
65
    id_ = std::stoi(get_node_value(surf_node, "id"));
90,586 ✔
66
    if (contains(settings::source_write_surf_id, id_) ||
90,586 ✔
67
        settings::source_write_surf_id.empty()) {
44,593 ✔
68
      surf_source_ = true;
44,364 ✔
69
    }
70
  } else {
71
    fatal_error("Must specify id of surface in geometry XML file.");
×
72
  }
73

74
  if (check_for_node(surf_node, "name")) {
45,293 ✔
75
    name_ = get_node_value(surf_node, "name", false);
10,656 ✔
76
  }
77

78
  if (check_for_node(surf_node, "boundary")) {
45,293 ✔
79
    std::string surf_bc = get_node_value(surf_node, "boundary", true, true);
26,760 ✔
80

81
    if (surf_bc == "transmission" || surf_bc == "transmit" || surf_bc.empty()) {
53,520 !
82
      // Leave the bc_ a nullptr
83
    } else if (surf_bc == "vacuum") {
26,760 ✔
84
      bc_ = make_unique<VacuumBC>();
12,922 ✔
85
    } else if (surf_bc == "reflective" || surf_bc == "reflect" ||
14,398 !
86
               surf_bc == "reflecting") {
560 !
87
      bc_ = make_unique<ReflectiveBC>();
13,278 ✔
88
    } else if (surf_bc == "white") {
560 ✔
89
      bc_ = make_unique<WhiteBC>();
78 ✔
90
    } else if (surf_bc == "periodic") {
482 !
91
      // Periodic BCs are handled separately
92
    } else {
93
      fatal_error(fmt::format("Unknown boundary condition \"{}\" specified "
×
94
                              "on surface {}",
95
        surf_bc, id_));
×
96
    }
97

98
    if (check_for_node(surf_node, "albedo") && bc_) {
26,760 !
99
      double surf_alb = std::stod(get_node_value(surf_node, "albedo"));
156 ✔
100

101
      if (surf_alb < 0.0)
78 !
102
        fatal_error(fmt::format("Surface {} has an albedo of {}. "
×
103
                                "Albedo values must be positive.",
104
          id_, surf_alb));
×
105

106
      if (surf_alb > 1.0)
78 !
107
        warning(fmt::format("Surface {} has an albedo of {}. "
×
108
                            "Albedos greater than 1 may cause "
109
                            "unphysical behaviour.",
110
          id_, surf_alb));
×
111

112
      bc_->set_albedo(surf_alb);
78 ✔
113
    }
114
  }
26,760 ✔
115
}
45,293 ✔
116

117
bool Surface::sense(Position r, Direction u) const
2,147,483,647 ✔
118
{
119
  // Evaluate the surface equation at the particle's coordinates to determine
120
  // which side the particle is on.
121
  const double f = evaluate(r);
2,147,483,647 ✔
122

123
  // Check which side of surface the point is on.
124
  if (std::abs(f) < FP_COINCIDENT) {
2,147,483,647 ✔
125
    // Particle may be coincident with this surface. To determine the sense, we
126
    // look at the direction of the particle relative to the surface normal (by
127
    // default in the positive direction) via their dot product.
128
    return u.dot(normal(r)) > 0.0;
9,100,588 ✔
129
  }
130
  return f > 0.0;
2,147,483,647 ✔
131
}
132

133
Direction Surface::reflect(Position r, Direction u, GeometryState* p) const
891,085,536 ✔
134
{
135
  // Determine projection of direction onto normal and squared magnitude of
136
  // normal.
137
  Direction n = normal(r);
891,085,536 ✔
138

139
  // Reflect direction according to normal.
140
  return u.reflect(n);
891,085,536 ✔
141
}
142

143
Direction Surface::diffuse_reflect(
915,590 ✔
144
  Position r, Direction u, uint64_t* seed) const
145
{
146
  // Diffuse reflect direction according to the normal.
147
  // cosine distribution
148

149
  Direction n = this->normal(r);
915,590 ✔
150
  n /= n.norm();
915,590 ✔
151
  const double projection = n.dot(u);
915,590 ✔
152

153
  // sample from inverse function, u=sqrt(rand) since p(u)=2u, so F(u)=u^2
154
  const double mu =
915,590 ✔
155
    (projection >= 0.0) ? -std::sqrt(prn(seed)) : std::sqrt(prn(seed));
915,590 ✔
156

157
  // sample azimuthal distribution uniformly
158
  u = rotate_angle(n, mu, nullptr, seed);
915,590 ✔
159

160
  // normalize the direction
161
  return u / u.norm();
915,590 ✔
162
}
163

164
void Surface::to_hdf5(hid_t group_id) const
37,559 ✔
165
{
166
  hid_t surf_group = create_group(group_id, fmt::format("surface {}", id_));
37,559 ✔
167

168
  if (geom_type() == GeometryType::DAG) {
37,559 !
UNCOV
169
    write_string(surf_group, "geom_type", "dagmc", false);
×
170
  } else if (geom_type() == GeometryType::CSG) {
37,559 !
171
    write_string(surf_group, "geom_type", "csg", false);
37,559 ✔
172

173
    if (bc_) {
37,559 ✔
174
      write_string(surf_group, "boundary_type", bc_->type(), false);
22,756 ✔
175
      bc_->to_hdf5(surf_group);
22,756 ✔
176

177
      // write periodic surface ID
178
      if (bc_->type() == "periodic") {
22,756 ✔
179
        auto pbc = dynamic_cast<PeriodicBC*>(bc_.get());
416 !
180
        Surface& surf1 {*model::surfaces[pbc->i_surf()]};
416 !
181
        Surface& surf2 {*model::surfaces[pbc->j_surf()]};
416 !
182

183
        if (id_ == surf1.id_) {
416 !
184
          write_dataset(surf_group, "periodic_surface_id", surf2.id_);
416 ✔
185
        } else {
186
          write_dataset(surf_group, "periodic_surface_id", surf1.id_);
×
187
        }
188
      }
189
    } else {
190
      write_string(surf_group, "boundary_type", "transmission", false);
29,606 ✔
191
    }
192
  }
193

194
  if (!name_.empty()) {
37,559 ✔
195
    write_string(surf_group, "name", name_, false);
9,010 ✔
196
  }
197

198
  to_hdf5_inner(surf_group);
37,559 ✔
199

200
  close_group(surf_group);
37,559 ✔
201
}
37,559 ✔
202

203
//==============================================================================
204
// Generic functions for x-, y-, and z-, planes.
205
//==============================================================================
206

207
// The template parameter indicates the axis normal to the plane.
208
template<int i>
209
double axis_aligned_plane_distance(
2,147,483,647 ✔
210
  Position r, Direction u, bool coincident, double offset)
211
{
212
  const double f = offset - r[i];
2,147,483,647 ✔
213
  if (coincident || std::abs(f) < FP_COINCIDENT || u[i] == 0.0)
2,147,483,647 ✔
214
    return INFTY;
215
  const double d = f / u[i];
2,147,483,647 ✔
216
  if (d < 0.0)
2,147,483,647 ✔
217
    return INFTY;
2,147,483,647 ✔
218
  return d;
219
}
220

221
//==============================================================================
222
// SurfaceXPlane implementation
223
//==============================================================================
224

225
SurfaceXPlane::SurfaceXPlane(pugi::xml_node surf_node) : Surface(surf_node)
11,995 ✔
226
{
227
  read_coeffs(surf_node, id_, {&x0_});
11,995 ✔
228
}
11,995 ✔
229

230
double SurfaceXPlane::evaluate(Position r) const
2,147,483,647 ✔
231
{
232
  return r.x - x0_;
2,147,483,647 ✔
233
}
234

235
double SurfaceXPlane::distance(Position r, Direction u, bool coincident) const
2,147,483,647 ✔
236
{
237
  return axis_aligned_plane_distance<0>(r, u, coincident, x0_);
2,147,483,647 ✔
238
}
239

240
Direction SurfaceXPlane::normal(Position r) const
338,775,270 ✔
241
{
242
  return {1., 0., 0.};
338,775,270 ✔
243
}
244

245
void SurfaceXPlane::to_hdf5_inner(hid_t group_id) const
9,414 ✔
246
{
247
  write_string(group_id, "type", "x-plane", false);
9,414 ✔
248
  array<double, 1> coeffs {{x0_}};
9,414 ✔
249
  write_dataset(group_id, "coefficients", coeffs);
9,414 ✔
250
}
9,414 ✔
251

252
BoundingBox SurfaceXPlane::bounding_box(bool pos_side) const
220 ✔
253
{
254
  if (pos_side) {
220 ✔
255
    return {{x0_, -INFTY, -INFTY}, {INFTY, INFTY, INFTY}};
110 ✔
256
  } else {
257
    return {{-INFTY, -INFTY, -INFTY}, {x0_, INFTY, INFTY}};
110 ✔
258
  }
259
}
260

261
//==============================================================================
262
// SurfaceYPlane implementation
263
//==============================================================================
264

265
SurfaceYPlane::SurfaceYPlane(pugi::xml_node surf_node) : Surface(surf_node)
10,107 ✔
266
{
267
  read_coeffs(surf_node, id_, {&y0_});
10,107 ✔
268
}
10,107 ✔
269

270
double SurfaceYPlane::evaluate(Position r) const
2,147,483,647 ✔
271
{
272
  return r.y - y0_;
2,147,483,647 ✔
273
}
274

275
double SurfaceYPlane::distance(Position r, Direction u, bool coincident) const
2,147,483,647 ✔
276
{
277
  return axis_aligned_plane_distance<1>(r, u, coincident, y0_);
2,147,483,647 ✔
278
}
279

280
Direction SurfaceYPlane::normal(Position r) const
475,112,351 ✔
281
{
282
  return {0., 1., 0.};
475,112,351 ✔
283
}
284

285
void SurfaceYPlane::to_hdf5_inner(hid_t group_id) const
8,666 ✔
286
{
287
  write_string(group_id, "type", "y-plane", false);
8,666 ✔
288
  array<double, 1> coeffs {{y0_}};
8,666 ✔
289
  write_dataset(group_id, "coefficients", coeffs);
8,666 ✔
290
}
8,666 ✔
291

292
BoundingBox SurfaceYPlane::bounding_box(bool pos_side) const
230 ✔
293
{
294
  if (pos_side) {
230 ✔
295
    return {{-INFTY, y0_, -INFTY}, {INFTY, INFTY, INFTY}};
110 ✔
296
  } else {
297
    return {{-INFTY, -INFTY, -INFTY}, {INFTY, y0_, INFTY}};
120 ✔
298
  }
299
}
300

301
//==============================================================================
302
// SurfaceZPlane implementation
303
//==============================================================================
304

305
SurfaceZPlane::SurfaceZPlane(pugi::xml_node surf_node) : Surface(surf_node)
7,462 ✔
306
{
307
  read_coeffs(surf_node, id_, {&z0_});
7,462 ✔
308
}
7,462 ✔
309

310
double SurfaceZPlane::evaluate(Position r) const
2,147,483,647 ✔
311
{
312
  return r.z - z0_;
2,147,483,647 ✔
313
}
314

315
double SurfaceZPlane::distance(Position r, Direction u, bool coincident) const
2,147,483,647 ✔
316
{
317
  return axis_aligned_plane_distance<2>(r, u, coincident, z0_);
2,147,483,647 ✔
318
}
319

320
Direction SurfaceZPlane::normal(Position r) const
235,246,975 ✔
321
{
322
  return {0., 0., 1.};
235,246,975 ✔
323
}
324

325
void SurfaceZPlane::to_hdf5_inner(hid_t group_id) const
6,608 ✔
326
{
327
  write_string(group_id, "type", "z-plane", false);
6,608 ✔
328
  array<double, 1> coeffs {{z0_}};
6,608 ✔
329
  write_dataset(group_id, "coefficients", coeffs);
6,608 ✔
330
}
6,608 ✔
331

332
BoundingBox SurfaceZPlane::bounding_box(bool pos_side) const
×
333
{
334
  if (pos_side) {
×
335
    return {{-INFTY, -INFTY, z0_}, {INFTY, INFTY, INFTY}};
×
336
  } else {
337
    return {{-INFTY, -INFTY, -INFTY}, {INFTY, INFTY, z0_}};
×
338
  }
339
}
340

341
//==============================================================================
342
// SurfacePlane implementation
343
//==============================================================================
344

345
SurfacePlane::SurfacePlane(pugi::xml_node surf_node) : Surface(surf_node)
1,912 ✔
346
{
347
  read_coeffs(surf_node, id_, {&A_, &B_, &C_, &D_});
1,912 ✔
348
}
1,912 ✔
349

350
double SurfacePlane::evaluate(Position r) const
329,064,969 ✔
351
{
352
  return A_ * r.x + B_ * r.y + C_ * r.z - D_;
329,064,969 ✔
353
}
354

355
BoundingBox SurfacePlane::bounding_box(bool pos_side) const
140 ✔
356
{
357
  // A general plane bounds a half-space in one direction only when its normal
358
  // is parallel to a coordinate axis; otherwise both half-spaces are unbounded
359
  // along every axis. This mirrors PlaneMixin.bounding_box on the Python side,
360
  // so that a plane whose off-axis coefficients are rotation-matrix roundoff
361
  // (e.g. B = 1 with A = C = 6.1e-17) yields the same box through both APIs.
362
  const Direction n = normal({});
140 ✔
363
  const double norm = n.norm();
140 ✔
364
  if (norm == 0.0)
140 ✔
365
    return {};
10 ✔
366

367
  int axis = -1;
360 ✔
368
  for (int i = 0; i < 3; ++i) {
360 ✔
369
    if (std::abs(std::abs(n[i] / norm) - 1.0) > PLANE_ALIGNMENT_TOL)
310 ✔
370
      continue;
210 ✔
371

372
    bool aligned = true;
360 ✔
373
    for (int j = 0; j < 3; ++j) {
360 ✔
374
      if (j != i && std::abs(n[j] / norm) > PLANE_ALIGNMENT_TOL) {
280 ✔
375
        aligned = false;
376
        break;
377
      }
378
    }
379
    if (aligned) {
100 ✔
380
      axis = i;
381
      break;
382
    }
383
  }
384
  if (axis == -1)
130 ✔
385
    return {};
50 ✔
386

387
  // The half-space is bounded below when the outward normal points along the
388
  // positive axis direction and we are on the positive side, or vice versa.
389
  BoundingBox bbox;
80 ✔
390
  const double intercept = D_ / n[axis];
80 ✔
391
  if (pos_side == (n[axis] > 0.0)) {
80 ✔
392
    bbox.min[axis] = intercept;
50 ✔
393
  } else {
394
    bbox.max[axis] = intercept;
30 ✔
395
  }
396
  return bbox;
80 ✔
397
}
398

399
double SurfacePlane::distance(Position r, Direction u, bool coincident) const
746,192,018 ✔
400
{
401
  const double f = A_ * r.x + B_ * r.y + C_ * r.z - D_;
746,192,018 ✔
402
  const double projection = A_ * u.x + B_ * u.y + C_ * u.z;
746,192,018 ✔
403
  if (coincident || std::abs(f) < FP_COINCIDENT || projection == 0.0) {
746,192,018 !
404
    return INFTY;
405
  } else {
406
    const double d = -f / projection;
684,549,644 ✔
407
    if (d < 0.0)
684,549,644 ✔
408
      return INFTY;
409
    return d;
373,070,833 ✔
410
  }
411
}
412

413
Direction SurfacePlane::normal(Position r) const
5,233,983 ✔
414
{
415
  return {A_, B_, C_};
5,233,983 ✔
416
}
417

418
void SurfacePlane::to_hdf5_inner(hid_t group_id) const
1,438 ✔
419
{
420
  write_string(group_id, "type", "plane", false);
1,438 ✔
421
  array<double, 4> coeffs {{A_, B_, C_, D_}};
1,438 ✔
422
  write_dataset(group_id, "coefficients", coeffs);
1,438 ✔
423
}
1,438 ✔
424

425
//==============================================================================
426
// Generic functions for x-, y-, and z-, cylinders
427
//==============================================================================
428

429
// The template parameters indicate the axes perpendicular to the axis of the
430
// cylinder.  offset1 and offset2 should correspond with i1 and i2,
431
// respectively.
432
template<int i1, int i2>
433
double axis_aligned_cylinder_evaluate(
2,147,483,647 ✔
434
  Position r, double offset1, double offset2, double radius)
435
{
436
  const double r1 = r.get<i1>() - offset1;
2,147,483,647 ✔
437
  const double r2 = r.get<i2>() - offset2;
2,147,483,647 ✔
438
  return r1 * r1 + r2 * r2 - radius * radius;
2,147,483,647 ✔
439
}
440

441
// The first template parameter indicates which axis the cylinder is aligned to.
442
// The other two parameters indicate the other two axes.  offset1 and offset2
443
// should correspond with i2 and i3, respectively.
444
template<int i1, int i2, int i3>
445
double axis_aligned_cylinder_distance(Position r, Direction u, bool coincident,
2,147,483,647 ✔
446
  double offset1, double offset2, double radius)
447
{
448
  const double a = 1.0 - u.get<i1>() * u.get<i1>(); // u^2 + v^2
2,147,483,647 ✔
449
  if (a == 0.0)
2,147,483,647 ✔
450
    return INFTY;
451

452
  const double r2 = r.get<i2>() - offset1;
2,147,483,647 ✔
453
  const double r3 = r.get<i3>() - offset2;
2,147,483,647 ✔
454
  const double k = r2 * u.get<i2>() + r3 * u.get<i3>();
2,147,483,647 ✔
455
  const double c = r2 * r2 + r3 * r3 - radius * radius;
2,147,483,647 ✔
456
  const double quad = k * k - a * c;
2,147,483,647 ✔
457

458
  if (quad < 0.0) {
2,147,483,647 ✔
459
    // No intersection with cylinder.
460
    return INFTY;
461

462
  } else if (coincident || std::abs(c) < FP_COINCIDENT) {
2,147,483,647 ✔
463
    // Particle is on the cylinder, thus one distance is positive/negative
464
    // and the other is zero. The sign of k determines if we are facing in or
465
    // out.
466
    if (k >= 0.0) {
1,734,881,764 ✔
467
      return INFTY;
468
    } else {
469
      return (-k + sqrt(quad)) / a;
991,343,779 ✔
470
    }
471

472
  } else if (c < 0.0) {
2,147,483,647 ✔
473
    // Particle is inside the cylinder, thus one distance must be negative
474
    // and one must be positive. The positive distance will be the one with
475
    // negative sign on sqrt(quad).
476
    return (-k + sqrt(quad)) / a;
978,961,847 ✔
477

478
  } else {
479
    // Particle is outside the cylinder, thus both distances are either
480
    // positive or negative. If positive, the smaller distance is the one
481
    // with positive sign on sqrt(quad).
482
    const double d = (-k - sqrt(quad)) / a;
1,333,349,026 ✔
483
    if (d < 0.0)
1,333,349,026 ✔
484
      return INFTY;
485
    return d;
1,039,083,100 ✔
486
  }
487
}
488

489
// The first template parameter indicates which axis the cylinder is aligned to.
490
// The other two parameters indicate the other two axes.  offset1 and offset2
491
// should correspond with i2 and i3, respectively.
492
template<int i1, int i2, int i3>
493
Direction axis_aligned_cylinder_normal(
255,544,567 ✔
494
  Position r, double offset1, double offset2)
495
{
496
  Direction u;
255,544,567 ✔
497
  u.get<i2>() = 2.0 * (r.get<i2>() - offset1);
255,544,567 ✔
498
  u.get<i3>() = 2.0 * (r.get<i3>() - offset2);
255,544,567 ✔
499
  u.get<i1>() = 0.0;
500
  return u;
501
}
502

503
//==============================================================================
504
// SurfaceXCylinder implementation
505
//==============================================================================
506

507
SurfaceXCylinder::SurfaceXCylinder(pugi::xml_node surf_node)
39 ✔
508
  : Surface(surf_node)
39 ✔
509
{
510
  read_coeffs(surf_node, id_, {&y0_, &z0_, &radius_});
39 ✔
511
}
39 ✔
512

513
double SurfaceXCylinder::evaluate(Position r) const
1,161,342 ✔
514
{
515
  return axis_aligned_cylinder_evaluate<1, 2>(r, y0_, z0_, radius_);
1,161,342 ✔
516
}
517

518
double SurfaceXCylinder::distance(
1,474,580 ✔
519
  Position r, Direction u, bool coincident) const
520
{
521
  return axis_aligned_cylinder_distance<0, 1, 2>(
1,474,580 ✔
522
    r, u, coincident, y0_, z0_, radius_);
1,474,580 ✔
523
}
524

525
Direction SurfaceXCylinder::normal(Position r) const
361,760 ✔
526
{
527
  return axis_aligned_cylinder_normal<0, 1, 2>(r, y0_, z0_);
361,760 ✔
528
}
529

530
void SurfaceXCylinder::to_hdf5_inner(hid_t group_id) const
30 ✔
531
{
532
  write_string(group_id, "type", "x-cylinder", false);
30 ✔
533
  array<double, 3> coeffs {{y0_, z0_, radius_}};
30 ✔
534
  write_dataset(group_id, "coefficients", coeffs);
30 ✔
535
}
30 ✔
536

537
BoundingBox SurfaceXCylinder::bounding_box(bool pos_side) const
×
538
{
539
  if (!pos_side) {
×
540
    return {{-INFTY, y0_ - radius_, z0_ - radius_},
×
541
      {INFTY, y0_ + radius_, z0_ + radius_}};
×
542
  } else {
543
    return {};
×
544
  }
545
}
546
//==============================================================================
547
// SurfaceYCylinder implementation
548
//==============================================================================
549

550
SurfaceYCylinder::SurfaceYCylinder(pugi::xml_node surf_node)
23 ✔
551
  : Surface(surf_node)
23 ✔
552
{
553
  read_coeffs(surf_node, id_, {&x0_, &z0_, &radius_});
23 ✔
554
}
23 ✔
555

556
double SurfaceYCylinder::evaluate(Position r) const
155,440 ✔
557
{
558
  return axis_aligned_cylinder_evaluate<0, 2>(r, x0_, z0_, radius_);
155,440 ✔
559
}
560

561
double SurfaceYCylinder::distance(
599,790 ✔
562
  Position r, Direction u, bool coincident) const
563
{
564
  return axis_aligned_cylinder_distance<1, 0, 2>(
599,790 ✔
565
    r, u, coincident, x0_, z0_, radius_);
599,790 ✔
566
}
567

568
Direction SurfaceYCylinder::normal(Position r) const
×
569
{
570
  return axis_aligned_cylinder_normal<1, 0, 2>(r, x0_, z0_);
×
571
}
572

573
void SurfaceYCylinder::to_hdf5_inner(hid_t group_id) const
20 ✔
574
{
575
  write_string(group_id, "type", "y-cylinder", false);
20 ✔
576
  array<double, 3> coeffs {{x0_, z0_, radius_}};
20 ✔
577
  write_dataset(group_id, "coefficients", coeffs);
20 ✔
578
}
20 ✔
579

580
BoundingBox SurfaceYCylinder::bounding_box(bool pos_side) const
10 ✔
581
{
582
  if (!pos_side) {
10 !
583
    return {{x0_ - radius_, -INFTY, z0_ - radius_},
10 ✔
584
      {x0_ + radius_, INFTY, z0_ + radius_}};
10 ✔
585
  } else {
586
    return {};
×
587
  }
588
}
589

590
//==============================================================================
591
// SurfaceZCylinder implementation
592
//==============================================================================
593

594
SurfaceZCylinder::SurfaceZCylinder(pugi::xml_node surf_node)
5,140 ✔
595
  : Surface(surf_node)
5,140 ✔
596
{
597
  read_coeffs(surf_node, id_, {&x0_, &y0_, &radius_});
5,140 ✔
598
}
5,140 ✔
599

600
double SurfaceZCylinder::evaluate(Position r) const
2,147,483,647 ✔
601
{
602
  return axis_aligned_cylinder_evaluate<0, 1>(r, x0_, y0_, radius_);
2,147,483,647 ✔
603
}
604

605
double SurfaceZCylinder::distance(
2,147,483,647 ✔
606
  Position r, Direction u, bool coincident) const
607
{
608
  return axis_aligned_cylinder_distance<2, 0, 1>(
2,147,483,647 ✔
609
    r, u, coincident, x0_, y0_, radius_);
2,147,483,647 ✔
610
}
611

612
Direction SurfaceZCylinder::normal(Position r) const
255,182,807 ✔
613
{
614
  return axis_aligned_cylinder_normal<2, 0, 1>(r, x0_, y0_);
255,182,807 ✔
615
}
616

617
void SurfaceZCylinder::to_hdf5_inner(hid_t group_id) const
4,179 ✔
618
{
619
  write_string(group_id, "type", "z-cylinder", false);
4,179 ✔
620
  array<double, 3> coeffs {{x0_, y0_, radius_}};
4,179 ✔
621
  write_dataset(group_id, "coefficients", coeffs);
4,179 ✔
622
}
4,179 ✔
623

624
BoundingBox SurfaceZCylinder::bounding_box(bool pos_side) const
40 ✔
625
{
626
  if (!pos_side) {
40 ✔
627
    return {{x0_ - radius_, y0_ - radius_, -INFTY},
20 ✔
628
      {x0_ + radius_, y0_ + radius_, INFTY}};
20 ✔
629
  } else {
630
    return {};
20 ✔
631
  }
632
}
633

634
//==============================================================================
635
// SurfaceSphere implementation
636
//==============================================================================
637

638
SurfaceSphere::SurfaceSphere(pugi::xml_node surf_node) : Surface(surf_node)
8,351 ✔
639
{
640
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &radius_});
8,351 ✔
641
}
8,351 ✔
642

643
double SurfaceSphere::evaluate(Position r) const
1,142,313,660 ✔
644
{
645
  const double x = r.x - x0_;
1,142,313,660 ✔
646
  const double y = r.y - y0_;
1,142,313,660 ✔
647
  const double z = r.z - z0_;
1,142,313,660 ✔
648
  return x * x + y * y + z * z - radius_ * radius_;
1,142,313,660 ✔
649
}
650

651
double SurfaceSphere::distance(Position r, Direction u, bool coincident) const
1,941,304,440 ✔
652
{
653
  const double x = r.x - x0_;
1,941,304,440 ✔
654
  const double y = r.y - y0_;
1,941,304,440 ✔
655
  const double z = r.z - z0_;
1,941,304,440 ✔
656
  const double k = x * u.x + y * u.y + z * u.z;
1,941,304,440 ✔
657
  const double c = x * x + y * y + z * z - radius_ * radius_;
1,941,304,440 ✔
658
  const double quad = k * k - c;
1,941,304,440 ✔
659

660
  if (quad < 0.0) {
1,941,304,440 ✔
661
    // No intersection with sphere.
662
    return INFTY;
663

664
  } else if (coincident || std::abs(c) < FP_COINCIDENT) {
1,769,871,064 !
665
    // Particle is on the sphere, thus one distance is positive/negative and
666
    // the other is zero. The sign of k determines if we are facing in or out.
667
    if (k >= 0.0) {
107,029,102 ✔
668
      return INFTY;
669
    } else {
670
      return -k + sqrt(quad);
61,018,114 ✔
671
    }
672

673
  } else if (c < 0.0) {
1,662,841,962 ✔
674
    // Particle is inside the sphere, thus one distance must be negative and
675
    // one must be positive. The positive distance will be the one with
676
    // negative sign on sqrt(quad)
677
    return -k + sqrt(quad);
1,601,282,431 ✔
678

679
  } else {
680
    // Particle is outside the sphere, thus both distances are either positive
681
    // or negative. If positive, the smaller distance is the one with positive
682
    // sign on sqrt(quad).
683
    const double d = -k - sqrt(quad);
61,559,531 ✔
684
    if (d < 0.0)
61,559,531 ✔
685
      return INFTY;
686
    return d;
48,094,163 ✔
687
  }
688
}
689

690
Direction SurfaceSphere::normal(Position r) const
354,297,100 ✔
691
{
692
  return {2.0 * (r.x - x0_), 2.0 * (r.y - y0_), 2.0 * (r.z - z0_)};
354,297,100 ✔
693
}
694

695
void SurfaceSphere::to_hdf5_inner(hid_t group_id) const
7,004 ✔
696
{
697
  write_string(group_id, "type", "sphere", false);
7,004 ✔
698
  array<double, 4> coeffs {{x0_, y0_, z0_, radius_}};
7,004 ✔
699
  write_dataset(group_id, "coefficients", coeffs);
7,004 ✔
700
}
7,004 ✔
701

702
BoundingBox SurfaceSphere::bounding_box(bool pos_side) const
×
703
{
704
  if (!pos_side) {
×
705
    return {{x0_ - radius_, y0_ - radius_, z0_ - radius_},
×
706
      {x0_ + radius_, y0_ + radius_, z0_ + radius_}};
×
707
  } else {
708
    return {};
×
709
  }
710
}
711

712
//==============================================================================
713
// Generic functions for x-, y-, and z-, cones
714
//==============================================================================
715

716
// The first template parameter indicates which axis the cone is aligned to.
717
// The other two parameters indicate the other two axes.  offset1, offset2,
718
// and offset3 should correspond with i1, i2, and i3, respectively.
719
template<int i1, int i2, int i3>
720
double axis_aligned_cone_evaluate(
296,230 ✔
721
  Position r, double offset1, double offset2, double offset3, double radius_sq)
722
{
723
  const double r1 = r.get<i1>() - offset1;
296,230 ✔
724
  const double r2 = r.get<i2>() - offset2;
296,230 ✔
725
  const double r3 = r.get<i3>() - offset3;
296,230 ✔
726
  return r2 * r2 + r3 * r3 - radius_sq * r1 * r1;
296,230 ✔
727
}
728

729
// The first template parameter indicates which axis the cone is aligned to.
730
// The other two parameters indicate the other two axes.  offset1, offset2,
731
// and offset3 should correspond with i1, i2, and i3, respectively.
732
template<int i1, int i2, int i3>
733
double axis_aligned_cone_distance(Position r, Direction u, bool coincident,
889,110 ✔
734
  double offset1, double offset2, double offset3, double radius_sq)
735
{
736
  const double r1 = r.get<i1>() - offset1;
889,110 ✔
737
  const double r2 = r.get<i2>() - offset2;
889,110 ✔
738
  const double r3 = r.get<i3>() - offset3;
889,110 ✔
739
  const double a = u.get<i2>() * u.get<i2>() + u.get<i3>() * u.get<i3>() -
889,110 ✔
740
                   radius_sq * u.get<i1>() * u.get<i1>();
889,110 ✔
741
  const double k =
889,110 ✔
742
    r2 * u.get<i2>() + r3 * u.get<i3>() - radius_sq * r1 * u.get<i1>();
889,110 ✔
743
  const double c = r2 * r2 + r3 * r3 - radius_sq * r1 * r1;
889,110 ✔
744
  double quad = k * k - a * c;
889,110 ✔
745

746
  double d;
747

748
  if (quad < 0.0) {
889,110 !
749
    // No intersection with cone.
750
    return INFTY;
751

752
  } else if (coincident || std::abs(c) < FP_COINCIDENT) {
889,110 !
753
    // Particle is on the cone, thus one distance is positive/negative
754
    // and the other is zero. The sign of k determines if we are facing in or
755
    // out.
756
    if (k >= 0.0) {
54,100 !
757
      d = (-k - sqrt(quad)) / a;
×
758
    } else {
759
      d = (-k + sqrt(quad)) / a;
54,100 ✔
760
    }
761

762
  } else {
763
    // Calculate both solutions to the quadratic.
764
    quad = sqrt(quad);
835,010 ✔
765
    d = (-k - quad) / a;
835,010 ✔
766
    const double b = (-k + quad) / a;
835,010 ✔
767

768
    // Determine the smallest positive solution.
769
    if (d < 0.0) {
835,010 ✔
770
      if (b > 0.0)
709,130 ✔
771
        d = b;
604,640 ✔
772
    } else {
773
      if (b > 0.0) {
125,880 !
774
        if (b < d)
125,880 !
775
          d = b;
125,880 ✔
776
      }
777
    }
778
  }
779

780
  // If the distance was negative, set boundary distance to infinity.
781
  if (d <= 0.0)
889,110 ✔
782
    return INFTY;
124,020 ✔
783
  return d;
784
}
785

786
// The first template parameter indicates which axis the cone is aligned to.
787
// The other two parameters indicate the other two axes.  offset1, offset2,
788
// and offset3 should correspond with i1, i2, and i3, respectively.
789
template<int i1, int i2, int i3>
790
Direction axis_aligned_cone_normal(
54,100 ✔
791
  Position r, double offset1, double offset2, double offset3, double radius_sq)
792
{
793
  Direction u;
794
  u.get<i1>() = -2.0 * radius_sq * (r.get<i1>() - offset1);
54,100 ✔
795
  u.get<i2>() = 2.0 * (r.get<i2>() - offset2);
54,100 ✔
796
  u.get<i3>() = 2.0 * (r.get<i3>() - offset3);
54,100 ✔
797
  return u;
798
}
799

800
//==============================================================================
801
// SurfaceXCone implementation
802
//==============================================================================
803

804
SurfaceXCone::SurfaceXCone(pugi::xml_node surf_node) : Surface(surf_node)
×
805
{
806
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &radius_sq_});
×
807
}
×
808

809
double SurfaceXCone::evaluate(Position r) const
×
810
{
811
  return axis_aligned_cone_evaluate<0, 1, 2>(r, x0_, y0_, z0_, radius_sq_);
×
812
}
813

814
double SurfaceXCone::distance(Position r, Direction u, bool coincident) const
×
815
{
816
  return axis_aligned_cone_distance<0, 1, 2>(
×
817
    r, u, coincident, x0_, y0_, z0_, radius_sq_);
×
818
}
819

820
Direction SurfaceXCone::normal(Position r) const
×
821
{
822
  return axis_aligned_cone_normal<0, 1, 2>(r, x0_, y0_, z0_, radius_sq_);
×
823
}
824

825
void SurfaceXCone::to_hdf5_inner(hid_t group_id) const
×
826
{
827
  write_string(group_id, "type", "x-cone", false);
×
828
  array<double, 4> coeffs {{x0_, y0_, z0_, radius_sq_}};
×
829
  write_dataset(group_id, "coefficients", coeffs);
×
830
}
×
831

832
//==============================================================================
833
// SurfaceYCone implementation
834
//==============================================================================
835

836
SurfaceYCone::SurfaceYCone(pugi::xml_node surf_node) : Surface(surf_node)
×
837
{
838
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &radius_sq_});
×
839
}
×
840

841
double SurfaceYCone::evaluate(Position r) const
×
842
{
843
  return axis_aligned_cone_evaluate<1, 0, 2>(r, y0_, x0_, z0_, radius_sq_);
×
844
}
845

846
double SurfaceYCone::distance(Position r, Direction u, bool coincident) const
×
847
{
848
  return axis_aligned_cone_distance<1, 0, 2>(
×
849
    r, u, coincident, y0_, x0_, z0_, radius_sq_);
×
850
}
851

852
Direction SurfaceYCone::normal(Position r) const
×
853
{
854
  return axis_aligned_cone_normal<1, 0, 2>(r, y0_, x0_, z0_, radius_sq_);
×
855
}
856

857
void SurfaceYCone::to_hdf5_inner(hid_t group_id) const
×
858
{
859
  write_string(group_id, "type", "y-cone", false);
×
860
  array<double, 4> coeffs {{x0_, y0_, z0_, radius_sq_}};
×
861
  write_dataset(group_id, "coefficients", coeffs);
×
862
}
×
863

864
//==============================================================================
865
// SurfaceZCone implementation
866
//==============================================================================
867

868
SurfaceZCone::SurfaceZCone(pugi::xml_node surf_node) : Surface(surf_node)
13 ✔
869
{
870
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &radius_sq_});
13 ✔
871
}
13 ✔
872

873
double SurfaceZCone::evaluate(Position r) const
296,230 ✔
874
{
875
  return axis_aligned_cone_evaluate<2, 0, 1>(r, z0_, x0_, y0_, radius_sq_);
296,230 ✔
876
}
877

878
double SurfaceZCone::distance(Position r, Direction u, bool coincident) const
889,110 ✔
879
{
880
  return axis_aligned_cone_distance<2, 0, 1>(
889,110 ✔
881
    r, u, coincident, z0_, x0_, y0_, radius_sq_);
889,110 ✔
882
}
883

884
Direction SurfaceZCone::normal(Position r) const
54,100 ✔
885
{
886
  return axis_aligned_cone_normal<2, 0, 1>(r, z0_, x0_, y0_, radius_sq_);
54,100 ✔
887
}
888

889
void SurfaceZCone::to_hdf5_inner(hid_t group_id) const
10 ✔
890
{
891
  write_string(group_id, "type", "z-cone", false);
10 ✔
892
  array<double, 4> coeffs {{x0_, y0_, z0_, radius_sq_}};
10 ✔
893
  write_dataset(group_id, "coefficients", coeffs);
10 ✔
894
}
10 ✔
895

896
//==============================================================================
897
// SurfaceQuadric implementation
898
//==============================================================================
899

900
SurfaceQuadric::SurfaceQuadric(pugi::xml_node surf_node) : Surface(surf_node)
23 ✔
901
{
902
  read_coeffs(
46 ✔
903
    surf_node, id_, {&A_, &B_, &C_, &D_, &E_, &F_, &G_, &H_, &J_, &K_});
23 ✔
904
}
23 ✔
905

906
double SurfaceQuadric::evaluate(Position r) const
161,086 ✔
907
{
908
  const double x = r.x;
161,086 ✔
909
  const double y = r.y;
161,086 ✔
910
  const double z = r.z;
161,086 ✔
911
  return x * (A_ * x + D_ * y + G_) + y * (B_ * y + E_ * z + H_) +
161,086 ✔
912
         z * (C_ * z + F_ * x + J_) + K_;
161,086 ✔
913
}
914

915
double SurfaceQuadric::distance(
287,900 ✔
916
  Position r, Direction ang, bool coincident) const
917
{
918
  const double& x = r.x;
287,900 ✔
919
  const double& y = r.y;
287,900 ✔
920
  const double& z = r.z;
287,900 ✔
921
  const double& u = ang.x;
287,900 ✔
922
  const double& v = ang.y;
287,900 ✔
923
  const double& w = ang.z;
287,900 ✔
924

925
  const double a =
287,900 ✔
926
    A_ * u * u + B_ * v * v + C_ * w * w + D_ * u * v + E_ * v * w + F_ * u * w;
287,900 ✔
927
  const double k = A_ * u * x + B_ * v * y + C_ * w * z +
287,900 ✔
928
                   0.5 * (D_ * (u * y + v * x) + E_ * (v * z + w * y) +
287,900 ✔
929
                           F_ * (w * x + u * z) + G_ * u + H_ * v + J_ * w);
287,900 ✔
930
  const double c = A_ * x * x + B_ * y * y + C_ * z * z + D_ * x * y +
287,900 ✔
931
                   E_ * y * z + F_ * x * z + G_ * x + H_ * y + J_ * z + K_;
287,900 ✔
932
  double quad = k * k - a * c;
287,900 ✔
933

934
  double d;
287,900 ✔
935

936
  if (quad < 0.0) {
287,900 !
937
    // No intersection with surface.
938
    return INFTY;
939

940
  } else if (coincident || std::abs(c) < FP_COINCIDENT) {
287,900 !
941
    // Particle is on the surface, thus one distance is positive/negative and
942
    // the other is zero. The sign of k determines which distance is zero and
943
    // which is not. Additionally, if a is zero, it means the particle is on
944
    // a plane-like surface.
945
    if (a == 0.0) {
32,780 !
946
      d = INFTY; // see the below explanation
947
    } else if (k >= 0.0) {
32,780 !
948
      d = (-k - sqrt(quad)) / a;
×
949
    } else {
950
      d = (-k + sqrt(quad)) / a;
32,780 ✔
951
    }
952

953
  } else if (a == 0.0) {
255,120 !
954
    // Given the orientation of the particle, the quadric looks like a plane in
955
    // this case, and thus we have only one solution despite potentially having
956
    // quad > 0.0. While the term under the square root may be real, in one
957
    // case of the +/- of the quadratic formula, 0/0 results, and in another, a
958
    // finite value over 0 results. Applying L'Hopital's to the 0/0 case gives
959
    // the below. Alternatively this can be found by simply putting a=0 in the
960
    // equation ax^2 + bx + c = 0.
961
    d = -0.5 * c / k;
×
962
  } else {
963
    // Calculate both solutions to the quadratic.
964
    quad = sqrt(quad);
255,120 ✔
965
    d = (-k - quad) / a;
255,120 ✔
966
    double b = (-k + quad) / a;
255,120 ✔
967

968
    // Determine the smallest positive solution.
969
    if (d < 0.0) {
255,120 ✔
970
      if (b > 0.0)
255,110 !
971
        d = b;
255,110 ✔
972
    } else {
973
      if (b > 0.0) {
10 !
974
        if (b < d)
10 !
975
          d = b;
×
976
      }
977
    }
978
  }
979

980
  // If the distance was negative, set boundary distance to infinity.
981
  if (d <= 0.0)
287,900 !
982
    return INFTY;
×
983
  return d;
984
}
985

986
Direction SurfaceQuadric::normal(Position r) const
32,780 ✔
987
{
988
  const double& x = r.x;
32,780 ✔
989
  const double& y = r.y;
32,780 ✔
990
  const double& z = r.z;
32,780 ✔
991
  return {2.0 * A_ * x + D_ * y + F_ * z + G_,
32,780 ✔
992
    2.0 * B_ * y + D_ * x + E_ * z + H_, 2.0 * C_ * z + E_ * y + F_ * x + J_};
32,780 ✔
993
}
994

995
void SurfaceQuadric::to_hdf5_inner(hid_t group_id) const
10 ✔
996
{
997
  write_string(group_id, "type", "quadric", false);
10 ✔
998
  array<double, 10> coeffs {{A_, B_, C_, D_, E_, F_, G_, H_, J_, K_}};
10 ✔
999
  write_dataset(group_id, "coefficients", coeffs);
10 ✔
1000
}
10 ✔
1001

1002
//==============================================================================
1003
// Torus helper functions
1004
//==============================================================================
1005

1006
double torus_distance(double x1, double x2, double x3, double u1, double u2,
24,122,330 ✔
1007
  double u3, double A, double B, double C, bool coincident)
1008
{
1009
  // Coefficients for equation: (c2 t^2 + c1 t + c0)^2 = c2' t^2 + c1' t + c0'
1010
  double D = (C * C) / (B * B);
24,122,330 ✔
1011
  double c2 = u1 * u1 + u2 * u2 + D * u3 * u3;
24,122,330 ✔
1012
  double c1 = 2 * (u1 * x1 + u2 * x2 + D * u3 * x3);
24,122,330 ✔
1013
  double c0 = x1 * x1 + x2 * x2 + D * x3 * x3 + A * A - C * C;
24,122,330 ✔
1014
  double four_A2 = 4 * A * A;
24,122,330 ✔
1015
  double c2p = four_A2 * (u1 * u1 + u2 * u2);
24,122,330 ✔
1016
  double c1p = 2 * four_A2 * (u1 * x1 + u2 * x2);
24,122,330 ✔
1017
  double c0p = four_A2 * (x1 * x1 + x2 * x2);
24,122,330 ✔
1018

1019
  // Coefficient for equation: a t^4 + b t^3 + c t^2 + d t + e = 0. If the point
1020
  // is coincident, the 'e' coefficient should be zero. Explicitly setting it to
1021
  // zero helps avoid numerical issues below with root finding.
1022
  double coeff[5];
24,122,330 ✔
1023
  coeff[0] = coincident ? 0.0 : c0 * c0 - c0p;
24,122,330 ✔
1024
  coeff[1] = 2 * c0 * c1 - c1p;
24,122,330 ✔
1025
  coeff[2] = c1 * c1 + 2 * c0 * c2 - c2p;
24,122,330 ✔
1026
  coeff[3] = 2 * c1 * c2;
24,122,330 ✔
1027
  coeff[4] = c2 * c2;
24,122,330 ✔
1028

1029
  std::complex<double> roots[4];
24,122,330 ✔
1030
  oqs::quartic_solver(coeff, roots);
24,122,330 ✔
1031

1032
  // Find smallest positive, real root. In the case where the particle is
1033
  // coincident with the surface, we are sure to have one root very close to
1034
  // zero but possibly small and positive. A tolerance is set to discard that
1035
  // zero.
1036
  double distance = INFTY;
24,122,330 ✔
1037
  double cutoff = coincident ? TORUS_TOL : 0.0;
24,122,330 ✔
1038
  for (int i = 0; i < 4; ++i) {
120,611,650 ✔
1039
    if (roots[i].imag() == 0) {
96,489,320 ✔
1040
      double root = roots[i].real();
23,585,480 ✔
1041
      if (root > cutoff && root < distance) {
23,585,480 ✔
1042
        // Avoid roots corresponding to internal surfaces
1043
        double s1 = x1 + u1 * root;
9,465,450 ✔
1044
        double s2 = x2 + u2 * root;
9,465,450 ✔
1045
        double s3 = x3 + u3 * root;
9,465,450 ✔
1046
        double check = D * s3 * s3 + s1 * s1 + s2 * s2 + A * A - C * C;
9,465,450 ✔
1047
        if (check >= 0) {
9,465,450 !
1048
          distance = root;
9,465,450 ✔
1049
        }
1050
      }
1051
    }
1052
  }
1053
  return distance;
24,122,330 ✔
1054
}
1055

1056
//==============================================================================
1057
// SurfaceXTorus implementation
1058
//==============================================================================
1059

1060
SurfaceXTorus::SurfaceXTorus(pugi::xml_node surf_node) : Surface(surf_node)
63 ✔
1061
{
1062
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &A_, &B_, &C_});
63 ✔
1063
}
63 ✔
1064

1065
void SurfaceXTorus::to_hdf5_inner(hid_t group_id) const
50 ✔
1066
{
1067
  write_string(group_id, "type", "x-torus", false);
50 ✔
1068
  std::array<double, 6> coeffs {{x0_, y0_, z0_, A_, B_, C_}};
50 ✔
1069
  write_dataset(group_id, "coefficients", coeffs);
50 ✔
1070
}
50 ✔
1071

1072
double SurfaceXTorus::evaluate(Position r) const
737,515 ✔
1073
{
1074
  double x = r.x - x0_;
737,515 ✔
1075
  double y = r.y - y0_;
737,515 ✔
1076
  double z = r.z - z0_;
737,515 ✔
1077
  return (x * x) / (B_ * B_) +
1,475,030 ✔
1078
         std::pow(std::sqrt(y * y + z * z) - A_, 2) / (C_ * C_) - 1.;
737,515 ✔
1079
}
1080

1081
BoundingBox SurfaceXTorus::bounding_box(bool pos_side) const
20 ✔
1082
{
1083
  // The torus interior is compact: it extends +/-B_ along the axis of
1084
  // revolution and +/-(A_ + C_) in the two perpendicular directions. Mirrors
1085
  // XTorus.bounding_box on the Python side.
1086
  if (pos_side)
20 ✔
1087
    return {};
10 ✔
1088
  return {{x0_ - B_, y0_ - A_ - C_, z0_ - A_ - C_},
10 ✔
1089
    {x0_ + B_, y0_ + A_ + C_, z0_ + A_ + C_}};
10 ✔
1090
}
1091

1092
double SurfaceXTorus::distance(Position r, Direction u, bool coincident) const
7,589,650 ✔
1093
{
1094
  double x = r.x - x0_;
7,589,650 ✔
1095
  double y = r.y - y0_;
7,589,650 ✔
1096
  double z = r.z - z0_;
7,589,650 ✔
1097
  return torus_distance(y, z, x, u.y, u.z, u.x, A_, B_, C_, coincident);
7,589,650 ✔
1098
}
1099

1100
Direction SurfaceXTorus::normal(Position r) const
×
1101
{
1102
  // reduce the expansion of the full form for torus
1103
  double x = r.x - x0_;
×
1104
  double y = r.y - y0_;
×
1105
  double z = r.z - z0_;
×
1106

1107
  // f(x,y,z) = x^2/B^2 + (sqrt(y^2 + z^2) - A)^2/C^2 - 1
1108
  // ∂f/∂x = 2x/B^2
1109
  // ∂f/∂y = 2y(g - A)/(g*C^2) where g = sqrt(y^2 + z^2)
1110
  // ∂f/∂z = 2z(g - A)/(g*C^2)
1111
  // Multiplying by g*C^2*B^2 / 2 gives:
1112
  double g = std::sqrt(y * y + z * z);
×
1113
  double nx = C_ * C_ * g * x;
×
1114
  double ny = y * (g - A_) * B_ * B_;
×
1115
  double nz = z * (g - A_) * B_ * B_;
×
1116
  Direction n(nx, ny, nz);
×
1117
  return n / n.norm();
×
1118
}
1119

1120
//==============================================================================
1121
// SurfaceYTorus implementation
1122
//==============================================================================
1123

1124
SurfaceYTorus::SurfaceYTorus(pugi::xml_node surf_node) : Surface(surf_node)
63 ✔
1125
{
1126
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &A_, &B_, &C_});
63 ✔
1127
}
63 ✔
1128

1129
void SurfaceYTorus::to_hdf5_inner(hid_t group_id) const
50 ✔
1130
{
1131
  write_string(group_id, "type", "y-torus", false);
50 ✔
1132
  std::array<double, 6> coeffs {{x0_, y0_, z0_, A_, B_, C_}};
50 ✔
1133
  write_dataset(group_id, "coefficients", coeffs);
50 ✔
1134
}
50 ✔
1135

1136
double SurfaceYTorus::evaluate(Position r) const
707,716 ✔
1137
{
1138
  double x = r.x - x0_;
707,716 ✔
1139
  double y = r.y - y0_;
707,716 ✔
1140
  double z = r.z - z0_;
707,716 ✔
1141
  return (y * y) / (B_ * B_) +
1,415,432 ✔
1142
         std::pow(std::sqrt(x * x + z * z) - A_, 2) / (C_ * C_) - 1.;
707,716 ✔
1143
}
1144

1145
BoundingBox SurfaceYTorus::bounding_box(bool pos_side) const
20 ✔
1146
{
1147
  if (pos_side)
20 ✔
1148
    return {};
10 ✔
1149
  return {{x0_ - A_ - C_, y0_ - B_, z0_ - A_ - C_},
10 ✔
1150
    {x0_ + A_ + C_, y0_ + B_, z0_ + A_ + C_}};
10 ✔
1151
}
1152

1153
double SurfaceYTorus::distance(Position r, Direction u, bool coincident) const
7,649,000 ✔
1154
{
1155
  double x = r.x - x0_;
7,649,000 ✔
1156
  double y = r.y - y0_;
7,649,000 ✔
1157
  double z = r.z - z0_;
7,649,000 ✔
1158
  return torus_distance(x, z, y, u.x, u.z, u.y, A_, B_, C_, coincident);
7,649,000 ✔
1159
}
1160

1161
Direction SurfaceYTorus::normal(Position r) const
×
1162
{
1163
  // reduce the expansion of the full form for torus
1164
  double x = r.x - x0_;
×
1165
  double y = r.y - y0_;
×
1166
  double z = r.z - z0_;
×
1167

1168
  // f(x,y,z) = y^2/B^2 + (sqrt(x^2 + z^2) - A)^2/C^2 - 1
1169
  // ∂f/∂x = 2x(g - A)/(g*C^2) where g = sqrt(x^2 + z^2)
1170
  // ∂f/∂y = 2y/B^2
1171
  // ∂f/∂z = 2z(g - A)/(g*C^2)
1172
  // Multiplying by g*C^2*B^2 / 2 gives:
1173
  double g = std::sqrt(x * x + z * z);
×
1174
  double nx = x * (g - A_) * B_ * B_;
×
1175
  double ny = C_ * C_ * g * y;
×
1176
  double nz = z * (g - A_) * B_ * B_;
×
1177
  Direction n(nx, ny, nz);
×
1178
  return n / n.norm();
×
1179
}
1180

1181
//==============================================================================
1182
// SurfaceZTorus implementation
1183
//==============================================================================
1184

1185
SurfaceZTorus::SurfaceZTorus(pugi::xml_node surf_node) : Surface(surf_node)
102 ✔
1186
{
1187
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &A_, &B_, &C_});
102 ✔
1188
}
102 ✔
1189

1190
void SurfaceZTorus::to_hdf5_inner(hid_t group_id) const
80 ✔
1191
{
1192
  write_string(group_id, "type", "z-torus", false);
80 ✔
1193
  std::array<double, 6> coeffs {{x0_, y0_, z0_, A_, B_, C_}};
80 ✔
1194
  write_dataset(group_id, "coefficients", coeffs);
80 ✔
1195
}
80 ✔
1196

1197
double SurfaceZTorus::evaluate(Position r) const
1,189,498 ✔
1198
{
1199
  double x = r.x - x0_;
1,189,498 ✔
1200
  double y = r.y - y0_;
1,189,498 ✔
1201
  double z = r.z - z0_;
1,189,498 ✔
1202
  return (z * z) / (B_ * B_) +
2,378,996 ✔
1203
         std::pow(std::sqrt(x * x + y * y) - A_, 2) / (C_ * C_) - 1.;
1,189,498 ✔
1204
}
1205

1206
BoundingBox SurfaceZTorus::bounding_box(bool pos_side) const
20 ✔
1207
{
1208
  if (pos_side)
20 ✔
1209
    return {};
10 ✔
1210
  return {{x0_ - A_ - C_, y0_ - A_ - C_, z0_ - B_},
10 ✔
1211
    {x0_ + A_ + C_, y0_ + A_ + C_, z0_ + B_}};
10 ✔
1212
}
1213

1214
double SurfaceZTorus::distance(Position r, Direction u, bool coincident) const
8,883,680 ✔
1215
{
1216
  double x = r.x - x0_;
8,883,680 ✔
1217
  double y = r.y - y0_;
8,883,680 ✔
1218
  double z = r.z - z0_;
8,883,680 ✔
1219
  return torus_distance(x, y, z, u.x, u.y, u.z, A_, B_, C_, coincident);
8,883,680 ✔
1220
}
1221

1222
Direction SurfaceZTorus::normal(Position r) const
×
1223
{
1224
  // reduce the expansion of the full form for torus
1225
  double x = r.x - x0_;
×
1226
  double y = r.y - y0_;
×
1227
  double z = r.z - z0_;
×
1228

1229
  // f(x,y,z) = z^2/B^2 + (sqrt(x^2 + y^2) - A)^2/C^2 - 1
1230
  // ∂f/∂x = 2x(g - A)/(g*C^2) where g = sqrt(x^2 + y^2)
1231
  // ∂f/∂y = 2y(g - A)/(g*C^2)
1232
  // ∂f/∂z = 2z/B^2
1233
  // Multiplying by g*C^2*B^2 / 2 gives:
1234
  double g = std::sqrt(x * x + y * y);
×
1235
  double nx = x * (g - A_) * B_ * B_;
×
1236
  double ny = y * (g - A_) * B_ * B_;
×
1237
  double nz = C_ * C_ * g * z;
×
1238
  Position n(nx, ny, nz);
×
1239
  return n / n.norm();
×
1240
}
1241

1242
//==============================================================================
1243

1244
void read_surfaces(pugi::xml_node node,
8,536 ✔
1245
  std::set<std::pair<int, int>>& periodic_pairs,
1246
  std::unordered_map<int, double>& albedo_map,
1247
  std::unordered_map<int, int>& periodic_sense_map)
1248
{
1249
  // Count the number of surfaces
1250
  int n_surfaces = 0;
8,536 ✔
1251
  for (pugi::xml_node surf_node : node.children("surface")) {
52,819 ✔
1252
    n_surfaces++;
44,283 ✔
1253
  }
1254

1255
  // Loop over XML surface elements and populate the array.  Keep track of
1256
  // periodic surfaces and their albedos.
1257
  model::surfaces.reserve(n_surfaces);
8,536 ✔
1258
  {
8,536 ✔
1259
    pugi::xml_node surf_node;
8,536 ✔
1260
    int i_surf;
8,536 ✔
1261
    for (surf_node = node.child("surface"), i_surf = 0; surf_node;
52,819 ✔
1262
         surf_node = surf_node.next_sibling("surface"), i_surf++) {
44,283 ✔
1263
      std::string surf_type = get_node_value(surf_node, "type", true, true);
44,283 ✔
1264

1265
      // Allocate and initialize the new surface
1266

1267
      if (surf_type == "x-plane") {
44,283 ✔
1268
        model::surfaces.push_back(make_unique<SurfaceXPlane>(surf_node));
11,155 ✔
1269

1270
      } else if (surf_type == "y-plane") {
33,128 ✔
1271
        model::surfaces.push_back(make_unique<SurfaceYPlane>(surf_node));
10,107 ✔
1272

1273
      } else if (surf_type == "z-plane") {
23,021 ✔
1274
        model::surfaces.push_back(make_unique<SurfaceZPlane>(surf_node));
7,462 ✔
1275

1276
      } else if (surf_type == "plane") {
15,559 ✔
1277
        model::surfaces.push_back(make_unique<SurfacePlane>(surf_node));
1,832 ✔
1278

1279
      } else if (surf_type == "x-cylinder") {
13,727 ✔
1280
        model::surfaces.push_back(make_unique<SurfaceXCylinder>(surf_node));
39 ✔
1281

1282
      } else if (surf_type == "y-cylinder") {
13,688 ✔
1283
        model::surfaces.push_back(make_unique<SurfaceYCylinder>(surf_node));
23 ✔
1284

1285
      } else if (surf_type == "z-cylinder") {
13,665 ✔
1286
        model::surfaces.push_back(make_unique<SurfaceZCylinder>(surf_node));
5,130 ✔
1287

1288
      } else if (surf_type == "sphere") {
8,535 ✔
1289
        model::surfaces.push_back(make_unique<SurfaceSphere>(surf_node));
8,311 ✔
1290

1291
      } else if (surf_type == "x-cone") {
224 !
1292
        model::surfaces.push_back(make_unique<SurfaceXCone>(surf_node));
×
1293

1294
      } else if (surf_type == "y-cone") {
224 !
1295
        model::surfaces.push_back(make_unique<SurfaceYCone>(surf_node));
×
1296

1297
      } else if (surf_type == "z-cone") {
224 ✔
1298
        model::surfaces.push_back(make_unique<SurfaceZCone>(surf_node));
13 ✔
1299

1300
      } else if (surf_type == "quadric") {
211 ✔
1301
        model::surfaces.push_back(make_unique<SurfaceQuadric>(surf_node));
13 ✔
1302

1303
      } else if (surf_type == "x-torus") {
198 ✔
1304
        model::surfaces.push_back(std::make_unique<SurfaceXTorus>(surf_node));
53 ✔
1305

1306
      } else if (surf_type == "y-torus") {
145 ✔
1307
        model::surfaces.push_back(std::make_unique<SurfaceYTorus>(surf_node));
53 ✔
1308

1309
      } else if (surf_type == "z-torus") {
92 !
1310
        model::surfaces.push_back(std::make_unique<SurfaceZTorus>(surf_node));
92 ✔
1311

1312
      } else {
1313
        fatal_error(fmt::format("Invalid surface type, \"{}\"", surf_type));
×
1314
      }
1315

1316
      // Check for a periodic surface
1317
      if (check_for_node(surf_node, "boundary")) {
44,283 ✔
1318
        std::string surf_bc = get_node_value(surf_node, "boundary", true, true);
26,760 ✔
1319
        if (surf_bc == "periodic") {
26,760 ✔
1320
          periodic_sense_map[model::surfaces.back()->id_] = 0;
482 ✔
1321
          // Check for surface albedo. Skip sanity check as it is already done
1322
          // in the Surface class's constructor.
1323
          if (check_for_node(surf_node, "albedo")) {
482 !
1324
            albedo_map[model::surfaces.back()->id_] =
×
1325
              std::stod(get_node_value(surf_node, "albedo"));
×
1326
          }
1327
          if (check_for_node(surf_node, "periodic_surface_id")) {
482 ✔
1328
            int i_periodic =
316 ✔
1329
              std::stoi(get_node_value(surf_node, "periodic_surface_id"));
632 ✔
1330
            int lo_id = std::min(model::surfaces.back()->id_, i_periodic);
316 ✔
1331
            int hi_id = std::max(model::surfaces.back()->id_, i_periodic);
316 ✔
1332
            periodic_pairs.insert({lo_id, hi_id});
316 ✔
1333
          } else {
1334
            periodic_pairs.insert({model::surfaces.back()->id_, -1});
166 ✔
1335
          }
1336
        }
1337
      }
26,760 ✔
1338
    }
44,283 ✔
1339
  }
1340

1341
  // Fill the surface map
1342
  for (int i_surf = 0; i_surf < model::surfaces.size(); i_surf++) {
52,819 ✔
1343
    int id = model::surfaces[i_surf]->id_;
44,283 !
1344
    auto in_map = model::surface_map.find(id);
44,283 !
1345
    if (in_map == model::surface_map.end()) {
44,283 !
1346
      model::surface_map[id] = i_surf;
44,283 ✔
1347
    } else {
1348
      fatal_error(
×
1349
        fmt::format("Two or more surfaces use the same unique ID: {}", id));
×
1350
    }
1351
  }
1352
}
8,536 ✔
1353

1354
void prepare_boundary_conditions(std::set<std::pair<int, int>>& periodic_pairs,
8,536 ✔
1355
  std::unordered_map<int, double>& albedo_map,
1356
  std::unordered_map<int, int>& periodic_sense_map)
1357
{
1358
  // Fill the senses map for periodic surfaces
1359
  auto n_periodic = periodic_sense_map.size();
8,536 ✔
1360
  for (const auto& cell : model::cells) {
8,764 ✔
1361
    if (n_periodic == 0)
8,647 ✔
1362
      break; // Early exit once all periodic surfaces found
1363

1364
    for (auto s : cell->surfaces()) {
1,181 ✔
1365
      auto surf_idx = std::abs(s) - 1;
953 ✔
1366
      auto id = model::surfaces[surf_idx]->id_;
953 ✔
1367

1368
      if (periodic_sense_map.count(id)) {
953 ✔
1369
        periodic_sense_map[id] = std::copysign(1, s);
482 ✔
1370
        --n_periodic;
482 ✔
1371
      }
1372
    }
228 ✔
1373
  }
1374

1375
  // Resolve unpaired periodic surfaces.  A lambda function is used with
1376
  // std::find_if to identify the unpaired surfaces.
1377
  auto is_unresolved_pair = [](const std::pair<int, int> p) {
8,860 ✔
1378
    return p.second == -1;
324 ✔
1379
  };
1380
  auto first_unresolved = std::find_if(
8,536 ✔
1381
    periodic_pairs.begin(), periodic_pairs.end(), is_unresolved_pair);
1382
  if (first_unresolved != periodic_pairs.end()) {
8,536 ✔
1383
    // Found one unpaired surface; search for a second one
1384
    auto next_elem = first_unresolved;
83 ✔
1385
    next_elem++;
83 !
1386
    auto second_unresolved =
83 !
1387
      std::find_if(next_elem, periodic_pairs.end(), is_unresolved_pair);
83 !
1388
    if (second_unresolved == periodic_pairs.end()) {
83 !
1389
      fatal_error("Found only one periodic surface without a specified partner."
×
1390
                  " Please specify the partner for each periodic surface.");
1391
    }
1392

1393
    // Make sure there isn't a third unpaired surface
1394
    next_elem = second_unresolved;
83 ✔
1395
    next_elem++;
83 !
1396
    auto third_unresolved =
83 !
1397
      std::find_if(next_elem, periodic_pairs.end(), is_unresolved_pair);
83 !
1398
    if (third_unresolved != periodic_pairs.end()) {
83 !
1399
      fatal_error(
×
1400
        "Found at least three periodic surfaces without a specified "
1401
        "partner. Please specify the partner for each periodic surface.");
1402
    }
1403

1404
    // Add the completed pair and remove the old, unpaired entries
1405
    int lo_id = std::min(first_unresolved->first, second_unresolved->first);
83 !
1406
    int hi_id = std::max(first_unresolved->first, second_unresolved->first);
83 !
1407
    periodic_pairs.insert({lo_id, hi_id});
83 ✔
1408
    periodic_pairs.erase(first_unresolved);
83 ✔
1409
    periodic_pairs.erase(second_unresolved);
83 ✔
1410
  }
1411

1412
  // Assign the periodic boundary conditions with albedos
1413
  for (auto periodic_pair : periodic_pairs) {
8,777 ✔
1414
    int i_surf = model::surface_map[periodic_pair.first];
241 ✔
1415
    int j_surf = model::surface_map[periodic_pair.second];
241 ✔
1416
    Surface& surf1 {*model::surfaces[i_surf]};
241 ✔
1417
    Surface& surf2 {*model::surfaces[j_surf]};
241 ✔
1418

1419
    // Compute the dot product of the surface normals
1420
    Direction norm1 = surf1.normal({0, 0, 0});
241 ✔
1421
    Direction norm2 = surf2.normal({0, 0, 0});
241 ✔
1422
    norm1 /= norm1.norm();
241 ✔
1423
    norm2 /= norm2.norm();
241 ✔
1424
    double dot_prod = norm1.dot(norm2);
241 ✔
1425

1426
    // If the dot product is 1 (to within floating point precision) then the
1427
    // planes are parallel which indicates a translational periodic boundary
1428
    // condition.  Otherwise, it is a rotational periodic BC.
1429
    if (std::abs(1.0 - dot_prod) < FP_PRECISION) {
241 ✔
1430
      surf1.bc_ = make_unique<TranslationalPeriodicBC>(i_surf, j_surf);
100 ✔
1431
      surf2.bc_ = make_unique<TranslationalPeriodicBC>(j_surf, i_surf);
100 ✔
1432
    } else {
1433
      // check that both normals have at least one 0 component
1434
      if (std::abs(norm1.x) > FP_PRECISION &&
141 ✔
1435
          std::abs(norm1.y) > FP_PRECISION &&
141 !
1436
          std::abs(norm1.z) > FP_PRECISION) {
102 !
1437
        fatal_error(fmt::format(
×
1438
          "The normal ({}) of the periodic surface ({}) does not contain any "
1439
          "component with a zero value. A RotationalPeriodicBC requires one "
1440
          "component which is zero for both plane normals.",
1441
          norm1, i_surf));
1442
      }
1443
      if (std::abs(norm2.x) > FP_PRECISION &&
141 ✔
1444
          std::abs(norm2.y) > FP_PRECISION &&
141 !
1445
          std::abs(norm2.z) > FP_PRECISION) {
102 !
1446
        fatal_error(fmt::format(
×
1447
          "The normal ({}) of the periodic surface ({}) does not contain any "
1448
          "component with a zero value. A RotationalPeriodicBC requires one "
1449
          "component which is zero for both plane normals.",
1450
          norm2, j_surf));
1451
      }
1452
      // find common zero component, which indicates the periodic axis
1453
      RotationalPeriodicBC::PeriodicAxis axis;
141 ✔
1454
      if (std::abs(norm1.x) <= FP_PRECISION &&
141 ✔
1455
          std::abs(norm2.x) <= FP_PRECISION) {
13 !
1456
        axis = RotationalPeriodicBC::PeriodicAxis::x;
13 ✔
1457
      } else if (std::abs(norm1.y) <= FP_PRECISION &&
128 ✔
1458
                 std::abs(norm2.y) <= FP_PRECISION) {
26 ✔
1459
        axis = RotationalPeriodicBC::PeriodicAxis::y;
13 ✔
1460
      } else if (std::abs(norm1.z) <= FP_PRECISION &&
115 !
1461
                 std::abs(norm2.z) <= FP_PRECISION) {
115 !
1462
        axis = RotationalPeriodicBC::PeriodicAxis::z;
115 ✔
1463
      } else {
1464
        fatal_error(fmt::format(
×
1465
          "There is no component which is 0.0 in both normal vectors. This "
1466
          "indicates that the two planes are not periodic about the X, Y, or Z "
1467
          "axis, which is not supported."));
1468
      }
1469
      auto i_sign = periodic_sense_map[periodic_pair.first];
141 ✔
1470
      auto j_sign = periodic_sense_map[periodic_pair.second];
141 ✔
1471
      surf1.bc_ = make_unique<RotationalPeriodicBC>(
141 ✔
1472
        i_sign * (i_surf + 1), j_sign * (j_surf + 1), axis);
141 ✔
1473
      surf2.bc_ = make_unique<RotationalPeriodicBC>(
141 ✔
1474
        j_sign * (j_surf + 1), i_sign * (i_surf + 1), axis);
282 ✔
1475
    }
1476

1477
    // If albedo data is present in albedo map, set the boundary albedo.
1478
    if (albedo_map.count(surf1.id_)) {
241 !
1479
      surf1.bc_->set_albedo(albedo_map[surf1.id_]);
×
1480
    }
1481
    if (albedo_map.count(surf2.id_)) {
241 !
1482
      surf2.bc_->set_albedo(albedo_map[surf2.id_]);
241 ✔
1483
    }
1484
  }
1485
}
8,536 ✔
1486

1487
void free_memory_surfaces()
8,623 ✔
1488
{
1489
  model::surfaces.clear();
8,623 ✔
1490
  model::surface_map.clear();
8,623 ✔
1491
}
8,623 ✔
1492

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