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

openmc-dev / openmc / 22073868368

16 Feb 2026 06:32PM UTC coverage: 81.546% (-0.2%) from 81.721%
22073868368

Pull #3810

github

web-flow
Merge fe77e3fdb into c6ef84d1d
Pull Request #3810: Implementation of migration area score

17050 of 23727 branches covered (71.86%)

Branch coverage included in aggregate %.

36 of 55 new or added lines in 5 files covered. (65.45%)

300 existing lines in 24 files now uncovered.

56162 of 66053 relevant lines covered (85.03%)

43232737.44 hits per line

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

79.47
/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/simulation.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(
34,867✔
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");
34,867✔
44
  if (coeffs_file.size() != coeffs.size()) {
34,867!
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;
34,867✔
52
  for (auto c : coeffs) {
104,141✔
53
    *c = coeffs_file[i++];
69,274✔
54
  }
55
}
34,867✔
56

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

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

63
Surface::Surface(pugi::xml_node surf_node)
34,867✔
64
{
65
  if (check_for_node(surf_node, "id")) {
34,867!
66
    id_ = std::stoi(get_node_value(surf_node, "id"));
34,867✔
67
    if (contains(settings::source_write_surf_id, id_) ||
69,110✔
68
        settings::source_write_surf_id.empty()) {
34,243✔
69
      surf_source_ = true;
34,039✔
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")) {
34,867✔
76
    name_ = get_node_value(surf_node, "name", false);
8,589✔
77
  }
78

79
  if (check_for_node(surf_node, "boundary")) {
34,867✔
80
    std::string surf_bc = get_node_value(surf_node, "boundary", true, true);
20,452✔
81

82
    if (surf_bc == "transmission" || surf_bc == "transmit" || surf_bc.empty()) {
20,452!
83
      // Leave the bc_ a nullptr
84
    } else if (surf_bc == "vacuum") {
20,452✔
85
      bc_ = make_unique<VacuumBC>();
9,789✔
86
    } else if (surf_bc == "reflective" || surf_bc == "reflect" ||
11,175!
87
               surf_bc == "reflecting") {
512✔
88
      bc_ = make_unique<ReflectiveBC>();
10,151✔
89
    } else if (surf_bc == "white") {
512✔
90
      bc_ = make_unique<WhiteBC>();
72✔
91
    } else if (surf_bc == "periodic") {
440!
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_) {
20,452!
100
      double surf_alb = std::stod(get_node_value(surf_node, "albedo"));
72✔
101

102
      if (surf_alb < 0.0)
72!
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)
72!
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);
72✔
114
    }
115
  }
20,452✔
116
}
34,867✔
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;
8,642,215✔
130
  }
131
  return f > 0.0;
2,147,483,647✔
132
}
133

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

140
  // Reflect direction according to normal.
141
  return u.reflect(n);
1,116,752,026✔
142
}
143

144
Direction Surface::diffuse_reflect(
824,031✔
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);
824,031✔
151
  n /= n.norm();
824,031✔
152
  const double projection = n.dot(u);
824,031✔
153

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

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

161
  // normalize the direction
162
  return u / u.norm();
1,648,062✔
163
}
164

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

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

174
    if (bc_) {
28,361✔
175
      write_string(surf_group, "boundary_type", bc_->type(), false);
17,188✔
176
      bc_->to_hdf5(surf_group);
17,188✔
177

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

184
        if (id_ == surf1.id_) {
374!
185
          write_dataset(surf_group, "periodic_surface_id", surf2.id_);
374✔
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);
11,173✔
192
    }
193
  }
194

195
  if (!name_.empty()) {
28,361✔
196
    write_string(surf_group, "name", name_, false);
7,074✔
197
  }
198

199
  to_hdf5_inner(surf_group);
28,361✔
200

201
  close_group(surf_group);
28,361✔
202
}
28,361✔
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;
573,695,733✔
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;
2,147,483,647✔
220
}
221

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

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

231
double SurfaceXPlane::evaluate(Position r) const
1,394,919,358✔
232
{
233
  return r.x - x0_;
1,394,919,358✔
234
}
235

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

241
Direction SurfaceXPlane::normal(Position r) const
246,008,677✔
242
{
243
  return {1., 0., 0.};
246,008,677✔
244
}
245

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

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

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

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

271
double SurfaceYPlane::evaluate(Position r) const
1,385,787,028✔
272
{
273
  return r.y - y0_;
1,385,787,028✔
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
262,645,938✔
282
{
283
  return {0., 1., 0.};
262,645,938✔
284
}
285

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

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

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

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

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

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

321
Direction SurfaceZPlane::normal(Position r) const
45,418,241✔
322
{
323
  return {0., 0., 1.};
45,418,241✔
324
}
325

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

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

356
double SurfacePlane::distance(Position r, Direction u, bool coincident) const
658,004,502✔
357
{
358
  const double f = A_ * r.x + B_ * r.y + C_ * r.z - D_;
658,004,502✔
359
  const double projection = A_ * u.x + B_ * u.y + C_ * u.z;
658,004,502✔
360
  if (coincident || std::abs(f) < FP_COINCIDENT || projection == 0.0) {
658,004,502!
361
    return INFTY;
53,776,256✔
362
  } else {
363
    const double d = -f / projection;
604,228,246✔
364
    if (d < 0.0)
604,228,246✔
365
      return INFTY;
275,401,602✔
366
    return d;
328,826,644✔
367
  }
368
}
369

370
Direction SurfacePlane::normal(Position r) const
4,678,108✔
371
{
372
  return {A_, B_, C_};
4,678,108✔
373
}
374

375
void SurfacePlane::to_hdf5_inner(hid_t group_id) const
1,212✔
376
{
377
  write_string(group_id, "type", "plane", false);
1,212✔
378
  array<double, 4> coeffs {{A_, B_, C_, D_}};
1,212✔
379
  write_dataset(group_id, "coefficients", coeffs);
1,212✔
380
}
1,212✔
381

382
//==============================================================================
383
// Generic functions for x-, y-, and z-, cylinders
384
//==============================================================================
385

386
// The template parameters indicate the axes perpendicular to the axis of the
387
// cylinder.  offset1 and offset2 should correspond with i1 and i2,
388
// respectively.
389
template<int i1, int i2>
390
double axis_aligned_cylinder_evaluate(
1,528,095,863✔
391
  Position r, double offset1, double offset2, double radius)
392
{
393
  const double r1 = r.get<i1>() - offset1;
1,528,095,863✔
394
  const double r2 = r.get<i2>() - offset2;
1,528,095,863✔
395
  return r1 * r1 + r2 * r2 - radius * radius;
1,528,095,863✔
396
}
397

398
// The first template parameter indicates which axis the cylinder is aligned to.
399
// The other two parameters indicate the other two axes.  offset1 and offset2
400
// should correspond with i2 and i3, respectively.
401
template<int i1, int i2, int i3>
402
double axis_aligned_cylinder_distance(Position r, Direction u, bool coincident,
1,692,123,539✔
403
  double offset1, double offset2, double radius)
404
{
405
  const double a = 1.0 - u.get<i1>() * u.get<i1>(); // u^2 + v^2
1,692,123,539✔
406
  if (a == 0.0)
1,692,123,539✔
407
    return INFTY;
662,724✔
408

409
  const double r2 = r.get<i2>() - offset1;
1,691,460,815✔
410
  const double r3 = r.get<i3>() - offset2;
1,691,460,815✔
411
  const double k = r2 * u.get<i2>() + r3 * u.get<i3>();
1,691,460,815✔
412
  const double c = r2 * r2 + r3 * r3 - radius * radius;
1,691,460,815✔
413
  const double quad = k * k - a * c;
1,691,460,815✔
414

415
  if (quad < 0.0) {
1,691,460,815✔
416
    // No intersection with cylinder.
417
    return INFTY;
266,807,911✔
418

419
  } else if (coincident || std::abs(c) < FP_COINCIDENT) {
1,424,652,904✔
420
    // Particle is on the cylinder, thus one distance is positive/negative
421
    // and the other is zero. The sign of k determines if we are facing in or
422
    // out.
423
    if (k >= 0.0) {
656,809,916✔
424
      return INFTY;
329,374,599✔
425
    } else {
426
      return (-k + sqrt(quad)) / a;
327,435,317✔
427
    }
428

429
  } else if (c < 0.0) {
767,842,988✔
430
    // Particle is inside the cylinder, thus one distance must be negative
431
    // and one must be positive. The positive distance will be the one with
432
    // negative sign on sqrt(quad).
433
    return (-k + sqrt(quad)) / a;
314,310,196✔
434

435
  } else {
436
    // Particle is outside the cylinder, thus both distances are either
437
    // positive or negative. If positive, the smaller distance is the one
438
    // with positive sign on sqrt(quad).
439
    const double d = (-k - sqrt(quad)) / a;
453,532,792✔
440
    if (d < 0.0)
453,532,792✔
441
      return INFTY;
60,477,133✔
442
    return d;
393,055,659✔
443
  }
444
}
445

446
// The first template parameter indicates which axis the cylinder is aligned to.
447
// The other two parameters indicate the other two axes.  offset1 and offset2
448
// should correspond with i2 and i3, respectively.
449
template<int i1, int i2, int i3>
450
Direction axis_aligned_cylinder_normal(
1,236,763✔
451
  Position r, double offset1, double offset2)
452
{
453
  Direction u;
1,236,763✔
454
  u.get<i2>() = 2.0 * (r.get<i2>() - offset1);
1,236,763✔
455
  u.get<i3>() = 2.0 * (r.get<i3>() - offset2);
1,236,763✔
456
  u.get<i1>() = 0.0;
1,236,763✔
457
  return u;
1,236,763✔
458
}
459

460
//==============================================================================
461
// SurfaceXCylinder implementation
462
//==============================================================================
463

464
SurfaceXCylinder::SurfaceXCylinder(pugi::xml_node surf_node)
36✔
465
  : Surface(surf_node)
36✔
466
{
467
  read_coeffs(surf_node, id_, {&y0_, &z0_, &radius_});
36✔
468
}
36✔
469

470
double SurfaceXCylinder::evaluate(Position r) const
1,248,653✔
471
{
472
  return axis_aligned_cylinder_evaluate<1, 2>(r, y0_, z0_, radius_);
1,248,653✔
473
}
474

475
double SurfaceXCylinder::distance(
1,429,308✔
476
  Position r, Direction u, bool coincident) const
477
{
478
  return axis_aligned_cylinder_distance<0, 1, 2>(
1,429,308✔
479
    r, u, coincident, y0_, z0_, radius_);
1,429,308✔
480
}
481

482
Direction SurfaceXCylinder::normal(Position r) const
×
483
{
484
  return axis_aligned_cylinder_normal<0, 1, 2>(r, y0_, z0_);
×
485
}
486

487
void SurfaceXCylinder::to_hdf5_inner(hid_t group_id) const
27✔
488
{
489
  write_string(group_id, "type", "x-cylinder", false);
27✔
490
  array<double, 3> coeffs {{y0_, z0_, radius_}};
27✔
491
  write_dataset(group_id, "coefficients", coeffs);
27✔
492
}
27✔
493

494
BoundingBox SurfaceXCylinder::bounding_box(bool pos_side) const
×
495
{
496
  if (!pos_side) {
×
497
    return {{-INFTY, y0_ - radius_, z0_ - radius_},
×
498
      {INFTY, y0_ + radius_, z0_ + radius_}};
×
499
  } else {
500
    return {};
×
501
  }
502
}
503
//==============================================================================
504
// SurfaceYCylinder implementation
505
//==============================================================================
506

507
SurfaceYCylinder::SurfaceYCylinder(pugi::xml_node surf_node)
12✔
508
  : Surface(surf_node)
12✔
509
{
510
  read_coeffs(surf_node, id_, {&x0_, &z0_, &radius_});
12✔
511
}
12✔
512

513
double SurfaceYCylinder::evaluate(Position r) const
139,896✔
514
{
515
  return axis_aligned_cylinder_evaluate<0, 2>(r, x0_, z0_, radius_);
139,896✔
516
}
517

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

525
Direction SurfaceYCylinder::normal(Position r) const
×
526
{
527
  return axis_aligned_cylinder_normal<1, 0, 2>(r, x0_, z0_);
×
528
}
529

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

537
BoundingBox SurfaceYCylinder::bounding_box(bool pos_side) const
×
538
{
539
  if (!pos_side) {
×
540
    return {{x0_ - radius_, -INFTY, z0_ - radius_},
×
541
      {x0_ + radius_, INFTY, z0_ + radius_}};
×
542
  } else {
543
    return {};
×
544
  }
545
}
546

547
//==============================================================================
548
// SurfaceZCylinder implementation
549
//==============================================================================
550

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

557
double SurfaceZCylinder::evaluate(Position r) const
1,526,707,314✔
558
{
559
  return axis_aligned_cylinder_evaluate<0, 1>(r, x0_, y0_, radius_);
1,526,707,314✔
560
}
561

562
double SurfaceZCylinder::distance(
1,690,154,420✔
563
  Position r, Direction u, bool coincident) const
564
{
565
  return axis_aligned_cylinder_distance<2, 0, 1>(
1,690,154,420✔
566
    r, u, coincident, x0_, y0_, radius_);
1,690,154,420✔
567
}
568

569
Direction SurfaceZCylinder::normal(Position r) const
1,236,763✔
570
{
571
  return axis_aligned_cylinder_normal<2, 0, 1>(r, x0_, y0_);
1,236,763✔
572
}
573

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

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

591
//==============================================================================
592
// SurfaceSphere implementation
593
//==============================================================================
594

595
SurfaceSphere::SurfaceSphere(pugi::xml_node surf_node) : Surface(surf_node)
6,711✔
596
{
597
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &radius_});
6,711✔
598
}
6,711✔
599

600
double SurfaceSphere::evaluate(Position r) const
547,967,991✔
601
{
602
  const double x = r.x - x0_;
547,967,991✔
603
  const double y = r.y - y0_;
547,967,991✔
604
  const double z = r.z - z0_;
547,967,991✔
605
  return x * x + y * y + z * z - radius_ * radius_;
547,967,991✔
606
}
607

608
double SurfaceSphere::distance(Position r, Direction u, bool coincident) const
922,779,050✔
609
{
610
  const double x = r.x - x0_;
922,779,050✔
611
  const double y = r.y - y0_;
922,779,050✔
612
  const double z = r.z - z0_;
922,779,050✔
613
  const double k = x * u.x + y * u.y + z * u.z;
922,779,050✔
614
  const double c = x * x + y * y + z * z - radius_ * radius_;
922,779,050✔
615
  const double quad = k * k - c;
922,779,050✔
616

617
  if (quad < 0.0) {
922,779,050✔
618
    // No intersection with sphere.
619
    return INFTY;
135,058,247✔
620

621
  } else if (coincident || std::abs(c) < FP_COINCIDENT) {
787,720,803!
622
    // Particle is on the sphere, thus one distance is positive/negative and
623
    // the other is zero. The sign of k determines if we are facing in or out.
624
    if (k >= 0.0) {
85,165,379✔
625
      return INFTY;
36,352,929✔
626
    } else {
627
      return -k + sqrt(quad);
48,812,450✔
628
    }
629

630
  } else if (c < 0.0) {
702,555,424✔
631
    // Particle is inside the sphere, thus one distance must be negative and
632
    // one must be positive. The positive distance will be the one with
633
    // negative sign on sqrt(quad)
634
    return -k + sqrt(quad);
656,340,471✔
635

636
  } else {
637
    // Particle is outside the sphere, thus both distances are either positive
638
    // or negative. If positive, the smaller distance is the one with positive
639
    // sign on sqrt(quad).
640
    const double d = -k - sqrt(quad);
46,214,953✔
641
    if (d < 0.0)
46,214,953✔
642
      return INFTY;
8,915,688✔
643
    return d;
37,299,265✔
644
  }
645
}
646

647
Direction SurfaceSphere::normal(Position r) const
17,569,107✔
648
{
649
  return {2.0 * (r.x - x0_), 2.0 * (r.y - y0_), 2.0 * (r.z - z0_)};
17,569,107✔
650
}
651

652
void SurfaceSphere::to_hdf5_inner(hid_t group_id) const
5,617✔
653
{
654
  write_string(group_id, "type", "sphere", false);
5,617✔
655
  array<double, 4> coeffs {{x0_, y0_, z0_, radius_}};
5,617✔
656
  write_dataset(group_id, "coefficients", coeffs);
5,617✔
657
}
5,617✔
658

659
BoundingBox SurfaceSphere::bounding_box(bool pos_side) const
×
660
{
661
  if (!pos_side) {
×
662
    return {{x0_ - radius_, y0_ - radius_, z0_ - radius_},
×
663
      {x0_ + radius_, y0_ + radius_, z0_ + radius_}};
×
664
  } else {
665
    return {};
×
666
  }
667
}
668

669
//==============================================================================
670
// Generic functions for x-, y-, and z-, cones
671
//==============================================================================
672

673
// The first template parameter indicates which axis the cone is aligned to.
674
// The other two parameters indicate the other two axes.  offset1, offset2,
675
// and offset3 should correspond with i1, i2, and i3, respectively.
676
template<int i1, int i2, int i3>
677
double axis_aligned_cone_evaluate(
266,607✔
678
  Position r, double offset1, double offset2, double offset3, double radius_sq)
679
{
680
  const double r1 = r.get<i1>() - offset1;
266,607✔
681
  const double r2 = r.get<i2>() - offset2;
266,607✔
682
  const double r3 = r.get<i3>() - offset3;
266,607✔
683
  return r2 * r2 + r3 * r3 - radius_sq * r1 * r1;
266,607✔
684
}
685

686
// The first template parameter indicates which axis the cone is aligned to.
687
// The other two parameters indicate the other two axes.  offset1, offset2,
688
// and offset3 should correspond with i1, i2, and i3, respectively.
689
template<int i1, int i2, int i3>
690
double axis_aligned_cone_distance(Position r, Direction u, bool coincident,
800,199✔
691
  double offset1, double offset2, double offset3, double radius_sq)
692
{
693
  const double r1 = r.get<i1>() - offset1;
800,199✔
694
  const double r2 = r.get<i2>() - offset2;
800,199✔
695
  const double r3 = r.get<i3>() - offset3;
800,199✔
696
  const double a = u.get<i2>() * u.get<i2>() + u.get<i3>() * u.get<i3>() -
800,199✔
697
                   radius_sq * u.get<i1>() * u.get<i1>();
800,199✔
698
  const double k =
800,199✔
699
    r2 * u.get<i2>() + r3 * u.get<i3>() - radius_sq * r1 * u.get<i1>();
800,199✔
700
  const double c = r2 * r2 + r3 * r3 - radius_sq * r1 * r1;
800,199✔
701
  double quad = k * k - a * c;
800,199✔
702

703
  double d;
704

705
  if (quad < 0.0) {
800,199!
706
    // No intersection with cone.
707
    return INFTY;
×
708

709
  } else if (coincident || std::abs(c) < FP_COINCIDENT) {
800,199!
710
    // Particle is on the cone, thus one distance is positive/negative
711
    // and the other is zero. The sign of k determines if we are facing in or
712
    // out.
713
    if (k >= 0.0) {
48,690!
714
      d = (-k - sqrt(quad)) / a;
×
715
    } else {
716
      d = (-k + sqrt(quad)) / a;
48,690✔
717
    }
718

719
  } else {
720
    // Calculate both solutions to the quadratic.
721
    quad = sqrt(quad);
751,509✔
722
    d = (-k - quad) / a;
751,509✔
723
    const double b = (-k + quad) / a;
751,509✔
724

725
    // Determine the smallest positive solution.
726
    if (d < 0.0) {
751,509✔
727
      if (b > 0.0)
638,217✔
728
        d = b;
544,176✔
729
    } else {
730
      if (b > 0.0) {
113,292!
731
        if (b < d)
113,292!
732
          d = b;
113,292✔
733
      }
734
    }
735
  }
736

737
  // If the distance was negative, set boundary distance to infinity.
738
  if (d <= 0.0)
800,199✔
739
    return INFTY;
111,618✔
740
  return d;
688,581✔
741
}
742

743
// The first template parameter indicates which axis the cone is aligned to.
744
// The other two parameters indicate the other two axes.  offset1, offset2,
745
// and offset3 should correspond with i1, i2, and i3, respectively.
746
template<int i1, int i2, int i3>
747
Direction axis_aligned_cone_normal(
48,690✔
748
  Position r, double offset1, double offset2, double offset3, double radius_sq)
749
{
750
  Direction u;
48,690✔
751
  u.get<i1>() = -2.0 * radius_sq * (r.get<i1>() - offset1);
48,690✔
752
  u.get<i2>() = 2.0 * (r.get<i2>() - offset2);
48,690✔
753
  u.get<i3>() = 2.0 * (r.get<i3>() - offset3);
48,690✔
754
  return u;
48,690✔
755
}
756

757
//==============================================================================
758
// SurfaceXCone implementation
759
//==============================================================================
760

761
SurfaceXCone::SurfaceXCone(pugi::xml_node surf_node) : Surface(surf_node)
×
762
{
763
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &radius_sq_});
×
764
}
×
765

766
double SurfaceXCone::evaluate(Position r) const
×
767
{
768
  return axis_aligned_cone_evaluate<0, 1, 2>(r, x0_, y0_, z0_, radius_sq_);
×
769
}
770

771
double SurfaceXCone::distance(Position r, Direction u, bool coincident) const
×
772
{
773
  return axis_aligned_cone_distance<0, 1, 2>(
×
774
    r, u, coincident, x0_, y0_, z0_, radius_sq_);
×
775
}
776

777
Direction SurfaceXCone::normal(Position r) const
×
778
{
779
  return axis_aligned_cone_normal<0, 1, 2>(r, x0_, y0_, z0_, radius_sq_);
×
780
}
781

782
void SurfaceXCone::to_hdf5_inner(hid_t group_id) const
×
783
{
784
  write_string(group_id, "type", "x-cone", false);
×
785
  array<double, 4> coeffs {{x0_, y0_, z0_, radius_sq_}};
×
786
  write_dataset(group_id, "coefficients", coeffs);
×
787
}
×
788

789
//==============================================================================
790
// SurfaceYCone implementation
791
//==============================================================================
792

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

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

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

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

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

821
//==============================================================================
822
// SurfaceZCone implementation
823
//==============================================================================
824

825
SurfaceZCone::SurfaceZCone(pugi::xml_node surf_node) : Surface(surf_node)
12✔
826
{
827
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &radius_sq_});
12✔
828
}
12✔
829

830
double SurfaceZCone::evaluate(Position r) const
266,607✔
831
{
832
  return axis_aligned_cone_evaluate<2, 0, 1>(r, z0_, x0_, y0_, radius_sq_);
266,607✔
833
}
834

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

841
Direction SurfaceZCone::normal(Position r) const
48,690✔
842
{
843
  return axis_aligned_cone_normal<2, 0, 1>(r, z0_, x0_, y0_, radius_sq_);
48,690✔
844
}
845

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

853
//==============================================================================
854
// SurfaceQuadric implementation
855
//==============================================================================
856

857
SurfaceQuadric::SurfaceQuadric(pugi::xml_node surf_node) : Surface(surf_node)
12✔
858
{
859
  read_coeffs(
×
860
    surf_node, id_, {&A_, &B_, &C_, &D_, &E_, &F_, &G_, &H_, &J_, &K_});
12✔
861
}
12✔
862

863
double SurfaceQuadric::evaluate(Position r) const
147,138✔
864
{
865
  const double x = r.x;
147,138✔
866
  const double y = r.y;
147,138✔
867
  const double z = r.z;
147,138✔
868
  return x * (A_ * x + D_ * y + G_) + y * (B_ * y + E_ * z + H_) +
147,138✔
869
         z * (C_ * z + F_ * x + J_) + K_;
147,138✔
870
}
871

872
double SurfaceQuadric::distance(
259,101✔
873
  Position r, Direction ang, bool coincident) const
874
{
875
  const double& x = r.x;
259,101✔
876
  const double& y = r.y;
259,101✔
877
  const double& z = r.z;
259,101✔
878
  const double& u = ang.x;
259,101✔
879
  const double& v = ang.y;
259,101✔
880
  const double& w = ang.z;
259,101✔
881

882
  const double a =
259,101✔
883
    A_ * u * u + B_ * v * v + C_ * w * w + D_ * u * v + E_ * v * w + F_ * u * w;
259,101✔
884
  const double k = A_ * u * x + B_ * v * y + C_ * w * z +
259,101✔
885
                   0.5 * (D_ * (u * y + v * x) + E_ * (v * z + w * y) +
259,101✔
886
                           F_ * (w * x + u * z) + G_ * u + H_ * v + J_ * w);
259,101✔
887
  const double c = A_ * x * x + B_ * y * y + C_ * z * z + D_ * x * y +
259,101✔
888
                   E_ * y * z + F_ * x * z + G_ * x + H_ * y + J_ * z + K_;
259,101✔
889
  double quad = k * k - a * c;
259,101✔
890

891
  double d;
892

893
  if (quad < 0.0) {
259,101!
894
    // No intersection with surface.
895
    return INFTY;
×
896

897
  } else if (coincident || std::abs(c) < FP_COINCIDENT) {
259,101!
898
    // Particle is on the surface, thus one distance is positive/negative and
899
    // the other is zero. The sign of k determines which distance is zero and
900
    // which is not. Additionally, if a is zero, it means the particle is on
901
    // a plane-like surface.
902
    if (a == 0.0) {
29,502!
903
      d = INFTY; // see the below explanation
×
904
    } else if (k >= 0.0) {
29,502!
905
      d = (-k - sqrt(quad)) / a;
×
906
    } else {
907
      d = (-k + sqrt(quad)) / a;
29,502✔
908
    }
909

910
  } else if (a == 0.0) {
229,599!
911
    // Given the orientation of the particle, the quadric looks like a plane in
912
    // this case, and thus we have only one solution despite potentially having
913
    // quad > 0.0. While the term under the square root may be real, in one
914
    // case of the +/- of the quadratic formula, 0/0 results, and in another, a
915
    // finite value over 0 results. Applying L'Hopital's to the 0/0 case gives
916
    // the below. Alternatively this can be found by simply putting a=0 in the
917
    // equation ax^2 + bx + c = 0.
918
    d = -0.5 * c / k;
×
919
  } else {
920
    // Calculate both solutions to the quadratic.
921
    quad = sqrt(quad);
229,599✔
922
    d = (-k - quad) / a;
229,599✔
923
    double b = (-k + quad) / a;
229,599✔
924

925
    // Determine the smallest positive solution.
926
    if (d < 0.0) {
229,599!
927
      if (b > 0.0)
229,599!
928
        d = b;
229,599✔
929
    } else {
930
      if (b > 0.0) {
×
931
        if (b < d)
×
932
          d = b;
×
933
      }
934
    }
935
  }
936

937
  // If the distance was negative, set boundary distance to infinity.
938
  if (d <= 0.0)
259,101!
939
    return INFTY;
×
940
  return d;
259,101✔
941
}
942

943
Direction SurfaceQuadric::normal(Position r) const
29,502✔
944
{
945
  const double& x = r.x;
29,502✔
946
  const double& y = r.y;
29,502✔
947
  const double& z = r.z;
29,502✔
948
  return {2.0 * A_ * x + D_ * y + F_ * z + G_,
29,502✔
949
    2.0 * B_ * y + D_ * x + E_ * z + H_, 2.0 * C_ * z + E_ * y + F_ * x + J_};
29,502✔
950
}
951

952
void SurfaceQuadric::to_hdf5_inner(hid_t group_id) const
9✔
953
{
954
  write_string(group_id, "type", "quadric", false);
9✔
955
  array<double, 10> coeffs {{A_, B_, C_, D_, E_, F_, G_, H_, J_, K_}};
9✔
956
  write_dataset(group_id, "coefficients", coeffs);
9✔
957
}
9✔
958

959
//==============================================================================
960
// Torus helper functions
961
//==============================================================================
962

963
double torus_distance(double x1, double x2, double x3, double u1, double u2,
21,710,097✔
964
  double u3, double A, double B, double C, bool coincident)
965
{
966
  // Coefficients for equation: (c2 t^2 + c1 t + c0)^2 = c2' t^2 + c1' t + c0'
967
  double D = (C * C) / (B * B);
21,710,097✔
968
  double c2 = u1 * u1 + u2 * u2 + D * u3 * u3;
21,710,097✔
969
  double c1 = 2 * (u1 * x1 + u2 * x2 + D * u3 * x3);
21,710,097✔
970
  double c0 = x1 * x1 + x2 * x2 + D * x3 * x3 + A * A - C * C;
21,710,097✔
971
  double four_A2 = 4 * A * A;
21,710,097✔
972
  double c2p = four_A2 * (u1 * u1 + u2 * u2);
21,710,097✔
973
  double c1p = 2 * four_A2 * (u1 * x1 + u2 * x2);
21,710,097✔
974
  double c0p = four_A2 * (x1 * x1 + x2 * x2);
21,710,097✔
975

976
  // Coefficient for equation: a t^4 + b t^3 + c t^2 + d t + e = 0. If the point
977
  // is coincident, the 'e' coefficient should be zero. Explicitly setting it to
978
  // zero helps avoid numerical issues below with root finding.
979
  double coeff[5];
980
  coeff[0] = coincident ? 0.0 : c0 * c0 - c0p;
21,710,097✔
981
  coeff[1] = 2 * c0 * c1 - c1p;
21,710,097✔
982
  coeff[2] = c1 * c1 + 2 * c0 * c2 - c2p;
21,710,097✔
983
  coeff[3] = 2 * c1 * c2;
21,710,097✔
984
  coeff[4] = c2 * c2;
21,710,097✔
985

986
  std::complex<double> roots[4];
21,710,097✔
987
  oqs::quartic_solver(coeff, roots);
21,710,097✔
988

989
  // Find smallest positive, real root. In the case where the particle is
990
  // coincident with the surface, we are sure to have one root very close to
991
  // zero but possibly small and positive. A tolerance is set to discard that
992
  // zero.
993
  double distance = INFTY;
21,710,097✔
994
  double cutoff = coincident ? TORUS_TOL : 0.0;
21,710,097✔
995
  for (int i = 0; i < 4; ++i) {
108,550,485✔
996
    if (roots[i].imag() == 0) {
86,840,388✔
997
      double root = roots[i].real();
21,226,932✔
998
      if (root > cutoff && root < distance) {
21,226,932✔
999
        // Avoid roots corresponding to internal surfaces
1000
        double s1 = x1 + u1 * root;
8,518,905✔
1001
        double s2 = x2 + u2 * root;
8,518,905✔
1002
        double s3 = x3 + u3 * root;
8,518,905✔
1003
        double check = D * s3 * s3 + s1 * s1 + s2 * s2 + A * A - C * C;
8,518,905✔
1004
        if (check >= 0) {
8,518,905!
1005
          distance = root;
8,518,905✔
1006
        }
1007
      }
1008
    }
1009
  }
1010
  return distance;
21,710,097✔
1011
}
1012

1013
//==============================================================================
1014
// SurfaceXTorus implementation
1015
//==============================================================================
1016

1017
SurfaceXTorus::SurfaceXTorus(pugi::xml_node surf_node) : Surface(surf_node)
48✔
1018
{
1019
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &A_, &B_, &C_});
48✔
1020
}
48✔
1021

1022
void SurfaceXTorus::to_hdf5_inner(hid_t group_id) const
45✔
1023
{
1024
  write_string(group_id, "type", "x-torus", false);
45✔
1025
  std::array<double, 6> coeffs {{x0_, y0_, z0_, A_, B_, C_}};
45✔
1026
  write_dataset(group_id, "coefficients", coeffs);
45✔
1027
}
45✔
1028

1029
double SurfaceXTorus::evaluate(Position r) const
663,766✔
1030
{
1031
  double x = r.x - x0_;
663,766✔
1032
  double y = r.y - y0_;
663,766✔
1033
  double z = r.z - z0_;
663,766✔
1034
  return (x * x) / (B_ * B_) +
1,327,532✔
1035
         std::pow(std::sqrt(y * y + z * z) - A_, 2) / (C_ * C_) - 1.;
663,766✔
1036
}
1037

1038
double SurfaceXTorus::distance(Position r, Direction u, bool coincident) const
6,830,685✔
1039
{
1040
  double x = r.x - x0_;
6,830,685✔
1041
  double y = r.y - y0_;
6,830,685✔
1042
  double z = r.z - z0_;
6,830,685✔
1043
  return torus_distance(y, z, x, u.y, u.z, u.x, A_, B_, C_, coincident);
6,830,685✔
1044
}
1045

1046
Direction SurfaceXTorus::normal(Position r) const
×
1047
{
1048
  // reduce the expansion of the full form for torus
1049
  double x = r.x - x0_;
×
1050
  double y = r.y - y0_;
×
1051
  double z = r.z - z0_;
×
1052

1053
  // f(x,y,z) = x^2/B^2 + (sqrt(y^2 + z^2) - A)^2/C^2 - 1
1054
  // ∂f/∂x = 2x/B^2
1055
  // ∂f/∂y = 2y(g - A)/(g*C^2) where g = sqrt(y^2 + z^2)
1056
  // ∂f/∂z = 2z(g - A)/(g*C^2)
1057
  // Multiplying by g*C^2*B^2 / 2 gives:
1058
  double g = std::sqrt(y * y + z * z);
×
1059
  double nx = C_ * C_ * g * x;
×
1060
  double ny = y * (g - A_) * B_ * B_;
×
1061
  double nz = z * (g - A_) * B_ * B_;
×
1062
  Direction n(nx, ny, nz);
×
1063
  return n / n.norm();
×
1064
}
1065

1066
//==============================================================================
1067
// SurfaceYTorus implementation
1068
//==============================================================================
1069

1070
SurfaceYTorus::SurfaceYTorus(pugi::xml_node surf_node) : Surface(surf_node)
48✔
1071
{
1072
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &A_, &B_, &C_});
48✔
1073
}
48✔
1074

1075
void SurfaceYTorus::to_hdf5_inner(hid_t group_id) const
45✔
1076
{
1077
  write_string(group_id, "type", "y-torus", false);
45✔
1078
  std::array<double, 6> coeffs {{x0_, y0_, z0_, A_, B_, C_}};
45✔
1079
  write_dataset(group_id, "coefficients", coeffs);
45✔
1080
}
45✔
1081

1082
double SurfaceYTorus::evaluate(Position r) const
634,718✔
1083
{
1084
  double x = r.x - x0_;
634,718✔
1085
  double y = r.y - y0_;
634,718✔
1086
  double z = r.z - z0_;
634,718✔
1087
  return (y * y) / (B_ * B_) +
1,269,436✔
1088
         std::pow(std::sqrt(x * x + z * z) - A_, 2) / (C_ * C_) - 1.;
634,718✔
1089
}
1090

1091
double SurfaceYTorus::distance(Position r, Direction u, bool coincident) const
6,884,100✔
1092
{
1093
  double x = r.x - x0_;
6,884,100✔
1094
  double y = r.y - y0_;
6,884,100✔
1095
  double z = r.z - z0_;
6,884,100✔
1096
  return torus_distance(x, z, y, u.x, u.z, u.y, A_, B_, C_, coincident);
6,884,100✔
1097
}
1098

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

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

1119
//==============================================================================
1120
// SurfaceZTorus implementation
1121
//==============================================================================
1122

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

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

1135
double SurfaceZTorus::evaluate(Position r) const
1,071,821✔
1136
{
1137
  double x = r.x - x0_;
1,071,821✔
1138
  double y = r.y - y0_;
1,071,821✔
1139
  double z = r.z - z0_;
1,071,821✔
1140
  return (z * z) / (B_ * B_) +
2,143,642✔
1141
         std::pow(std::sqrt(x * x + y * y) - A_, 2) / (C_ * C_) - 1.;
1,071,821✔
1142
}
1143

1144
double SurfaceZTorus::distance(Position r, Direction u, bool coincident) const
7,995,312✔
1145
{
1146
  double x = r.x - x0_;
7,995,312✔
1147
  double y = r.y - y0_;
7,995,312✔
1148
  double z = r.z - z0_;
7,995,312✔
1149
  return torus_distance(x, y, z, u.x, u.y, u.z, A_, B_, C_, coincident);
7,995,312✔
1150
}
1151

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

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

1172
//==============================================================================
1173

1174
void read_surfaces(pugi::xml_node node,
6,463✔
1175
  std::set<std::pair<int, int>>& periodic_pairs,
1176
  std::unordered_map<int, double>& albedo_map,
1177
  std::unordered_map<int, int>& periodic_sense_map)
1178
{
1179
  simulation::nonvacuum_boundary_present = false;
6,463✔
1180

1181
  // Count the number of surfaces
1182
  int n_surfaces = 0;
6,463✔
1183
  for (pugi::xml_node surf_node : node.children("surface")) {
40,601✔
1184
    n_surfaces++;
34,138✔
1185
  }
1186

1187
  // Loop over XML surface elements and populate the array.  Keep track of
1188
  // periodic surfaces and their albedos.
1189
  model::surfaces.reserve(n_surfaces);
6,463✔
1190
  {
1191
    pugi::xml_node surf_node;
6,463✔
1192
    int i_surf;
1193
    for (surf_node = node.child("surface"), i_surf = 0; surf_node;
40,601✔
1194
         surf_node = surf_node.next_sibling("surface"), i_surf++) {
34,138✔
1195
      std::string surf_type = get_node_value(surf_node, "type", true, true);
34,138✔
1196

1197
      // Allocate and initialize the new surface
1198

1199
      if (surf_type == "x-plane") {
34,138✔
1200
        model::surfaces.push_back(make_unique<SurfaceXPlane>(surf_node));
8,393✔
1201

1202
      } else if (surf_type == "y-plane") {
25,745✔
1203
        model::surfaces.push_back(make_unique<SurfaceYPlane>(surf_node));
7,549✔
1204

1205
      } else if (surf_type == "z-plane") {
18,196✔
1206
        model::surfaces.push_back(make_unique<SurfaceZPlane>(surf_node));
5,455✔
1207

1208
      } else if (surf_type == "plane") {
12,741✔
1209
        model::surfaces.push_back(make_unique<SurfacePlane>(surf_node));
1,578✔
1210

1211
      } else if (surf_type == "x-cylinder") {
11,163✔
1212
        model::surfaces.push_back(make_unique<SurfaceXCylinder>(surf_node));
36✔
1213

1214
      } else if (surf_type == "y-cylinder") {
11,127✔
1215
        model::surfaces.push_back(make_unique<SurfaceYCylinder>(surf_node));
12✔
1216

1217
      } else if (surf_type == "z-cylinder") {
11,115✔
1218
        model::surfaces.push_back(make_unique<SurfaceZCylinder>(surf_node));
4,200✔
1219

1220
      } else if (surf_type == "sphere") {
6,915✔
1221
        model::surfaces.push_back(make_unique<SurfaceSphere>(surf_node));
6,711✔
1222

1223
      } else if (surf_type == "x-cone") {
204!
1224
        model::surfaces.push_back(make_unique<SurfaceXCone>(surf_node));
×
1225

1226
      } else if (surf_type == "y-cone") {
204!
1227
        model::surfaces.push_back(make_unique<SurfaceYCone>(surf_node));
×
1228

1229
      } else if (surf_type == "z-cone") {
204✔
1230
        model::surfaces.push_back(make_unique<SurfaceZCone>(surf_node));
12✔
1231

1232
      } else if (surf_type == "quadric") {
192✔
1233
        model::surfaces.push_back(make_unique<SurfaceQuadric>(surf_node));
12✔
1234

1235
      } else if (surf_type == "x-torus") {
180✔
1236
        model::surfaces.push_back(std::make_unique<SurfaceXTorus>(surf_node));
48✔
1237

1238
      } else if (surf_type == "y-torus") {
132✔
1239
        model::surfaces.push_back(std::make_unique<SurfaceYTorus>(surf_node));
48✔
1240

1241
      } else if (surf_type == "z-torus") {
84!
1242
        model::surfaces.push_back(std::make_unique<SurfaceZTorus>(surf_node));
84✔
1243

1244
      } else {
1245
        fatal_error(fmt::format("Invalid surface type, \"{}\"", surf_type));
×
1246
      }
1247

1248
      // Check for a periodic surface
1249
      if (check_for_node(surf_node, "boundary")) {
34,138✔
1250
        std::string surf_bc = get_node_value(surf_node, "boundary", true, true);
20,452✔
1251
        if (surf_bc != "vacuum")
20,452✔
1252
          simulation::nonvacuum_boundary_present = true;
10,663✔
1253
        if (surf_bc == "periodic") {
20,452✔
1254
          periodic_sense_map[model::surfaces.back()->id_] = 0;
440✔
1255
          // Check for surface albedo. Skip sanity check as it is already done
1256
          // in the Surface class's constructor.
1257
          if (check_for_node(surf_node, "albedo")) {
440!
1258
            albedo_map[model::surfaces.back()->id_] =
×
1259
              std::stod(get_node_value(surf_node, "albedo"));
×
1260
          }
1261
          if (check_for_node(surf_node, "periodic_surface_id")) {
440✔
1262
            int i_periodic =
1263
              std::stoi(get_node_value(surf_node, "periodic_surface_id"));
288✔
1264
            int lo_id = std::min(model::surfaces.back()->id_, i_periodic);
288✔
1265
            int hi_id = std::max(model::surfaces.back()->id_, i_periodic);
288✔
1266
            periodic_pairs.insert({lo_id, hi_id});
288✔
1267
          } else {
1268
            periodic_pairs.insert({model::surfaces.back()->id_, -1});
152✔
1269
          }
1270
        }
1271
      }
20,452✔
1272
    }
34,138✔
1273
  }
1274

1275
  // Fill the surface map
1276
  for (int i_surf = 0; i_surf < model::surfaces.size(); i_surf++) {
40,601✔
1277
    int id = model::surfaces[i_surf]->id_;
34,138✔
1278
    auto in_map = model::surface_map.find(id);
34,138✔
1279
    if (in_map == model::surface_map.end()) {
34,138!
1280
      model::surface_map[id] = i_surf;
34,138✔
1281
    } else {
1282
      fatal_error(
×
1283
        fmt::format("Two or more surfaces use the same unique ID: {}", id));
×
1284
    }
1285
  }
1286
}
6,463✔
1287

1288
void prepare_boundary_conditions(std::set<std::pair<int, int>>& periodic_pairs,
6,463✔
1289
  std::unordered_map<int, double>& albedo_map,
1290
  std::unordered_map<int, int>& periodic_sense_map)
1291
{
1292
  // Fill the senses map for periodic surfaces
1293
  auto n_periodic = periodic_sense_map.size();
6,463✔
1294
  for (const auto& cell : model::cells) {
6,670✔
1295
    if (n_periodic == 0)
6,564✔
1296
      break; // Early exit once all periodic surfaces found
6,357✔
1297

1298
    for (auto s : cell->surfaces()) {
1,074✔
1299
      auto surf_idx = std::abs(s) - 1;
867✔
1300
      auto id = model::surfaces[surf_idx]->id_;
867✔
1301

1302
      if (periodic_sense_map.count(id)) {
867✔
1303
        periodic_sense_map[id] = std::copysign(1, s);
440✔
1304
        --n_periodic;
440✔
1305
      }
1306
    }
207✔
1307
  }
1308

1309
  // Resolve unpaired periodic surfaces.  A lambda function is used with
1310
  // std::find_if to identify the unpaired surfaces.
1311
  auto is_unresolved_pair = [](const std::pair<int, int> p) {
296✔
1312
    return p.second == -1;
296✔
1313
  };
1314
  auto first_unresolved = std::find_if(
6,463✔
1315
    periodic_pairs.begin(), periodic_pairs.end(), is_unresolved_pair);
1316
  if (first_unresolved != periodic_pairs.end()) {
6,463✔
1317
    // Found one unpaired surface; search for a second one
1318
    auto next_elem = first_unresolved;
76✔
1319
    next_elem++;
76✔
1320
    auto second_unresolved =
1321
      std::find_if(next_elem, periodic_pairs.end(), is_unresolved_pair);
76✔
1322
    if (second_unresolved == periodic_pairs.end()) {
76!
1323
      fatal_error("Found only one periodic surface without a specified partner."
×
1324
                  " Please specify the partner for each periodic surface.");
1325
    }
1326

1327
    // Make sure there isn't a third unpaired surface
1328
    next_elem = second_unresolved;
76✔
1329
    next_elem++;
76✔
1330
    auto third_unresolved =
1331
      std::find_if(next_elem, periodic_pairs.end(), is_unresolved_pair);
76✔
1332
    if (third_unresolved != periodic_pairs.end()) {
76!
1333
      fatal_error(
×
1334
        "Found at least three periodic surfaces without a specified "
1335
        "partner. Please specify the partner for each periodic surface.");
1336
    }
1337

1338
    // Add the completed pair and remove the old, unpaired entries
1339
    int lo_id = std::min(first_unresolved->first, second_unresolved->first);
76✔
1340
    int hi_id = std::max(first_unresolved->first, second_unresolved->first);
76✔
1341
    periodic_pairs.insert({lo_id, hi_id});
76✔
1342
    periodic_pairs.erase(first_unresolved);
76✔
1343
    periodic_pairs.erase(second_unresolved);
76✔
1344
  }
1345

1346
  // Assign the periodic boundary conditions with albedos
1347
  for (auto periodic_pair : periodic_pairs) {
6,683✔
1348
    int i_surf = model::surface_map[periodic_pair.first];
220✔
1349
    int j_surf = model::surface_map[periodic_pair.second];
220✔
1350
    Surface& surf1 {*model::surfaces[i_surf]};
220✔
1351
    Surface& surf2 {*model::surfaces[j_surf]};
220✔
1352

1353
    // Compute the dot product of the surface normals
1354
    Direction norm1 = surf1.normal({0, 0, 0});
220✔
1355
    Direction norm2 = surf2.normal({0, 0, 0});
220✔
1356
    norm1 /= norm1.norm();
220✔
1357
    norm2 /= norm2.norm();
220✔
1358
    double dot_prod = norm1.dot(norm2);
220✔
1359

1360
    // If the dot product is 1 (to within floating point precision) then the
1361
    // planes are parallel which indicates a translational periodic boundary
1362
    // condition.  Otherwise, it is a rotational periodic BC.
1363
    if (std::abs(1.0 - dot_prod) < FP_PRECISION) {
220✔
1364
      surf1.bc_ = make_unique<TranslationalPeriodicBC>(i_surf, j_surf);
91✔
1365
      surf2.bc_ = make_unique<TranslationalPeriodicBC>(j_surf, i_surf);
91✔
1366
    } else {
1367
      // check that both normals have at least one 0 component
1368
      if (std::abs(norm1.x) > FP_PRECISION &&
246✔
1369
          std::abs(norm1.y) > FP_PRECISION &&
246!
1370
          std::abs(norm1.z) > FP_PRECISION) {
93✔
1371
        fatal_error(fmt::format(
×
1372
          "The normal ({}) of the periodic surface ({}) does not contain any "
1373
          "component with a zero value. A RotationalPeriodicBC requires one "
1374
          "component which is zero for both plane normals.",
1375
          norm1, i_surf));
1376
      }
1377
      if (std::abs(norm2.x) > FP_PRECISION &&
234✔
1378
          std::abs(norm2.y) > FP_PRECISION &&
234!
1379
          std::abs(norm2.z) > FP_PRECISION) {
93✔
1380
        fatal_error(fmt::format(
×
1381
          "The normal ({}) of the periodic surface ({}) does not contain any "
1382
          "component with a zero value. A RotationalPeriodicBC requires one "
1383
          "component which is zero for both plane normals.",
1384
          norm2, j_surf));
1385
      }
1386
      // find common zero component, which indicates the periodic axis
1387
      RotationalPeriodicBC::PeriodicAxis axis;
1388
      if (std::abs(norm1.x) <= FP_PRECISION &&
141!
1389
          std::abs(norm2.x) <= FP_PRECISION) {
12✔
1390
        axis = RotationalPeriodicBC::PeriodicAxis::x;
12✔
1391
      } else if (std::abs(norm1.y) <= FP_PRECISION &&
141✔
1392
                 std::abs(norm2.y) <= FP_PRECISION) {
24✔
1393
        axis = RotationalPeriodicBC::PeriodicAxis::y;
12✔
1394
      } else if (std::abs(norm1.z) <= FP_PRECISION &&
210!
1395
                 std::abs(norm2.z) <= FP_PRECISION) {
105✔
1396
        axis = RotationalPeriodicBC::PeriodicAxis::z;
105✔
1397
      } else {
1398
        fatal_error(fmt::format(
×
1399
          "There is no component which is 0.0 in both normal vectors. This "
1400
          "indicates that the two planes are not periodic about the X, Y, or Z "
1401
          "axis, which is not supported."));
1402
      }
1403
      auto i_sign = periodic_sense_map[periodic_pair.first];
129✔
1404
      auto j_sign = periodic_sense_map[periodic_pair.second];
129✔
1405
      surf1.bc_ = make_unique<RotationalPeriodicBC>(
129✔
1406
        i_sign * (i_surf + 1), j_sign * (j_surf + 1), axis);
129✔
1407
      surf2.bc_ = make_unique<RotationalPeriodicBC>(
129✔
1408
        j_sign * (j_surf + 1), i_sign * (i_surf + 1), axis);
258✔
1409
    }
1410

1411
    // If albedo data is present in albedo map, set the boundary albedo.
1412
    if (albedo_map.count(surf1.id_)) {
220!
1413
      surf1.bc_->set_albedo(albedo_map[surf1.id_]);
×
1414
    }
1415
    if (albedo_map.count(surf2.id_)) {
220!
1416
      surf2.bc_->set_albedo(albedo_map[surf2.id_]);
×
1417
    }
1418
  }
1419
}
6,463✔
1420

1421
void free_memory_surfaces()
6,564✔
1422
{
1423
  model::surfaces.clear();
6,564✔
1424
  model::surface_map.clear();
6,564✔
1425
}
6,564✔
1426

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

© 2026 Coveralls, Inc