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

openmc-dev / openmc / 34416149688

09 Sep 2026 11:15PM UTC coverage: 81.48% (+0.1%) from 81.347%
34416149688

Pull #4087

github

web-flow
Merge 93138e589 into 5260b9a0f
Pull Request #4087: Compute bounding boxes for general planes and tori in C++

18756 of 27197 branches covered (68.96%)

Branch coverage included in aggregate %.

43 of 46 new or added lines in 2 files covered. (93.48%)

959 existing lines in 29 files now uncovered.

60863 of 70519 relevant lines covered (86.31%)

49704801.91 hits per line

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

82.13
/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(
49,530 ✔
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");
49,530 ✔
43
  if (coeffs_file.size() != coeffs.size()) {
49,530 !
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;
49,530 ✔
51
  for (auto c : coeffs) {
146,653 ✔
52
    *c = coeffs_file[i++];
97,123 ✔
53
  }
54
}
49,530 ✔
55

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

60
Surface::Surface() {} // empty constructor
851 ✔
61

62
Surface::Surface(pugi::xml_node surf_node)
49,530 ✔
63
{
64
  if (check_for_node(surf_node, "id")) {
49,530 !
65
    id_ = std::stoi(get_node_value(surf_node, "id"));
99,060 ✔
66
    if (contains(settings::source_write_surf_id, id_) ||
99,060 ✔
67
        settings::source_write_surf_id.empty()) {
48,742 ✔
68
      surf_source_ = true;
48,488 ✔
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")) {
49,530 ✔
75
    name_ = get_node_value(surf_node, "name", false);
12,028 ✔
76
  }
77

78
  if (check_for_node(surf_node, "boundary")) {
49,530 ✔
79
    std::string surf_bc = get_node_value(surf_node, "boundary", true, true);
28,569 ✔
80

81
    if (surf_bc == "transmission" || surf_bc == "transmit" || surf_bc.empty()) {
57,138 !
82
      // Leave the bc_ a nullptr
83
    } else if (surf_bc == "vacuum") {
28,569 ✔
84
      bc_ = make_unique<VacuumBC>();
14,119 ✔
85
    } else if (surf_bc == "reflective" || surf_bc == "reflect" ||
15,086 !
86
               surf_bc == "reflecting") {
636 !
87
      bc_ = make_unique<ReflectiveBC>();
13,814 ✔
88
    } else if (surf_bc == "white") {
636 ✔
89
      bc_ = make_unique<WhiteBC>();
90 ✔
90
    } else if (surf_bc == "periodic") {
546 !
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_) {
28,569 !
99
      double surf_alb = std::stod(get_node_value(surf_node, "albedo"));
180 ✔
100

101
      if (surf_alb < 0.0)
90 !
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)
90 !
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);
90 ✔
113
    }
114
  }
28,569 ✔
115
}
49,530 ✔
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,995,212 ✔
129
  }
130
  return f > 0.0;
2,147,483,647 ✔
131
}
132

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

139
  // Reflect direction according to normal.
140
  return u.reflect(n);
975,590,078 ✔
141
}
142

143
Direction Surface::diffuse_reflect(
1,007,149 ✔
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);
1,007,149 ✔
150
  n /= n.norm();
1,007,149 ✔
151
  const double projection = n.dot(u);
1,007,149 ✔
152

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

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

160
  // normalize the direction
161
  return u / u.norm();
1,007,149 ✔
162
}
163

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

168
  if (geom_type() == GeometryType::DAG) {
40,691 ✔
169
    write_string(surf_group, "geom_type", "dagmc", false);
1,268 !
170
  } else if (geom_type() == GeometryType::CSG) {
40,057 !
171
    write_string(surf_group, "geom_type", "csg", false);
40,057 ✔
172

173
    if (bc_) {
40,057 ✔
174
      write_string(surf_group, "boundary_type", bc_->type(), false);
23,866 ✔
175
      bc_->to_hdf5(surf_group);
23,866 ✔
176

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

183
        if (id_ == surf1.id_) {
458 !
184
          write_dataset(surf_group, "periodic_surface_id", surf2.id_);
458 ✔
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);
32,382 ✔
191
    }
192
  }
193

194
  if (!name_.empty()) {
40,691 ✔
195
    write_string(surf_group, "name", name_, false);
9,806 ✔
196
  }
197

198
  to_hdf5_inner(surf_group);
40,691 ✔
199

200
  close_group(surf_group);
40,691 ✔
201
}
40,691 ✔
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)
13,023 ✔
226
{
227
  read_coeffs(surf_node, id_, {&x0_});
13,023 ✔
228
}
13,023 ✔
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
371,985,414 ✔
241
{
242
  return {1., 0., 0.};
371,985,414 ✔
243
}
244

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

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

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

265
SurfaceYPlane::SurfaceYPlane(pugi::xml_node surf_node) : Surface(surf_node)
10,918 ✔
266
{
267
  read_coeffs(surf_node, id_, {&y0_});
10,918 ✔
268
}
10,918 ✔
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
522,032,237 ✔
281
{
282
  return {0., 1., 0.};
522,032,237 ✔
283
}
284

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

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

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

305
SurfaceZPlane::SurfaceZPlane(pugi::xml_node surf_node) : Surface(surf_node)
8,026 ✔
306
{
307
  read_coeffs(surf_node, id_, {&z0_});
8,026 ✔
308
}
8,026 ✔
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
260,425,701 ✔
321
{
322
  return {0., 0., 1.};
260,425,701 ✔
323
}
324

325
void SurfaceZPlane::to_hdf5_inner(hid_t group_id) const
6,980 ✔
326
{
327
  write_string(group_id, "type", "z-plane", false);
6,980 ✔
328
  array<double, 1> coeffs {{z0_}};
6,980 ✔
329
  write_dataset(group_id, "coefficients", coeffs);
6,980 ✔
330
}
6,980 ✔
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)
2,118 ✔
346
{
347
  read_coeffs(surf_node, id_, {&A_, &B_, &C_, &D_});
2,118 ✔
348
}
2,118 ✔
349

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

355
BoundingBox SurfacePlane::bounding_box(bool pos_side) const
209 ✔
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 array<double, 3> coeffs {A_, B_, C_};
209 ✔
363
  const double norm = std::sqrt(A_ * A_ + B_ * B_ + C_ * C_);
209 ✔
364
  if (norm == 0.0)
209 ✔
365
    return {};
11 ✔
366

367
  int axis = -1;
539 ✔
368
  for (int i = 0; i < 3; ++i) {
539 ✔
369
    if (std::abs(std::abs(coeffs[i] / norm) - 1.0) <= PLANE_ALIGNMENT_TOL) {
462 ✔
370
      axis = i;
371
      break;
372
    }
373
  }
374
  if (axis == -1)
198 ✔
375
    return {};
77 ✔
376

377
  // The half-space is bounded below when the outward normal points along the
378
  // positive axis direction and we are on the positive side, or vice versa.
379
  BoundingBox bbox;
121 ✔
380
  const double intercept = D_ / coeffs[axis];
121 ✔
381
  if (pos_side == (coeffs[axis] > 0.0)) {
121 ✔
382
    bbox.min[axis] = intercept;
77 ✔
383
  } else {
384
    bbox.max[axis] = intercept;
44 ✔
385
  }
386
  return bbox;
121 ✔
387
}
388

389
double SurfacePlane::distance(Position r, Direction u, bool coincident) const
804,031,734 ✔
390
{
391
  const double f = A_ * r.x + B_ * r.y + C_ * r.z - D_;
804,031,734 ✔
392
  const double projection = A_ * u.x + B_ * u.y + C_ * u.z;
804,031,734 ✔
393
  if (coincident || std::abs(f) < FP_COINCIDENT || projection == 0.0) {
804,031,734 !
394
    return INFTY;
395
  } else {
396
    const double d = -f / projection;
738,337,652 ✔
397
    if (d < 0.0)
738,337,652 ✔
398
      return INFTY;
399
    return d;
401,792,253 ✔
400
  }
401
}
402

403
Direction SurfacePlane::normal(Position r) const
5,745,398 ✔
404
{
405
  return {A_, B_, C_};
5,745,398 ✔
406
}
407

408
void SurfacePlane::to_hdf5_inner(hid_t group_id) const
1,480 ✔
409
{
410
  write_string(group_id, "type", "plane", false);
1,480 ✔
411
  array<double, 4> coeffs {{A_, B_, C_, D_}};
1,480 ✔
412
  write_dataset(group_id, "coefficients", coeffs);
1,480 ✔
413
}
1,480 ✔
414

415
//==============================================================================
416
// Generic functions for x-, y-, and z-, cylinders
417
//==============================================================================
418

419
// The template parameters indicate the axes perpendicular to the axis of the
420
// cylinder.  offset1 and offset2 should correspond with i1 and i2,
421
// respectively.
422
template<int i1, int i2>
423
double axis_aligned_cylinder_evaluate(
2,147,483,647 ✔
424
  Position r, double offset1, double offset2, double radius)
425
{
426
  const double r1 = r.get<i1>() - offset1;
2,147,483,647 ✔
427
  const double r2 = r.get<i2>() - offset2;
2,147,483,647 ✔
428
  return r1 * r1 + r2 * r2 - radius * radius;
2,147,483,647 ✔
429
}
430

431
// The first template parameter indicates which axis the cylinder is aligned to.
432
// The other two parameters indicate the other two axes.  offset1 and offset2
433
// should correspond with i2 and i3, respectively.
434
template<int i1, int i2, int i3>
435
double axis_aligned_cylinder_distance(Position r, Direction u, bool coincident,
2,147,483,647 ✔
436
  double offset1, double offset2, double radius)
437
{
438
  const double a = 1.0 - u.get<i1>() * u.get<i1>(); // u^2 + v^2
2,147,483,647 ✔
439
  if (a == 0.0)
2,147,483,647 ✔
440
    return INFTY;
441

442
  const double r2 = r.get<i2>() - offset1;
2,147,483,647 ✔
443
  const double r3 = r.get<i3>() - offset2;
2,147,483,647 ✔
444
  const double k = r2 * u.get<i2>() + r3 * u.get<i3>();
2,147,483,647 ✔
445
  const double c = r2 * r2 + r3 * r3 - radius * radius;
2,147,483,647 ✔
446
  const double quad = k * k - a * c;
2,147,483,647 ✔
447

448
  if (quad < 0.0) {
2,147,483,647 ✔
449
    // No intersection with cylinder.
450
    return INFTY;
451

452
  } else if (coincident || std::abs(c) < FP_COINCIDENT) {
2,147,483,647 ✔
453
    // Particle is on the cylinder, thus one distance is positive/negative
454
    // and the other is zero. The sign of k determines if we are facing in or
455
    // out.
456
    if (k >= 0.0) {
1,895,810,439 ✔
457
      return INFTY;
458
    } else {
459
      return (-k + sqrt(quad)) / a;
1,084,633,771 ✔
460
    }
461

462
  } else if (c < 0.0) {
2,147,483,647 ✔
463
    // Particle is inside the cylinder, thus one distance must be negative
464
    // and one must be positive. The positive distance will be the one with
465
    // negative sign on sqrt(quad).
466
    return (-k + sqrt(quad)) / a;
1,068,133,754 ✔
467

468
  } else {
469
    // Particle is outside the cylinder, thus both distances are either
470
    // positive or negative. If positive, the smaller distance is the one
471
    // with positive sign on sqrt(quad).
472
    const double d = (-k - sqrt(quad)) / a;
1,458,116,762 ✔
473
    if (d < 0.0)
1,458,116,762 ✔
474
      return INFTY;
475
    return d;
1,135,192,362 ✔
476
  }
477
}
478

479
// The first template parameter indicates which axis the cylinder is aligned to.
480
// The other two parameters indicate the other two axes.  offset1 and offset2
481
// should correspond with i2 and i3, respectively.
482
template<int i1, int i2, int i3>
483
Direction axis_aligned_cylinder_normal(
281,972,311 ✔
484
  Position r, double offset1, double offset2)
485
{
486
  Direction u;
281,972,311 ✔
487
  u.get<i2>() = 2.0 * (r.get<i2>() - offset1);
281,972,311 ✔
488
  u.get<i3>() = 2.0 * (r.get<i3>() - offset2);
281,972,311 ✔
489
  u.get<i1>() = 0.0;
490
  return u;
491
}
492

493
//==============================================================================
494
// SurfaceXCylinder implementation
495
//==============================================================================
496

497
SurfaceXCylinder::SurfaceXCylinder(pugi::xml_node surf_node)
45 ✔
498
  : Surface(surf_node)
45 ✔
499
{
500
  read_coeffs(surf_node, id_, {&y0_, &z0_, &radius_});
45 ✔
501
}
45 ✔
502

503
double SurfaceXCylinder::evaluate(Position r) const
1,277,481 ✔
504
{
505
  return axis_aligned_cylinder_evaluate<1, 2>(r, y0_, z0_, radius_);
1,277,481 ✔
506
}
507

508
double SurfaceXCylinder::distance(
1,622,038 ✔
509
  Position r, Direction u, bool coincident) const
510
{
511
  return axis_aligned_cylinder_distance<0, 1, 2>(
1,622,038 ✔
512
    r, u, coincident, y0_, z0_, radius_);
1,622,038 ✔
513
}
514

515
Direction SurfaceXCylinder::normal(Position r) const
397,936 ✔
516
{
517
  return axis_aligned_cylinder_normal<0, 1, 2>(r, y0_, z0_);
397,936 ✔
518
}
519

520
void SurfaceXCylinder::to_hdf5_inner(hid_t group_id) const
33 ✔
521
{
522
  write_string(group_id, "type", "x-cylinder", false);
33 ✔
523
  array<double, 3> coeffs {{y0_, z0_, radius_}};
33 ✔
524
  write_dataset(group_id, "coefficients", coeffs);
33 ✔
525
}
33 ✔
526

527
BoundingBox SurfaceXCylinder::bounding_box(bool pos_side) const
×
528
{
529
  if (!pos_side) {
×
530
    return {{-INFTY, y0_ - radius_, z0_ - radius_},
×
531
      {INFTY, y0_ + radius_, z0_ + radius_}};
×
532
  } else {
533
    return {};
×
534
  }
535
}
536
//==============================================================================
537
// SurfaceYCylinder implementation
538
//==============================================================================
539

540
SurfaceYCylinder::SurfaceYCylinder(pugi::xml_node surf_node)
26 ✔
541
  : Surface(surf_node)
26 ✔
542
{
543
  read_coeffs(surf_node, id_, {&x0_, &z0_, &radius_});
26 ✔
544
}
26 ✔
545

546
double SurfaceYCylinder::evaluate(Position r) const
170,984 ✔
547
{
548
  return axis_aligned_cylinder_evaluate<0, 2>(r, x0_, z0_, radius_);
170,984 ✔
549
}
550

551
double SurfaceYCylinder::distance(
659,769 ✔
552
  Position r, Direction u, bool coincident) const
553
{
554
  return axis_aligned_cylinder_distance<1, 0, 2>(
659,769 ✔
555
    r, u, coincident, x0_, z0_, radius_);
659,769 ✔
556
}
557

558
Direction SurfaceYCylinder::normal(Position r) const
×
559
{
560
  return axis_aligned_cylinder_normal<1, 0, 2>(r, x0_, z0_);
×
561
}
562

563
void SurfaceYCylinder::to_hdf5_inner(hid_t group_id) const
11 ✔
564
{
565
  write_string(group_id, "type", "y-cylinder", false);
11 ✔
566
  array<double, 3> coeffs {{x0_, z0_, radius_}};
11 ✔
567
  write_dataset(group_id, "coefficients", coeffs);
11 ✔
568
}
11 ✔
569

570
BoundingBox SurfaceYCylinder::bounding_box(bool pos_side) const
11 ✔
571
{
572
  if (!pos_side) {
11 !
573
    return {{x0_ - radius_, -INFTY, z0_ - radius_},
11 ✔
574
      {x0_ + radius_, INFTY, z0_ + radius_}};
11 ✔
575
  } else {
576
    return {};
×
577
  }
578
}
579

580
//==============================================================================
581
// SurfaceZCylinder implementation
582
//==============================================================================
583

584
SurfaceZCylinder::SurfaceZCylinder(pugi::xml_node surf_node)
5,691 ✔
585
  : Surface(surf_node)
5,691 ✔
586
{
587
  read_coeffs(surf_node, id_, {&x0_, &y0_, &radius_});
5,691 ✔
588
}
5,691 ✔
589

590
double SurfaceZCylinder::evaluate(Position r) const
2,147,483,647 ✔
591
{
592
  return axis_aligned_cylinder_evaluate<0, 1>(r, x0_, y0_, radius_);
2,147,483,647 ✔
593
}
594

595
double SurfaceZCylinder::distance(
2,147,483,647 ✔
596
  Position r, Direction u, bool coincident) const
597
{
598
  return axis_aligned_cylinder_distance<2, 0, 1>(
2,147,483,647 ✔
599
    r, u, coincident, x0_, y0_, radius_);
2,147,483,647 ✔
600
}
601

602
Direction SurfaceZCylinder::normal(Position r) const
281,574,375 ✔
603
{
604
  return axis_aligned_cylinder_normal<2, 0, 1>(r, x0_, y0_);
281,574,375 ✔
605
}
606

607
void SurfaceZCylinder::to_hdf5_inner(hid_t group_id) const
4,497 ✔
608
{
609
  write_string(group_id, "type", "z-cylinder", false);
4,497 ✔
610
  array<double, 3> coeffs {{x0_, y0_, radius_}};
4,497 ✔
611
  write_dataset(group_id, "coefficients", coeffs);
4,497 ✔
612
}
4,497 ✔
613

614
BoundingBox SurfaceZCylinder::bounding_box(bool pos_side) const
44 ✔
615
{
616
  if (!pos_side) {
44 ✔
617
    return {{x0_ - radius_, y0_ - radius_, -INFTY},
22 ✔
618
      {x0_ + radius_, y0_ + radius_, INFTY}};
22 ✔
619
  } else {
620
    return {};
22 ✔
621
  }
622
}
623

624
//==============================================================================
625
// SurfaceSphere implementation
626
//==============================================================================
627

628
SurfaceSphere::SurfaceSphere(pugi::xml_node surf_node) : Surface(surf_node)
9,387 ✔
629
{
630
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &radius_});
9,387 ✔
631
}
9,387 ✔
632

633
double SurfaceSphere::evaluate(Position r) const
1,253,264,034 ✔
634
{
635
  const double x = r.x - x0_;
1,253,264,034 ✔
636
  const double y = r.y - y0_;
1,253,264,034 ✔
637
  const double z = r.z - z0_;
1,253,264,034 ✔
638
  return x * x + y * y + z * z - radius_ * radius_;
1,253,264,034 ✔
639
}
640

641
double SurfaceSphere::distance(Position r, Direction u, bool coincident) const
2,133,775,862 ✔
642
{
643
  const double x = r.x - x0_;
2,133,775,862 ✔
644
  const double y = r.y - y0_;
2,133,775,862 ✔
645
  const double z = r.z - z0_;
2,133,775,862 ✔
646
  const double k = x * u.x + y * u.y + z * u.z;
2,133,775,862 ✔
647
  const double c = x * x + y * y + z * z - radius_ * radius_;
2,133,775,862 ✔
648
  const double quad = k * k - c;
2,133,775,862 ✔
649

650
  if (quad < 0.0) {
2,133,775,862 ✔
651
    // No intersection with sphere.
652
    return INFTY;
653

654
  } else if (coincident || std::abs(c) < FP_COINCIDENT) {
1,947,230,619 !
655
    // Particle is on the sphere, thus one distance is positive/negative and
656
    // the other is zero. The sign of k determines if we are facing in or out.
657
    if (k >= 0.0) {
117,484,513 ✔
658
      return INFTY;
659
    } else {
660
      return -k + sqrt(quad);
66,996,266 ✔
661
    }
662

663
  } else if (c < 0.0) {
1,829,746,106 ✔
664
    // Particle is inside the sphere, thus one distance must be negative and
665
    // one must be positive. The positive distance will be the one with
666
    // negative sign on sqrt(quad)
667
    return -k + sqrt(quad);
1,762,322,365 ✔
668

669
  } else {
670
    // Particle is outside the sphere, thus both distances are either positive
671
    // or negative. If positive, the smaller distance is the one with positive
672
    // sign on sqrt(quad).
673
    const double d = -k - sqrt(quad);
67,423,741 ✔
674
    if (d < 0.0)
67,423,741 ✔
675
      return INFTY;
676
    return d;
52,696,158 ✔
677
  }
678
}
679

680
Direction SurfaceSphere::normal(Position r) const
389,726,810 ✔
681
{
682
  return {2.0 * (r.x - x0_), 2.0 * (r.y - y0_), 2.0 * (r.z - z0_)};
389,726,810 ✔
683
}
684

685
void SurfaceSphere::to_hdf5_inner(hid_t group_id) const
7,676 ✔
686
{
687
  write_string(group_id, "type", "sphere", false);
7,676 ✔
688
  array<double, 4> coeffs {{x0_, y0_, z0_, radius_}};
7,676 ✔
689
  write_dataset(group_id, "coefficients", coeffs);
7,676 ✔
690
}
7,676 ✔
691

692
BoundingBox SurfaceSphere::bounding_box(bool pos_side) const
×
693
{
694
  if (!pos_side) {
×
695
    return {{x0_ - radius_, y0_ - radius_, z0_ - radius_},
×
696
      {x0_ + radius_, y0_ + radius_, z0_ + radius_}};
×
697
  } else {
698
    return {};
×
699
  }
700
}
701

702
//==============================================================================
703
// Generic functions for x-, y-, and z-, cones
704
//==============================================================================
705

706
// The first template parameter indicates which axis the cone is aligned to.
707
// The other two parameters indicate the other two axes.  offset1, offset2,
708
// and offset3 should correspond with i1, i2, and i3, respectively.
709
template<int i1, int i2, int i3>
710
double axis_aligned_cone_evaluate(
325,853 ✔
711
  Position r, double offset1, double offset2, double offset3, double radius_sq)
712
{
713
  const double r1 = r.get<i1>() - offset1;
325,853 ✔
714
  const double r2 = r.get<i2>() - offset2;
325,853 ✔
715
  const double r3 = r.get<i3>() - offset3;
325,853 ✔
716
  return r2 * r2 + r3 * r3 - radius_sq * r1 * r1;
325,853 ✔
717
}
718

719
// The first template parameter indicates which axis the cone is aligned to.
720
// The other two parameters indicate the other two axes.  offset1, offset2,
721
// and offset3 should correspond with i1, i2, and i3, respectively.
722
template<int i1, int i2, int i3>
723
double axis_aligned_cone_distance(Position r, Direction u, bool coincident,
978,021 ✔
724
  double offset1, double offset2, double offset3, double radius_sq)
725
{
726
  const double r1 = r.get<i1>() - offset1;
978,021 ✔
727
  const double r2 = r.get<i2>() - offset2;
978,021 ✔
728
  const double r3 = r.get<i3>() - offset3;
978,021 ✔
729
  const double a = u.get<i2>() * u.get<i2>() + u.get<i3>() * u.get<i3>() -
978,021 ✔
730
                   radius_sq * u.get<i1>() * u.get<i1>();
978,021 ✔
731
  const double k =
978,021 ✔
732
    r2 * u.get<i2>() + r3 * u.get<i3>() - radius_sq * r1 * u.get<i1>();
978,021 ✔
733
  const double c = r2 * r2 + r3 * r3 - radius_sq * r1 * r1;
978,021 ✔
734
  double quad = k * k - a * c;
978,021 ✔
735

736
  double d;
737

738
  if (quad < 0.0) {
978,021 !
739
    // No intersection with cone.
740
    return INFTY;
741

742
  } else if (coincident || std::abs(c) < FP_COINCIDENT) {
978,021 !
743
    // Particle is on the cone, thus one distance is positive/negative
744
    // and the other is zero. The sign of k determines if we are facing in or
745
    // out.
746
    if (k >= 0.0) {
59,510 !
747
      d = (-k - sqrt(quad)) / a;
×
748
    } else {
749
      d = (-k + sqrt(quad)) / a;
59,510 ✔
750
    }
751

752
  } else {
753
    // Calculate both solutions to the quadratic.
754
    quad = sqrt(quad);
918,511 ✔
755
    d = (-k - quad) / a;
918,511 ✔
756
    const double b = (-k + quad) / a;
918,511 ✔
757

758
    // Determine the smallest positive solution.
759
    if (d < 0.0) {
918,511 ✔
760
      if (b > 0.0)
780,043 ✔
761
        d = b;
665,104 ✔
762
    } else {
763
      if (b > 0.0) {
138,468 !
764
        if (b < d)
138,468 !
765
          d = b;
138,468 ✔
766
      }
767
    }
768
  }
769

770
  // If the distance was negative, set boundary distance to infinity.
771
  if (d <= 0.0)
978,021 ✔
772
    return INFTY;
136,422 ✔
773
  return d;
774
}
775

776
// The first template parameter indicates which axis the cone is aligned to.
777
// The other two parameters indicate the other two axes.  offset1, offset2,
778
// and offset3 should correspond with i1, i2, and i3, respectively.
779
template<int i1, int i2, int i3>
780
Direction axis_aligned_cone_normal(
59,510 ✔
781
  Position r, double offset1, double offset2, double offset3, double radius_sq)
782
{
783
  Direction u;
784
  u.get<i1>() = -2.0 * radius_sq * (r.get<i1>() - offset1);
59,510 ✔
785
  u.get<i2>() = 2.0 * (r.get<i2>() - offset2);
59,510 ✔
786
  u.get<i3>() = 2.0 * (r.get<i3>() - offset3);
59,510 ✔
787
  return u;
788
}
789

790
//==============================================================================
791
// SurfaceXCone implementation
792
//==============================================================================
793

794
SurfaceXCone::SurfaceXCone(pugi::xml_node surf_node) : Surface(surf_node)
×
795
{
796
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &radius_sq_});
×
797
}
×
798

799
double SurfaceXCone::evaluate(Position r) const
×
800
{
801
  return axis_aligned_cone_evaluate<0, 1, 2>(r, x0_, y0_, z0_, radius_sq_);
×
802
}
803

804
double SurfaceXCone::distance(Position r, Direction u, bool coincident) const
×
805
{
806
  return axis_aligned_cone_distance<0, 1, 2>(
×
807
    r, u, coincident, x0_, y0_, z0_, radius_sq_);
×
808
}
809

810
Direction SurfaceXCone::normal(Position r) const
×
811
{
812
  return axis_aligned_cone_normal<0, 1, 2>(r, x0_, y0_, z0_, radius_sq_);
×
813
}
814

815
void SurfaceXCone::to_hdf5_inner(hid_t group_id) const
×
816
{
817
  write_string(group_id, "type", "x-cone", false);
×
818
  array<double, 4> coeffs {{x0_, y0_, z0_, radius_sq_}};
×
819
  write_dataset(group_id, "coefficients", coeffs);
×
820
}
×
821

822
//==============================================================================
823
// SurfaceYCone implementation
824
//==============================================================================
825

826
SurfaceYCone::SurfaceYCone(pugi::xml_node surf_node) : Surface(surf_node)
×
827
{
828
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &radius_sq_});
×
829
}
×
830

831
double SurfaceYCone::evaluate(Position r) const
×
832
{
833
  return axis_aligned_cone_evaluate<1, 0, 2>(r, y0_, x0_, z0_, radius_sq_);
×
834
}
835

836
double SurfaceYCone::distance(Position r, Direction u, bool coincident) const
×
837
{
838
  return axis_aligned_cone_distance<1, 0, 2>(
×
839
    r, u, coincident, y0_, x0_, z0_, radius_sq_);
×
840
}
841

842
Direction SurfaceYCone::normal(Position r) const
×
843
{
844
  return axis_aligned_cone_normal<1, 0, 2>(r, y0_, x0_, z0_, radius_sq_);
×
845
}
846

847
void SurfaceYCone::to_hdf5_inner(hid_t group_id) const
×
848
{
849
  write_string(group_id, "type", "y-cone", false);
×
850
  array<double, 4> coeffs {{x0_, y0_, z0_, radius_sq_}};
×
851
  write_dataset(group_id, "coefficients", coeffs);
×
852
}
×
853

854
//==============================================================================
855
// SurfaceZCone implementation
856
//==============================================================================
857

858
SurfaceZCone::SurfaceZCone(pugi::xml_node surf_node) : Surface(surf_node)
15 ✔
859
{
860
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &radius_sq_});
15 ✔
861
}
15 ✔
862

863
double SurfaceZCone::evaluate(Position r) const
325,853 ✔
864
{
865
  return axis_aligned_cone_evaluate<2, 0, 1>(r, z0_, x0_, y0_, radius_sq_);
325,853 ✔
866
}
867

868
double SurfaceZCone::distance(Position r, Direction u, bool coincident) const
978,021 ✔
869
{
870
  return axis_aligned_cone_distance<2, 0, 1>(
978,021 ✔
871
    r, u, coincident, z0_, x0_, y0_, radius_sq_);
978,021 ✔
872
}
873

874
Direction SurfaceZCone::normal(Position r) const
59,510 ✔
875
{
876
  return axis_aligned_cone_normal<2, 0, 1>(r, z0_, x0_, y0_, radius_sq_);
59,510 ✔
877
}
878

879
void SurfaceZCone::to_hdf5_inner(hid_t group_id) const
11 ✔
880
{
881
  write_string(group_id, "type", "z-cone", false);
11 ✔
882
  array<double, 4> coeffs {{x0_, y0_, z0_, radius_sq_}};
11 ✔
883
  write_dataset(group_id, "coefficients", coeffs);
11 ✔
884
}
11 ✔
885

886
//==============================================================================
887
// SurfaceQuadric implementation
888
//==============================================================================
889

890
SurfaceQuadric::SurfaceQuadric(pugi::xml_node surf_node) : Surface(surf_node)
26 ✔
891
{
892
  read_coeffs(
52 ✔
893
    surf_node, id_, {&A_, &B_, &C_, &D_, &E_, &F_, &G_, &H_, &J_, &K_});
26 ✔
894
}
26 ✔
895

896
double SurfaceQuadric::evaluate(Position r) const
177,411 ✔
897
{
898
  const double x = r.x;
177,411 ✔
899
  const double y = r.y;
177,411 ✔
900
  const double z = r.z;
177,411 ✔
901
  return x * (A_ * x + D_ * y + G_) + y * (B_ * y + E_ * z + H_) +
177,411 ✔
902
         z * (C_ * z + F_ * x + J_) + K_;
177,411 ✔
903
}
904

905
double SurfaceQuadric::distance(
316,690 ✔
906
  Position r, Direction ang, bool coincident) const
907
{
908
  const double& x = r.x;
316,690 ✔
909
  const double& y = r.y;
316,690 ✔
910
  const double& z = r.z;
316,690 ✔
911
  const double& u = ang.x;
316,690 ✔
912
  const double& v = ang.y;
316,690 ✔
913
  const double& w = ang.z;
316,690 ✔
914

915
  const double a =
316,690 ✔
916
    A_ * u * u + B_ * v * v + C_ * w * w + D_ * u * v + E_ * v * w + F_ * u * w;
316,690 ✔
917
  const double k = A_ * u * x + B_ * v * y + C_ * w * z +
316,690 ✔
918
                   0.5 * (D_ * (u * y + v * x) + E_ * (v * z + w * y) +
316,690 ✔
919
                           F_ * (w * x + u * z) + G_ * u + H_ * v + J_ * w);
316,690 ✔
920
  const double c = A_ * x * x + B_ * y * y + C_ * z * z + D_ * x * y +
316,690 ✔
921
                   E_ * y * z + F_ * x * z + G_ * x + H_ * y + J_ * z + K_;
316,690 ✔
922
  double quad = k * k - a * c;
316,690 ✔
923

924
  double d;
316,690 ✔
925

926
  if (quad < 0.0) {
316,690 !
927
    // No intersection with surface.
928
    return INFTY;
929

930
  } else if (coincident || std::abs(c) < FP_COINCIDENT) {
316,690 !
931
    // Particle is on the surface, thus one distance is positive/negative and
932
    // the other is zero. The sign of k determines which distance is zero and
933
    // which is not. Additionally, if a is zero, it means the particle is on
934
    // a plane-like surface.
935
    if (a == 0.0) {
36,058 !
936
      d = INFTY; // see the below explanation
937
    } else if (k >= 0.0) {
36,058 !
938
      d = (-k - sqrt(quad)) / a;
×
939
    } else {
940
      d = (-k + sqrt(quad)) / a;
36,058 ✔
941
    }
942

943
  } else if (a == 0.0) {
280,632 !
944
    // Given the orientation of the particle, the quadric looks like a plane in
945
    // this case, and thus we have only one solution despite potentially having
946
    // quad > 0.0. While the term under the square root may be real, in one
947
    // case of the +/- of the quadratic formula, 0/0 results, and in another, a
948
    // finite value over 0 results. Applying L'Hopital's to the 0/0 case gives
949
    // the below. Alternatively this can be found by simply putting a=0 in the
950
    // equation ax^2 + bx + c = 0.
951
    d = -0.5 * c / k;
×
952
  } else {
953
    // Calculate both solutions to the quadratic.
954
    quad = sqrt(quad);
280,632 ✔
955
    d = (-k - quad) / a;
280,632 ✔
956
    double b = (-k + quad) / a;
280,632 ✔
957

958
    // Determine the smallest positive solution.
959
    if (d < 0.0) {
280,632 ✔
960
      if (b > 0.0)
280,621 !
961
        d = b;
280,621 ✔
962
    } else {
963
      if (b > 0.0) {
11 !
964
        if (b < d)
11 !
965
          d = b;
×
966
      }
967
    }
968
  }
969

970
  // If the distance was negative, set boundary distance to infinity.
971
  if (d <= 0.0)
316,690 !
972
    return INFTY;
×
973
  return d;
974
}
975

976
Direction SurfaceQuadric::normal(Position r) const
36,058 ✔
977
{
978
  const double& x = r.x;
36,058 ✔
979
  const double& y = r.y;
36,058 ✔
980
  const double& z = r.z;
36,058 ✔
981
  return {2.0 * A_ * x + D_ * y + F_ * z + G_,
36,058 ✔
982
    2.0 * B_ * y + D_ * x + E_ * z + H_, 2.0 * C_ * z + E_ * y + F_ * x + J_};
36,058 ✔
983
}
984

985
void SurfaceQuadric::to_hdf5_inner(hid_t group_id) const
11 ✔
986
{
987
  write_string(group_id, "type", "quadric", false);
11 ✔
988
  array<double, 10> coeffs {{A_, B_, C_, D_, E_, F_, G_, H_, J_, K_}};
11 ✔
989
  write_dataset(group_id, "coefficients", coeffs);
11 ✔
990
}
11 ✔
991

992
//==============================================================================
993
// Torus helper functions
994
//==============================================================================
995

996
double torus_distance(double x1, double x2, double x3, double u1, double u2,
26,534,563 ✔
997
  double u3, double A, double B, double C, bool coincident)
998
{
999
  // Coefficients for equation: (c2 t^2 + c1 t + c0)^2 = c2' t^2 + c1' t + c0'
1000
  double D = (C * C) / (B * B);
26,534,563 ✔
1001
  double c2 = u1 * u1 + u2 * u2 + D * u3 * u3;
26,534,563 ✔
1002
  double c1 = 2 * (u1 * x1 + u2 * x2 + D * u3 * x3);
26,534,563 ✔
1003
  double c0 = x1 * x1 + x2 * x2 + D * x3 * x3 + A * A - C * C;
26,534,563 ✔
1004
  double four_A2 = 4 * A * A;
26,534,563 ✔
1005
  double c2p = four_A2 * (u1 * u1 + u2 * u2);
26,534,563 ✔
1006
  double c1p = 2 * four_A2 * (u1 * x1 + u2 * x2);
26,534,563 ✔
1007
  double c0p = four_A2 * (x1 * x1 + x2 * x2);
26,534,563 ✔
1008

1009
  // Coefficient for equation: a t^4 + b t^3 + c t^2 + d t + e = 0. If the point
1010
  // is coincident, the 'e' coefficient should be zero. Explicitly setting it to
1011
  // zero helps avoid numerical issues below with root finding.
1012
  double coeff[5];
26,534,563 ✔
1013
  coeff[0] = coincident ? 0.0 : c0 * c0 - c0p;
26,534,563 ✔
1014
  coeff[1] = 2 * c0 * c1 - c1p;
26,534,563 ✔
1015
  coeff[2] = c1 * c1 + 2 * c0 * c2 - c2p;
26,534,563 ✔
1016
  coeff[3] = 2 * c1 * c2;
26,534,563 ✔
1017
  coeff[4] = c2 * c2;
26,534,563 ✔
1018

1019
  std::complex<double> roots[4];
26,534,563 ✔
1020
  oqs::quartic_solver(coeff, roots);
26,534,563 ✔
1021

1022
  // Find smallest positive, real root. In the case where the particle is
1023
  // coincident with the surface, we are sure to have one root very close to
1024
  // zero but possibly small and positive. A tolerance is set to discard that
1025
  // zero.
1026
  double distance = INFTY;
26,534,563 ✔
1027
  double cutoff = coincident ? TORUS_TOL : 0.0;
26,534,563 ✔
1028
  for (int i = 0; i < 4; ++i) {
132,672,815 ✔
1029
    if (roots[i].imag() == 0) {
106,138,252 ✔
1030
      double root = roots[i].real();
25,944,028 ✔
1031
      if (root > cutoff && root < distance) {
25,944,028 ✔
1032
        // Avoid roots corresponding to internal surfaces
1033
        double s1 = x1 + u1 * root;
10,411,995 ✔
1034
        double s2 = x2 + u2 * root;
10,411,995 ✔
1035
        double s3 = x3 + u3 * root;
10,411,995 ✔
1036
        double check = D * s3 * s3 + s1 * s1 + s2 * s2 + A * A - C * C;
10,411,995 ✔
1037
        if (check >= 0) {
10,411,995 !
1038
          distance = root;
10,411,995 ✔
1039
        }
1040
      }
1041
    }
1042
  }
1043
  return distance;
26,534,563 ✔
1044
}
1045

1046
//==============================================================================
1047
// SurfaceXTorus implementation
1048
//==============================================================================
1049

1050
SurfaceXTorus::SurfaceXTorus(pugi::xml_node surf_node) : Surface(surf_node)
70 ✔
1051
{
1052
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &A_, &B_, &C_});
70 ✔
1053
}
70 ✔
1054

1055
void SurfaceXTorus::to_hdf5_inner(hid_t group_id) const
55 ✔
1056
{
1057
  write_string(group_id, "type", "x-torus", false);
55 ✔
1058
  std::array<double, 6> coeffs {{x0_, y0_, z0_, A_, B_, C_}};
55 ✔
1059
  write_dataset(group_id, "coefficients", coeffs);
55 ✔
1060
}
55 ✔
1061

1062
double SurfaceXTorus::evaluate(Position r) const
811,264 ✔
1063
{
1064
  double x = r.x - x0_;
811,264 ✔
1065
  double y = r.y - y0_;
811,264 ✔
1066
  double z = r.z - z0_;
811,264 ✔
1067
  return (x * x) / (B_ * B_) +
1,622,528 ✔
1068
         std::pow(std::sqrt(y * y + z * z) - A_, 2) / (C_ * C_) - 1.;
811,264 ✔
1069
}
1070

1071
BoundingBox SurfaceXTorus::bounding_box(bool pos_side) const
22 ✔
1072
{
1073
  // The torus interior is compact: it extends +/-B_ along the axis of
1074
  // revolution and +/-(A_ + C_) in the two perpendicular directions. Mirrors
1075
  // XTorus.bounding_box on the Python side.
1076
  if (pos_side)
22 ✔
1077
    return {};
11 ✔
1078
  return {{x0_ - B_, y0_ - A_ - C_, z0_ - A_ - C_},
11 ✔
1079
    {x0_ + B_, y0_ + A_ + C_, z0_ + A_ + C_}};
11 ✔
1080
}
1081

1082
double SurfaceXTorus::distance(Position r, Direction u, bool coincident) const
8,348,615 ✔
1083
{
1084
  double x = r.x - x0_;
8,348,615 ✔
1085
  double y = r.y - y0_;
8,348,615 ✔
1086
  double z = r.z - z0_;
8,348,615 ✔
1087
  return torus_distance(y, z, x, u.y, u.z, u.x, A_, B_, C_, coincident);
8,348,615 ✔
1088
}
1089

NEW
1090
Direction SurfaceXTorus::normal(Position r) const
×
1091
{
1092
  // reduce the expansion of the full form for torus
1093
  double x = r.x - x0_;
×
1094
  double y = r.y - y0_;
×
1095
  double z = r.z - z0_;
×
1096

1097
  // f(x,y,z) = x^2/B^2 + (sqrt(y^2 + z^2) - A)^2/C^2 - 1
1098
  // ∂f/∂x = 2x/B^2
1099
  // ∂f/∂y = 2y(g - A)/(g*C^2) where g = sqrt(y^2 + z^2)
1100
  // ∂f/∂z = 2z(g - A)/(g*C^2)
1101
  // Multiplying by g*C^2*B^2 / 2 gives:
1102
  double g = std::sqrt(y * y + z * z);
×
1103
  double nx = C_ * C_ * g * x;
×
1104
  double ny = y * (g - A_) * B_ * B_;
×
1105
  double nz = z * (g - A_) * B_ * B_;
×
1106
  Direction n(nx, ny, nz);
×
1107
  return n / n.norm();
×
1108
}
1109

1110
//==============================================================================
1111
// SurfaceYTorus implementation
1112
//==============================================================================
1113

1114
SurfaceYTorus::SurfaceYTorus(pugi::xml_node surf_node) : Surface(surf_node)
70 ✔
1115
{
1116
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &A_, &B_, &C_});
70 ✔
1117
}
70 ✔
1118

1119
void SurfaceYTorus::to_hdf5_inner(hid_t group_id) const
55 ✔
1120
{
1121
  write_string(group_id, "type", "y-torus", false);
55 ✔
1122
  std::array<double, 6> coeffs {{x0_, y0_, z0_, A_, B_, C_}};
55 ✔
1123
  write_dataset(group_id, "coefficients", coeffs);
55 ✔
1124
}
55 ✔
1125

1126
double SurfaceYTorus::evaluate(Position r) const
780,719 ✔
1127
{
1128
  double x = r.x - x0_;
780,719 ✔
1129
  double y = r.y - y0_;
780,719 ✔
1130
  double z = r.z - z0_;
780,719 ✔
1131
  return (y * y) / (B_ * B_) +
1,561,438 ✔
1132
         std::pow(std::sqrt(x * x + z * z) - A_, 2) / (C_ * C_) - 1.;
780,719 ✔
1133
}
1134

1135
BoundingBox SurfaceYTorus::bounding_box(bool pos_side) const
22 ✔
1136
{
1137
  if (pos_side)
22 ✔
1138
    return {};
11 ✔
1139
  return {{x0_ - A_ - C_, y0_ - B_, z0_ - A_ - C_},
11 ✔
1140
    {x0_ + A_ + C_, y0_ + B_, z0_ + A_ + C_}};
11 ✔
1141
}
1142

1143
double SurfaceYTorus::distance(Position r, Direction u, bool coincident) const
8,413,900 ✔
1144
{
1145
  double x = r.x - x0_;
8,413,900 ✔
1146
  double y = r.y - y0_;
8,413,900 ✔
1147
  double z = r.z - z0_;
8,413,900 ✔
1148
  return torus_distance(x, z, y, u.x, u.z, u.y, A_, B_, C_, coincident);
8,413,900 ✔
1149
}
1150

NEW
1151
Direction SurfaceYTorus::normal(Position r) const
×
1152
{
1153
  // reduce the expansion of the full form for torus
1154
  double x = r.x - x0_;
×
1155
  double y = r.y - y0_;
×
1156
  double z = r.z - z0_;
×
1157

1158
  // f(x,y,z) = y^2/B^2 + (sqrt(x^2 + z^2) - A)^2/C^2 - 1
1159
  // ∂f/∂x = 2x(g - A)/(g*C^2) where g = sqrt(x^2 + z^2)
1160
  // ∂f/∂y = 2y/B^2
1161
  // ∂f/∂z = 2z(g - A)/(g*C^2)
1162
  // Multiplying by g*C^2*B^2 / 2 gives:
1163
  double g = std::sqrt(x * x + z * z);
×
1164
  double nx = x * (g - A_) * B_ * B_;
×
1165
  double ny = C_ * C_ * g * y;
×
1166
  double nz = z * (g - A_) * B_ * B_;
×
1167
  Direction n(nx, ny, nz);
×
1168
  return n / n.norm();
×
1169
}
1170

1171
//==============================================================================
1172
// SurfaceZTorus implementation
1173
//==============================================================================
1174

1175
SurfaceZTorus::SurfaceZTorus(pugi::xml_node surf_node) : Surface(surf_node)
115 ✔
1176
{
1177
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &A_, &B_, &C_});
115 ✔
1178
}
115 ✔
1179

1180
void SurfaceZTorus::to_hdf5_inner(hid_t group_id) const
88 ✔
1181
{
1182
  write_string(group_id, "type", "z-torus", false);
88 ✔
1183
  std::array<double, 6> coeffs {{x0_, y0_, z0_, A_, B_, C_}};
88 ✔
1184
  write_dataset(group_id, "coefficients", coeffs);
88 ✔
1185
}
88 ✔
1186

1187
double SurfaceZTorus::evaluate(Position r) const
1,307,175 ✔
1188
{
1189
  double x = r.x - x0_;
1,307,175 ✔
1190
  double y = r.y - y0_;
1,307,175 ✔
1191
  double z = r.z - z0_;
1,307,175 ✔
1192
  return (z * z) / (B_ * B_) +
2,614,350 ✔
1193
         std::pow(std::sqrt(x * x + y * y) - A_, 2) / (C_ * C_) - 1.;
1,307,175 ✔
1194
}
1195

1196
BoundingBox SurfaceZTorus::bounding_box(bool pos_side) const
22 ✔
1197
{
1198
  if (pos_side)
22 ✔
1199
    return {};
11 ✔
1200
  return {{x0_ - A_ - C_, y0_ - A_ - C_, z0_ - B_},
11 ✔
1201
    {x0_ + A_ + C_, y0_ + A_ + C_, z0_ + B_}};
11 ✔
1202
}
1203

1204
double SurfaceZTorus::distance(Position r, Direction u, bool coincident) const
9,772,048 ✔
1205
{
1206
  double x = r.x - x0_;
9,772,048 ✔
1207
  double y = r.y - y0_;
9,772,048 ✔
1208
  double z = r.z - z0_;
9,772,048 ✔
1209
  return torus_distance(x, y, z, u.x, u.y, u.z, A_, B_, C_, coincident);
9,772,048 ✔
1210
}
1211

NEW
1212
Direction SurfaceZTorus::normal(Position r) const
×
1213
{
1214
  // reduce the expansion of the full form for torus
1215
  double x = r.x - x0_;
×
1216
  double y = r.y - y0_;
×
1217
  double z = r.z - z0_;
×
1218

1219
  // f(x,y,z) = z^2/B^2 + (sqrt(x^2 + y^2) - A)^2/C^2 - 1
1220
  // ∂f/∂x = 2x(g - A)/(g*C^2) where g = sqrt(x^2 + y^2)
1221
  // ∂f/∂y = 2y(g - A)/(g*C^2)
1222
  // ∂f/∂z = 2z/B^2
1223
  // Multiplying by g*C^2*B^2 / 2 gives:
1224
  double g = std::sqrt(x * x + y * y);
×
1225
  double nx = x * (g - A_) * B_ * B_;
×
1226
  double ny = y * (g - A_) * B_ * B_;
×
1227
  double nz = C_ * C_ * g * z;
×
1228
  Position n(nx, ny, nz);
×
1229
  return n / n.norm();
×
1230
}
1231

1232
//==============================================================================
1233

1234
void read_surfaces(pugi::xml_node node,
9,323 ✔
1235
  std::set<std::pair<int, int>>& periodic_pairs,
1236
  std::unordered_map<int, double>& albedo_map,
1237
  std::unordered_map<int, int>& periodic_sense_map)
1238
{
1239
  // Count the number of surfaces
1240
  int n_surfaces = 0;
9,323 ✔
1241
  for (pugi::xml_node surf_node : node.children("surface")) {
57,654 ✔
1242
    n_surfaces++;
48,331 ✔
1243
  }
1244

1245
  // Loop over XML surface elements and populate the array.  Keep track of
1246
  // periodic surfaces and their albedos.
1247
  model::surfaces.reserve(n_surfaces);
9,323 ✔
1248
  {
9,323 ✔
1249
    pugi::xml_node surf_node;
9,323 ✔
1250
    int i_surf;
9,323 ✔
1251
    for (surf_node = node.child("surface"), i_surf = 0; surf_node;
57,654 ✔
1252
         surf_node = surf_node.next_sibling("surface"), i_surf++) {
48,331 ✔
1253
      std::string surf_type = get_node_value(surf_node, "type", true, true);
48,331 ✔
1254

1255
      // Allocate and initialize the new surface
1256

1257
      if (surf_type == "x-plane") {
48,331 ✔
1258
        model::surfaces.push_back(make_unique<SurfaceXPlane>(surf_node));
12,099 ✔
1259

1260
      } else if (surf_type == "y-plane") {
36,232 ✔
1261
        model::surfaces.push_back(make_unique<SurfaceYPlane>(surf_node));
10,907 ✔
1262

1263
      } else if (surf_type == "z-plane") {
25,325 ✔
1264
        model::surfaces.push_back(make_unique<SurfaceZPlane>(surf_node));
8,026 ✔
1265

1266
      } else if (surf_type == "plane") {
17,299 ✔
1267
        model::surfaces.push_back(make_unique<SurfacePlane>(surf_node));
1,964 ✔
1268

1269
      } else if (surf_type == "x-cylinder") {
15,335 ✔
1270
        model::surfaces.push_back(make_unique<SurfaceXCylinder>(surf_node));
45 ✔
1271

1272
      } else if (surf_type == "y-cylinder") {
15,290 ✔
1273
        model::surfaces.push_back(make_unique<SurfaceYCylinder>(surf_node));
15 ✔
1274

1275
      } else if (surf_type == "z-cylinder") {
15,275 ✔
1276
        model::surfaces.push_back(make_unique<SurfaceZCylinder>(surf_node));
5,680 ✔
1277

1278
      } else if (surf_type == "sphere") {
9,595 ✔
1279
        model::surfaces.push_back(make_unique<SurfaceSphere>(surf_node));
9,343 ✔
1280

1281
      } else if (surf_type == "x-cone") {
252 !
1282
        model::surfaces.push_back(make_unique<SurfaceXCone>(surf_node));
×
1283

1284
      } else if (surf_type == "y-cone") {
252 !
1285
        model::surfaces.push_back(make_unique<SurfaceYCone>(surf_node));
×
1286

1287
      } else if (surf_type == "z-cone") {
252 ✔
1288
        model::surfaces.push_back(make_unique<SurfaceZCone>(surf_node));
15 ✔
1289

1290
      } else if (surf_type == "quadric") {
237 ✔
1291
        model::surfaces.push_back(make_unique<SurfaceQuadric>(surf_node));
15 ✔
1292

1293
      } else if (surf_type == "x-torus") {
222 ✔
1294
        model::surfaces.push_back(std::make_unique<SurfaceXTorus>(surf_node));
59 ✔
1295

1296
      } else if (surf_type == "y-torus") {
163 ✔
1297
        model::surfaces.push_back(std::make_unique<SurfaceYTorus>(surf_node));
59 ✔
1298

1299
      } else if (surf_type == "z-torus") {
104 !
1300
        model::surfaces.push_back(std::make_unique<SurfaceZTorus>(surf_node));
104 ✔
1301

1302
      } else {
1303
        fatal_error(fmt::format("Invalid surface type, \"{}\"", surf_type));
×
1304
      }
1305

1306
      // Check for a periodic surface
1307
      if (check_for_node(surf_node, "boundary")) {
48,331 ✔
1308
        std::string surf_bc = get_node_value(surf_node, "boundary", true, true);
28,569 ✔
1309
        if (surf_bc == "periodic") {
28,569 ✔
1310
          periodic_sense_map[model::surfaces.back()->id_] = 0;
546 ✔
1311
          // Check for surface albedo. Skip sanity check as it is already done
1312
          // in the Surface class's constructor.
1313
          if (check_for_node(surf_node, "albedo")) {
546 !
1314
            albedo_map[model::surfaces.back()->id_] =
×
1315
              std::stod(get_node_value(surf_node, "albedo"));
×
1316
          }
1317
          if (check_for_node(surf_node, "periodic_surface_id")) {
546 ✔
1318
            int i_periodic =
356 ✔
1319
              std::stoi(get_node_value(surf_node, "periodic_surface_id"));
712 ✔
1320
            int lo_id = std::min(model::surfaces.back()->id_, i_periodic);
356 ✔
1321
            int hi_id = std::max(model::surfaces.back()->id_, i_periodic);
356 ✔
1322
            periodic_pairs.insert({lo_id, hi_id});
356 ✔
1323
          } else {
1324
            periodic_pairs.insert({model::surfaces.back()->id_, -1});
190 ✔
1325
          }
1326
        }
1327
      }
28,569 ✔
1328
    }
48,331 ✔
1329
  }
1330

1331
  // Fill the surface map
1332
  for (int i_surf = 0; i_surf < model::surfaces.size(); i_surf++) {
57,654 ✔
1333
    int id = model::surfaces[i_surf]->id_;
48,331 !
1334
    auto in_map = model::surface_map.find(id);
48,331 !
1335
    if (in_map == model::surface_map.end()) {
48,331 !
1336
      model::surface_map[id] = i_surf;
48,331 ✔
1337
    } else {
1338
      fatal_error(
×
1339
        fmt::format("Two or more surfaces use the same unique ID: {}", id));
×
1340
    }
1341
  }
1342
}
9,323 ✔
1343

1344
void prepare_boundary_conditions(std::set<std::pair<int, int>>& periodic_pairs,
9,321 ✔
1345
  std::unordered_map<int, double>& albedo_map,
1346
  std::unordered_map<int, int>& periodic_sense_map)
1347
{
1348
  // Fill the senses map for periodic surfaces
1349
  auto n_periodic = periodic_sense_map.size();
9,321 ✔
1350
  for (const auto& cell : model::cells) {
9,578 ✔
1351
    if (n_periodic == 0)
9,447 ✔
1352
      break; // Early exit once all periodic surfaces found
1353

1354
    for (auto s : cell->surfaces()) {
1,335 ✔
1355
      auto surf_idx = std::abs(s) - 1;
1,078 ✔
1356
      auto id = model::surfaces[surf_idx]->id_;
1,078 ✔
1357

1358
      if (periodic_sense_map.count(id)) {
1,078 ✔
1359
        periodic_sense_map[id] = std::copysign(1, s);
546 ✔
1360
        --n_periodic;
546 ✔
1361
      }
1362
    }
257 ✔
1363
  }
1364

1365
  // Resolve unpaired periodic surfaces.  A lambda function is used with
1366
  // std::find_if to identify the unpaired surfaces.
1367
  auto is_unresolved_pair = [](const std::pair<int, int> p) {
9,689 ✔
1368
    return p.second == -1;
368 ✔
1369
  };
1370
  auto first_unresolved = std::find_if(
9,321 ✔
1371
    periodic_pairs.begin(), periodic_pairs.end(), is_unresolved_pair);
1372
  if (first_unresolved != periodic_pairs.end()) {
9,321 ✔
1373
    // Found one unpaired surface; search for a second one
1374
    auto next_elem = first_unresolved;
95 ✔
1375
    next_elem++;
95 !
1376
    auto second_unresolved =
95 !
1377
      std::find_if(next_elem, periodic_pairs.end(), is_unresolved_pair);
95 !
1378
    if (second_unresolved == periodic_pairs.end()) {
95 !
1379
      fatal_error("Found only one periodic surface without a specified partner."
×
1380
                  " Please specify the partner for each periodic surface.");
1381
    }
1382

1383
    // Make sure there isn't a third unpaired surface
1384
    next_elem = second_unresolved;
95 ✔
1385
    next_elem++;
95 !
1386
    auto third_unresolved =
95 !
1387
      std::find_if(next_elem, periodic_pairs.end(), is_unresolved_pair);
95 !
1388
    if (third_unresolved != periodic_pairs.end()) {
95 !
1389
      fatal_error(
×
1390
        "Found at least three periodic surfaces without a specified "
1391
        "partner. Please specify the partner for each periodic surface.");
1392
    }
1393

1394
    // Add the completed pair and remove the old, unpaired entries
1395
    int lo_id = std::min(first_unresolved->first, second_unresolved->first);
95 !
1396
    int hi_id = std::max(first_unresolved->first, second_unresolved->first);
95 !
1397
    periodic_pairs.insert({lo_id, hi_id});
95 ✔
1398
    periodic_pairs.erase(first_unresolved);
95 ✔
1399
    periodic_pairs.erase(second_unresolved);
95 ✔
1400
  }
1401

1402
  // Assign the periodic boundary conditions with albedos
1403
  for (auto periodic_pair : periodic_pairs) {
9,594 ✔
1404
    int i_surf = model::surface_map[periodic_pair.first];
273 ✔
1405
    int j_surf = model::surface_map[periodic_pair.second];
273 ✔
1406
    Surface& surf1 {*model::surfaces[i_surf]};
273 ✔
1407
    Surface& surf2 {*model::surfaces[j_surf]};
273 ✔
1408

1409
    // Compute the dot product of the surface normals
1410
    Direction norm1 = surf1.normal({0, 0, 0});
273 ✔
1411
    Direction norm2 = surf2.normal({0, 0, 0});
273 ✔
1412
    norm1 /= norm1.norm();
273 ✔
1413
    norm2 /= norm2.norm();
273 ✔
1414
    double dot_prod = norm1.dot(norm2);
273 ✔
1415

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

1467
    // If albedo data is present in albedo map, set the boundary albedo.
1468
    if (albedo_map.count(surf1.id_)) {
273 !
1469
      surf1.bc_->set_albedo(albedo_map[surf1.id_]);
×
1470
    }
1471
    if (albedo_map.count(surf2.id_)) {
273 !
1472
      surf2.bc_->set_albedo(albedo_map[surf2.id_]);
273 ✔
1473
    }
1474
  }
1475
}
9,321 ✔
1476

1477
void free_memory_surfaces()
9,452 ✔
1478
{
1479
  model::surfaces.clear();
9,452 ✔
1480
  model::surface_map.clear();
9,452 ✔
1481
}
9,452 ✔
1482

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