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

openmc-dev / openmc / 39481612286

20 Oct 2026 07:13PM UTC coverage: 80.772% (-0.7%) from 81.44%
39481612286

Pull #4176

github

web-flow
Merge 596ea72c3 into 9315d8301
Pull Request #4176: Build fine energy group structures

17719 of 25612 branches covered (69.18%)

Branch coverage included in aggregate %.

34 of 34 new or added lines in 2 files covered. (100.0%)

1081 existing lines in 50 files now uncovered.

59639 of 70161 relevant lines covered (85.0%)

18444085.23 hits per line

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

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

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

10
#include <fmt/core.h>
11

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

24
namespace openmc {
25

26
//==============================================================================
27
// Global variables
28
//==============================================================================
29

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

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

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

50
  // Copy the coefficients
51
  int i = 0;
8,054 ✔
52
  for (auto c : coeffs) {
23,548 ✔
53
    *c = coeffs_file[i++];
15,494 ✔
54
  }
55
}
8,054 ✔
56

57
//==============================================================================
58
// Surface implementation
59
//==============================================================================
60

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

63
Surface::Surface(pugi::xml_node surf_node)
8,054 ✔
64
{
65
  if (check_for_node(surf_node, "id")) {
8,054 !
66
    id_ = std::stoi(get_node_value(surf_node, "id"));
16,108 ✔
67
    if (contains(settings::source_write_surf_id, id_) ||
16,108 ✔
68
        settings::source_write_surf_id.empty()) {
7,902 ✔
69
      surf_source_ = true;
7,852 ✔
70
    }
71
  } else {
72
    fatal_error("Must specify id of surface in geometry XML file.");
×
73
  }
74

75
  if (check_for_node(surf_node, "name")) {
8,054 ✔
76
    name_ = get_node_value(surf_node, "name", false);
1,842 ✔
77
  }
78

79
  if (check_for_node(surf_node, "boundary")) {
8,054 ✔
80
    std::string surf_bc = get_node_value(surf_node, "boundary", true, true);
4,802 ✔
81

82
    if (surf_bc == "transmission" || surf_bc == "transmit" || surf_bc.empty()) {
9,604 !
83
      // Leave the bc_ a nullptr
84
    } else if (surf_bc == "vacuum") {
4,802 ✔
85
      bc_ = make_unique<VacuumBC>();
2,380 ✔
86
    } else if (surf_bc == "reflective" || surf_bc == "reflect" ||
2,518 !
87
               surf_bc == "reflecting") {
96 !
88
      bc_ = make_unique<ReflectiveBC>();
2,326 ✔
89
    } else if (surf_bc == "white") {
96 ✔
90
      bc_ = make_unique<WhiteBC>();
12 ✔
91
    } else if (surf_bc == "periodic") {
84 !
92
      // Periodic BCs are handled separately
93
    } else {
94
      fatal_error(fmt::format("Unknown boundary condition \"{}\" specified "
×
95
                              "on surface {}",
96
        surf_bc, id_));
×
97
    }
98

99
    if (check_for_node(surf_node, "albedo") && bc_) {
4,802 !
100
      double surf_alb = std::stod(get_node_value(surf_node, "albedo"));
24 ✔
101

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

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

113
      bc_->set_albedo(surf_alb);
12 ✔
114
    }
115
  }
4,802 ✔
116
}
8,054 ✔
117

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

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

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

140
  // Reflect direction according to normal.
141
  return u.reflect(n);
179,059,884 ✔
142
}
143

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

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

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

158
  // sample azimuthal distribution uniformly
159
  u = rotate_angle(n, mu, nullptr, seed);
183,118 ✔
160

161
  // normalize the direction
162
  return u / u.norm();
183,118 ✔
163
}
164

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

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

174
    if (bc_) {
7,558 ✔
175
      write_string(surf_group, "boundary_type", bc_->type(), false);
4,574 ✔
176
      bc_->to_hdf5(surf_group);
4,574 ✔
177

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

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

195
  if (!name_.empty()) {
7,558 ✔
196
    write_string(surf_group, "name", name_, false);
1,790 ✔
197
  }
198

199
  to_hdf5_inner(surf_group);
7,558 ✔
200

201
  close_group(surf_group);
7,558 ✔
202
}
7,558 ✔
203

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

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

222
//==============================================================================
223
// SurfaceXPlane implementation
224
//==============================================================================
225

226
SurfaceXPlane::SurfaceXPlane(pugi::xml_node surf_node) : Surface(surf_node)
2,162 ✔
227
{
228
  read_coeffs(surf_node, id_, {&x0_});
2,162 ✔
229
}
2,162 ✔
230

231
double SurfaceXPlane::evaluate(Position r) const
731,077,113 ✔
232
{
233
  return r.x - x0_;
731,077,113 ✔
234
}
235

236
double SurfaceXPlane::distance(Position r, Direction u, bool coincident) const
2,105,261,931 ✔
237
{
238
  return axis_aligned_plane_distance<0>(r, u, coincident, x0_);
2,105,261,931 ✔
239
}
240

241
Direction SurfaceXPlane::normal(Position r) const
67,620,864 ✔
242
{
243
  return {1., 0., 0.};
67,620,864 ✔
244
}
245

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

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

262
//==============================================================================
263
// SurfaceYPlane implementation
264
//==============================================================================
265

266
SurfaceYPlane::SurfaceYPlane(pugi::xml_node surf_node) : Surface(surf_node)
1,808 ✔
267
{
268
  read_coeffs(surf_node, id_, {&y0_});
1,808 ✔
269
}
1,808 ✔
270

271
double SurfaceYPlane::evaluate(Position r) const
474,604,685 ✔
272
{
273
  return r.y - y0_;
474,604,685 ✔
274
}
275

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

281
Direction SurfaceYPlane::normal(Position r) const
94,916,154 ✔
282
{
283
  return {0., 1., 0.};
94,916,154 ✔
284
}
285

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

293
BoundingBox SurfaceYPlane::bounding_box(bool pos_side) const
46 ✔
294
{
295
  if (pos_side) {
46 ✔
296
    return {{-INFTY, y0_, -INFTY}, {INFTY, INFTY, INFTY}};
22 ✔
297
  } else {
298
    return {{-INFTY, -INFTY, -INFTY}, {INFTY, y0_, INFTY}};
24 ✔
299
  }
300
}
301

302
//==============================================================================
303
// SurfaceZPlane implementation
304
//==============================================================================
305

306
SurfaceZPlane::SurfaceZPlane(pugi::xml_node surf_node) : Surface(surf_node)
1,336 ✔
307
{
308
  read_coeffs(surf_node, id_, {&z0_});
1,336 ✔
309
}
1,336 ✔
310

311
double SurfaceZPlane::evaluate(Position r) const
733,804,943 ✔
312
{
313
  return r.z - z0_;
733,804,943 ✔
314
}
315

316
double SurfaceZPlane::distance(Position r, Direction u, bool coincident) const
1,548,746,969 ✔
317
{
318
  return axis_aligned_plane_distance<2>(r, u, coincident, z0_);
1,548,746,969 ✔
319
}
320

321
Direction SurfaceZPlane::normal(Position r) const
46,522,428 ✔
322
{
323
  return {0., 0., 1.};
46,522,428 ✔
324
}
325

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

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

342
//==============================================================================
343
// SurfacePlane implementation
344
//==============================================================================
345

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

351
double SurfacePlane::evaluate(Position r) const
65,802,548 ✔
352
{
353
  return A_ * r.x + B_ * r.y + C_ * r.z - D_;
65,802,548 ✔
354
}
355

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

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

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

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

400
double SurfacePlane::distance(Position r, Direction u, bool coincident) const
149,184,016 ✔
401
{
402
  const double f = A_ * r.x + B_ * r.y + C_ * r.z - D_;
149,184,016 ✔
403
  const double projection = A_ * u.x + B_ * u.y + C_ * u.z;
149,184,016 ✔
404
  if (coincident || std::abs(f) < FP_COINCIDENT || projection == 0.0) {
149,184,016 !
405
    return INFTY;
406
  } else {
407
    const double d = -f / projection;
136,829,712 ✔
408
    if (d < 0.0)
136,829,712 ✔
409
      return INFTY;
410
    return d;
74,551,494 ✔
411
  }
412
}
413

414
Direction SurfacePlane::normal(Position r) const
1,054,178 ✔
415
{
416
  return {A_, B_, C_};
1,054,178 ✔
417
}
418

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

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

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

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

453
  const double r2 = r.get<i2>() - offset1;
951,252,104 ✔
454
  const double r3 = r.get<i3>() - offset2;
951,252,104 ✔
455
  const double k = r2 * u.get<i2>() + r3 * u.get<i3>();
951,252,104 ✔
456
  const double c = r2 * r2 + r3 * r3 - radius * radius;
951,252,104 ✔
457
  const double quad = k * k - a * c;
951,252,104 ✔
458

459
  if (quad < 0.0) {
951,252,104 ✔
460
    // No intersection with cylinder.
461
    return INFTY;
462

463
  } else if (coincident || std::abs(c) < FP_COINCIDENT) {
812,660,428 ✔
464
    // Particle is on the cylinder, thus one distance is positive/negative
465
    // and the other is zero. The sign of k determines if we are facing in or
466
    // out.
467
    if (k >= 0.0) {
348,914,329 ✔
468
      return INFTY;
469
    } else {
470
      return (-k + sqrt(quad)) / a;
199,230,513 ✔
471
    }
472

473
  } else if (c < 0.0) {
463,746,099 ✔
474
    // Particle is inside the cylinder, thus one distance must be negative
475
    // and one must be positive. The positive distance will be the one with
476
    // negative sign on sqrt(quad).
477
    return (-k + sqrt(quad)) / a;
195,966,837 ✔
478

479
  } else {
480
    // Particle is outside the cylinder, thus both distances are either
481
    // positive or negative. If positive, the smaller distance is the one
482
    // with positive sign on sqrt(quad).
483
    const double d = (-k - sqrt(quad)) / a;
267,779,262 ✔
484
    if (d < 0.0)
267,779,262 ✔
485
      return INFTY;
486
    return d;
208,817,107 ✔
487
  }
488
}
489

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

504
//==============================================================================
505
// SurfaceXCylinder implementation
506
//==============================================================================
507

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

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

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

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

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

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

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

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

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

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

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

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

591
//==============================================================================
592
// SurfaceZCylinder implementation
593
//==============================================================================
594

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

601
double SurfaceZCylinder::evaluate(Position r) const
759,239,866 ✔
602
{
603
  return axis_aligned_cylinder_evaluate<0, 1>(r, x0_, y0_, radius_);
759,239,866 ✔
604
}
605

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

613
Direction SurfaceZCylinder::normal(Position r) const
51,049,754 ✔
614
{
615
  return axis_aligned_cylinder_normal<2, 0, 1>(r, x0_, y0_);
51,049,754 ✔
616
}
617

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

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

635
//==============================================================================
636
// SurfaceSphere implementation
637
//==============================================================================
638

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

644
double SurfaceSphere::evaluate(Position r) const
228,493,747 ✔
645
{
646
  const double x = r.x - x0_;
228,493,747 ✔
647
  const double y = r.y - y0_;
228,493,747 ✔
648
  const double z = r.z - z0_;
228,493,747 ✔
649
  return x * x + y * y + z * z - radius_ * radius_;
228,493,747 ✔
650
}
651

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

661
  if (quad < 0.0) {
388,144,573 ✔
662
    // No intersection with sphere.
663
    return INFTY;
664

665
  } else if (coincident || std::abs(c) < FP_COINCIDENT) {
353,913,181 !
666
    // Particle is on the sphere, thus one distance is positive/negative and
667
    // the other is zero. The sign of k determines if we are facing in or out.
668
    if (k >= 0.0) {
21,397,050 ✔
669
      return INFTY;
670
    } else {
671
      return -k + sqrt(quad);
12,199,236 ✔
672
    }
673

674
  } else if (c < 0.0) {
332,516,131 ✔
675
    // Particle is inside the sphere, thus one distance must be negative and
676
    // one must be positive. The positive distance will be the one with
677
    // negative sign on sqrt(quad)
678
    return -k + sqrt(quad);
320,215,795 ✔
679

680
  } else {
681
    // Particle is outside the sphere, thus both distances are either positive
682
    // or negative. If positive, the smaller distance is the one with positive
683
    // sign on sqrt(quad).
684
    const double d = -k - sqrt(quad);
12,300,336 ✔
685
    if (d < 0.0)
12,300,336 ✔
686
      return INFTY;
687
    return d;
9,610,868 ✔
688
  }
689
}
690

691
Direction SurfaceSphere::normal(Position r) const
70,859,420 ✔
692
{
693
  return {2.0 * (r.x - x0_), 2.0 * (r.y - y0_), 2.0 * (r.z - z0_)};
70,859,420 ✔
694
}
695

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

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

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

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

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

747
  double d;
748

749
  if (quad < 0.0) {
177,822 !
750
    // No intersection with cone.
751
    return INFTY;
752

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

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

769
    // Determine the smallest positive solution.
770
    if (d < 0.0) {
167,002 ✔
771
      if (b > 0.0)
141,826 ✔
772
        d = b;
120,928 ✔
773
    } else {
774
      if (b > 0.0) {
25,176 !
775
        if (b < d)
25,176 !
776
          d = b;
25,176 ✔
777
      }
778
    }
779
  }
780

781
  // If the distance was negative, set boundary distance to infinity.
782
  if (d <= 0.0)
177,822 ✔
783
    return INFTY;
24,804 ✔
784
  return d;
785
}
786

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

801
//==============================================================================
802
// SurfaceXCone implementation
803
//==============================================================================
804

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

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

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

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

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

833
//==============================================================================
834
// SurfaceYCone implementation
835
//==============================================================================
836

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

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

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

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

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

865
//==============================================================================
866
// SurfaceZCone implementation
867
//==============================================================================
868

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

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

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

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

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

897
//==============================================================================
898
// SurfaceQuadric implementation
899
//==============================================================================
900

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

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

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

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

935
  double d;
57,580 ✔
936

937
  if (quad < 0.0) {
57,580 !
938
    // No intersection with surface.
939
    return INFTY;
940

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

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

969
    // Determine the smallest positive solution.
970
    if (d < 0.0) {
51,024 ✔
971
      if (b > 0.0)
51,022 !
972
        d = b;
51,022 ✔
973
    } else {
974
      if (b > 0.0) {
2 !
975
        if (b < d)
2 !
976
          d = b;
×
977
      }
978
    }
979
  }
980

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

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

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

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

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

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

1030
  std::complex<double> roots[4];
4,824,466 ✔
1031
  oqs::quartic_solver(coeff, roots);
4,824,466 ✔
1032

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

1057
//==============================================================================
1058
// SurfaceXTorus implementation
1059
//==============================================================================
1060

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

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

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

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

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

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

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

1121
//==============================================================================
1122
// SurfaceYTorus implementation
1123
//==============================================================================
1124

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

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

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

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

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

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

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

1182
//==============================================================================
1183
// SurfaceZTorus implementation
1184
//==============================================================================
1185

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

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

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

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

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

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

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

1243
//==============================================================================
1244

1245
void read_surfaces(pugi::xml_node node,
1,558 ✔
1246
  std::set<std::pair<int, int>>& periodic_pairs,
1247
  std::unordered_map<int, double>& albedo_map,
1248
  std::unordered_map<int, int>& periodic_sense_map)
1249
{
1250
  // Count the number of surfaces
1251
  auto surf_nodes = node.children("surface");
1,558 ✔
1252
  int n_surfaces = std::distance(surf_nodes.begin(), surf_nodes.end());
1,558 ✔
1253

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

1264
      // Allocate and initialize the new surface
1265

1266
      if (surf_type == "x-plane") {
7,852 ✔
1267
        model::surfaces.push_back(make_unique<SurfaceXPlane>(surf_node));
1,994 ✔
1268

1269
      } else if (surf_type == "y-plane") {
5,858 ✔
1270
        model::surfaces.push_back(make_unique<SurfaceYPlane>(surf_node));
1,808 ✔
1271

1272
      } else if (surf_type == "z-plane") {
4,050 ✔
1273
        model::surfaces.push_back(make_unique<SurfaceZPlane>(surf_node));
1,336 ✔
1274

1275
      } else if (surf_type == "plane") {
2,714 ✔
1276
        model::surfaces.push_back(make_unique<SurfacePlane>(surf_node));
298 ✔
1277

1278
      } else if (surf_type == "x-cylinder") {
2,416 ✔
1279
        model::surfaces.push_back(make_unique<SurfaceXCylinder>(surf_node));
6 ✔
1280

1281
      } else if (surf_type == "y-cylinder") {
2,410 ✔
1282
        model::surfaces.push_back(make_unique<SurfaceYCylinder>(surf_node));
4 ✔
1283

1284
      } else if (surf_type == "z-cylinder") {
2,406 ✔
1285
        model::surfaces.push_back(make_unique<SurfaceZCylinder>(surf_node));
900 ✔
1286

1287
      } else if (surf_type == "sphere") {
1,506 ✔
1288
        model::surfaces.push_back(make_unique<SurfaceSphere>(surf_node));
1,466 ✔
1289

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

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

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

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

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

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

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

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

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

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

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

1363
    for (auto s : cell->surfaces()) {
214 ✔
1364
      auto surf_idx = std::abs(s) - 1;
172 ✔
1365
      auto id = model::surfaces[surf_idx]->id_;
172 ✔
1366

1367
      if (periodic_sense_map.count(id)) {
172 ✔
1368
        periodic_sense_map[id] = std::copysign(1, s);
84 ✔
1369
        --n_periodic;
84 ✔
1370
      }
1371
    }
42 ✔
1372
  }
1373

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

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

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

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

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

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

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

1486
void free_memory_surfaces()
1,578 ✔
1487
{
1488
  model::surfaces.clear();
1,578 ✔
1489
  model::surface_map.clear();
1,578 ✔
1490
}
1,578 ✔
1491

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