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

openmc-dev / openmc / 20501343020

25 Dec 2025 07:38AM UTC coverage: 82.185% (+0.05%) from 82.139%
20501343020

Pull #3692

github

web-flow
Merge fc35e8018 into 3f06a42ab
Pull Request #3692: Fix a bug in rotational periodic boundary conditions

17096 of 23665 branches covered (72.24%)

Branch coverage included in aggregate %.

36 of 37 new or added lines in 3 files covered. (97.3%)

198 existing lines in 11 files now uncovered.

55209 of 64313 relevant lines covered (85.84%)

43488756.38 hits per line

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

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

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

9
#include <fmt/core.h>
10

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

23
namespace openmc {
24

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

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

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

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

49
  // Copy the coefficients
50
  int i = 0;
41,503✔
51
  for (auto c : coeffs) {
124,523✔
52
    *c = coeffs_file[i++];
83,020✔
53
  }
54
}
41,503✔
55

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

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

62
Surface::Surface(pugi::xml_node surf_node)
41,503✔
63
{
64
  if (check_for_node(surf_node, "id")) {
41,503!
65
    id_ = std::stoi(get_node_value(surf_node, "id"));
41,503✔
66
    if (contains(settings::source_write_surf_id, id_) ||
82,218✔
67
        settings::source_write_surf_id.empty()) {
40,715✔
68
      surf_source_ = true;
40,461✔
69
    }
70
  } else {
71
    fatal_error("Must specify id of surface in geometry XML file.");
×
72
  }
73

74
  if (check_for_node(surf_node, "name")) {
41,503✔
75
    name_ = get_node_value(surf_node, "name", false);
9,672✔
76
  }
77

78
  if (check_for_node(surf_node, "boundary")) {
41,503✔
79
    std::string surf_bc = get_node_value(surf_node, "boundary", true, true);
24,533✔
80

81
    if (surf_bc == "transmission" || surf_bc == "transmit" || surf_bc.empty()) {
24,533!
82
      // Leave the bc_ a nullptr
83
    } else if (surf_bc == "vacuum") {
24,533✔
84
      bc_ = make_unique<VacuumBC>();
12,184✔
85
    } else if (surf_bc == "reflective" || surf_bc == "reflect" ||
13,013!
86
               surf_bc == "reflecting") {
664✔
87
      bc_ = make_unique<ReflectiveBC>();
11,685✔
88
    } else if (surf_bc == "white") {
664✔
89
      bc_ = make_unique<WhiteBC>();
96✔
90
    } else if (surf_bc == "periodic") {
568!
91
      // Periodic BCs are handled separately
92
    } else {
93
      fatal_error(fmt::format("Unknown boundary condition \"{}\" specified "
×
94
                              "on surface {}",
95
        surf_bc, id_));
×
96
    }
97

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

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

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

112
      bc_->set_albedo(surf_alb);
96✔
113
    }
114
  }
24,533✔
115
}
41,503✔
116

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

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

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

139
  // Reflect direction according to normal.
140
  return u.reflect(n);
1,276,857,068✔
141
}
142

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

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

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

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

160
  // normalize the direction
161
  return u / u.norm();
2,014,298✔
162
}
163

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

168
  if (geom_type() == GeometryType::DAG) {
33,351!
169
    write_string(surf_group, "geom_type", "dagmc", false);
665!
170
  } else if (geom_type() == GeometryType::CSG) {
32,686!
171
    write_string(surf_group, "geom_type", "csg", false);
32,686✔
172

173
    if (bc_) {
32,686✔
174
      write_string(surf_group, "boundary_type", bc_->type(), false);
19,641✔
175
      bc_->to_hdf5(surf_group);
19,641✔
176

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

183
        if (id_ == surf1.id_) {
458✔
184
          write_dataset(surf_group, "periodic_surface_id", surf2.id_);
361✔
185
        } else {
186
          write_dataset(surf_group, "periodic_surface_id", surf1.id_);
97✔
187
        }
188
      }
189
    } else {
190
      write_string(surf_group, "boundary_type", "transmission", false);
13,045✔
191
    }
192
  }
193

194
  if (!name_.empty()) {
33,351✔
195
    write_string(surf_group, "name", name_, false);
7,419✔
196
  }
197

198
  to_hdf5_inner(surf_group);
33,351✔
199

200
  close_group(surf_group);
33,351✔
201
}
33,351✔
202

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

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

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

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

230
double SurfaceXPlane::evaluate(Position r) const
1,597,066,926✔
231
{
232
  return r.x - x0_;
1,597,066,926✔
233
}
234

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

240
Direction SurfaceXPlane::normal(Position r) const
277,460,915✔
241
{
242
  return {1., 0., 0.};
277,460,915✔
243
}
244

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

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

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

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

270
double SurfaceYPlane::evaluate(Position r) const
1,584,792,970✔
271
{
272
  return r.y - y0_;
1,584,792,970✔
273
}
274

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

280
Direction SurfaceYPlane::normal(Position r) const
297,839,402✔
281
{
282
  return {0., 1., 0.};
297,839,402✔
283
}
284

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

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

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

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

310
double SurfaceZPlane::evaluate(Position r) const
493,116,133✔
311
{
312
  return r.z - z0_;
493,116,133✔
313
}
314

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

320
Direction SurfaceZPlane::normal(Position r) const
54,400,050✔
321
{
322
  return {0., 0., 1.};
54,400,050✔
323
}
324

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

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

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

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

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

355
double SurfacePlane::distance(Position r, Direction u, bool coincident) const
587,077,940✔
356
{
357
  const double f = A_ * r.x + B_ * r.y + C_ * r.z - D_;
587,077,940✔
358
  const double projection = A_ * u.x + B_ * u.y + C_ * u.z;
587,077,940✔
359
  if (coincident || std::abs(f) < FP_COINCIDENT || projection == 0.0) {
587,077,940!
360
    return INFTY;
36,631,035✔
361
  } else {
362
    const double d = -f / projection;
550,446,905✔
363
    if (d < 0.0)
550,446,905✔
364
      return INFTY;
257,101,260✔
365
    return d;
293,345,645✔
366
  }
367
}
368

369
Direction SurfacePlane::normal(Position r) const
5,702,484✔
370
{
371
  return {A_, B_, C_};
5,702,484✔
372
}
373

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

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

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

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

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

414
  if (quad < 0.0) {
1,725,615,746✔
415
    // No intersection with cylinder.
416
    return INFTY;
289,013,820✔
417

418
  } else if (coincident || std::abs(c) < FP_COINCIDENT) {
1,436,601,926✔
419
    // Particle is on the cylinder, thus one distance is positive/negative
420
    // and the other is zero. The sign of k determines if we are facing in or
421
    // out.
422
    if (k >= 0.0) {
666,530,868✔
423
      return INFTY;
334,347,321✔
424
    } else {
425
      return (-k + sqrt(quad)) / a;
332,183,547✔
426
    }
427

428
  } else if (c < 0.0) {
770,071,058✔
429
    // Particle is inside the cylinder, thus one distance must be negative
430
    // and one must be positive. The positive distance will be the one with
431
    // negative sign on sqrt(quad).
432
    return (-k + sqrt(quad)) / a;
303,294,542✔
433

434
  } else {
435
    // Particle is outside the cylinder, thus both distances are either
436
    // positive or negative. If positive, the smaller distance is the one
437
    // with positive sign on sqrt(quad).
438
    const double d = (-k - sqrt(quad)) / a;
466,776,516✔
439
    if (d < 0.0)
466,776,516✔
440
      return INFTY;
64,204,645✔
441
    return d;
402,571,871✔
442
  }
443
}
444

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

459
//==============================================================================
460
// SurfaceXCylinder implementation
461
//==============================================================================
462

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

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

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

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

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

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

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

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

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

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

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

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

546
//==============================================================================
547
// SurfaceZCylinder implementation
548
//==============================================================================
549

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

556
double SurfaceZCylinder::evaluate(Position r) const
1,454,191,916✔
557
{
558
  return axis_aligned_cylinder_evaluate<0, 1>(r, x0_, y0_, radius_);
1,454,191,916✔
559
}
560

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

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

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

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

590
//==============================================================================
591
// SurfaceSphere implementation
592
//==============================================================================
593

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

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

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

616
  if (quad < 0.0) {
696,795,878✔
617
    // No intersection with sphere.
618
    return INFTY;
120,978,944✔
619

620
  } else if (coincident || std::abs(c) < FP_COINCIDENT) {
575,816,934!
621
    // Particle is on the sphere, thus one distance is positive/negative and
622
    // the other is zero. The sign of k determines if we are facing in or out.
623
    if (k >= 0.0) {
103,390,176✔
624
      return INFTY;
44,082,195✔
625
    } else {
626
      return -k + sqrt(quad);
59,307,981✔
627
    }
628

629
  } else if (c < 0.0) {
472,426,758✔
630
    // Particle is inside the sphere, thus one distance must be negative and
631
    // one must be positive. The positive distance will be the one with
632
    // negative sign on sqrt(quad)
633
    return -k + sqrt(quad);
416,530,829✔
634

635
  } else {
636
    // Particle is outside the sphere, thus both distances are either positive
637
    // or negative. If positive, the smaller distance is the one with positive
638
    // sign on sqrt(quad).
639
    const double d = -k - sqrt(quad);
55,895,929✔
640
    if (d < 0.0)
55,895,929✔
641
      return INFTY;
10,776,775✔
642
    return d;
45,119,154✔
643
  }
644
}
645

646
Direction SurfaceSphere::normal(Position r) const
21,468,436✔
647
{
648
  return {2.0 * (r.x - x0_), 2.0 * (r.y - y0_), 2.0 * (r.z - z0_)};
21,468,436✔
649
}
650

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

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

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

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

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

702
  double d;
703

704
  if (quad < 0.0) {
978,021!
705
    // No intersection with cone.
706
    return INFTY;
×
707

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

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

724
    // Determine the smallest positive solution.
725
    if (d < 0.0) {
918,511✔
726
      if (b > 0.0)
780,043✔
727
        d = b;
665,104✔
728
    } else {
729
      if (b > 0.0) {
138,468!
730
        if (b < d)
138,468!
731
          d = b;
138,468✔
732
      }
733
    }
734
  }
735

736
  // If the distance was negative, set boundary distance to infinity.
737
  if (d <= 0.0)
978,021✔
738
    return INFTY;
136,422✔
739
  return d;
841,599✔
740
}
741

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

756
//==============================================================================
757
// SurfaceXCone implementation
758
//==============================================================================
759

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

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

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

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

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

788
//==============================================================================
789
// SurfaceYCone implementation
790
//==============================================================================
791

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

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

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

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

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

820
//==============================================================================
821
// SurfaceZCone implementation
822
//==============================================================================
823

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

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

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

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

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

852
//==============================================================================
853
// SurfaceQuadric implementation
854
//==============================================================================
855

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

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

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

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

890
  double d;
891

892
  if (quad < 0.0) {
316,679!
893
    // No intersection with surface.
894
    return INFTY;
×
895

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

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

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

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

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

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

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

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

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

985
  std::complex<double> roots[4];
26,534,563✔
986
  oqs::quartic_solver(coeff, roots);
26,534,563✔
987

988
  // Find smallest positive, real root. In the case where the particle is
989
  // coincident with the surface, we are sure to have one root very close to
990
  // zero but possibly small and positive. A tolerance is set to discard that
991
  // zero.
992
  double distance = INFTY;
26,534,563✔
993
  double cutoff = coincident ? TORUS_TOL : 0.0;
26,534,563✔
994
  for (int i = 0; i < 4; ++i) {
132,672,815✔
995
    if (roots[i].imag() == 0) {
106,138,252✔
996
      double root = roots[i].real();
25,944,028✔
997
      if (root > cutoff && root < distance) {
25,944,028✔
998
        // Avoid roots corresponding to internal surfaces
999
        double s1 = x1 + u1 * root;
10,411,995✔
1000
        double s2 = x2 + u2 * root;
10,411,995✔
1001
        double s3 = x3 + u3 * root;
10,411,995✔
1002
        double check = D * s3 * s3 + s1 * s1 + s2 * s2 + A * A - C * C;
10,411,995✔
1003
        if (check >= 0) {
10,411,995!
1004
          distance = root;
10,411,995✔
1005
        }
1006
      }
1007
    }
1008
  }
1009
  return distance;
26,534,563✔
1010
}
1011

1012
//==============================================================================
1013
// SurfaceXTorus implementation
1014
//==============================================================================
1015

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

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

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

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

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

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

1065
//==============================================================================
1066
// SurfaceYTorus implementation
1067
//==============================================================================
1068

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

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

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

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

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

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

1118
//==============================================================================
1119
// SurfaceZTorus implementation
1120
//==============================================================================
1121

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

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

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

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

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

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

1171
//==============================================================================
1172

1173
void read_surfaces(pugi::xml_node node)
7,708✔
1174
{
1175
  // Count the number of surfaces
1176
  int n_surfaces = 0;
7,708✔
1177
  for (pugi::xml_node surf_node : node.children("surface")) {
49,211✔
1178
    n_surfaces++;
41,503✔
1179
  }
1180

1181
  // Loop over XML surface elements and populate the array.  Keep track of
1182
  // periodic surfaces and their albedos.
1183
  model::surfaces.reserve(n_surfaces);
7,708✔
1184
  std::set<std::pair<int, int>> periodic_pairs;
7,708✔
1185
  std::unordered_map<int, double> albedo_map;
7,708✔
1186
  std::unordered_map<int, int> periodic_sense_map;
7,708✔
1187
  {
1188
    pugi::xml_node surf_node;
7,708✔
1189
    int i_surf;
1190
    for (surf_node = node.child("surface"), i_surf = 0; surf_node;
49,211✔
1191
         surf_node = surf_node.next_sibling("surface"), i_surf++) {
41,503✔
1192
      std::string surf_type = get_node_value(surf_node, "type", true, true);
41,503✔
1193

1194
      // Allocate and initialize the new surface
1195

1196
      if (surf_type == "x-plane") {
41,503✔
1197
        model::surfaces.push_back(make_unique<SurfaceXPlane>(surf_node));
10,145✔
1198

1199
      } else if (surf_type == "y-plane") {
31,358✔
1200
        model::surfaces.push_back(make_unique<SurfaceYPlane>(surf_node));
9,065✔
1201

1202
      } else if (surf_type == "z-plane") {
22,293✔
1203
        model::surfaces.push_back(make_unique<SurfaceZPlane>(surf_node));
6,969✔
1204

1205
      } else if (surf_type == "plane") {
15,324✔
1206
        model::surfaces.push_back(make_unique<SurfacePlane>(surf_node));
1,726✔
1207

1208
      } else if (surf_type == "x-cylinder") {
13,598✔
1209
        model::surfaces.push_back(make_unique<SurfaceXCylinder>(surf_node));
48✔
1210

1211
      } else if (surf_type == "y-cylinder") {
13,550✔
1212
        model::surfaces.push_back(make_unique<SurfaceYCylinder>(surf_node));
16✔
1213

1214
      } else if (surf_type == "z-cylinder") {
13,534✔
1215
        model::surfaces.push_back(make_unique<SurfaceZCylinder>(surf_node));
4,943✔
1216

1217
      } else if (surf_type == "sphere") {
8,591✔
1218
        model::surfaces.push_back(make_unique<SurfaceSphere>(surf_node));
8,331✔
1219

1220
      } else if (surf_type == "x-cone") {
260!
1221
        model::surfaces.push_back(make_unique<SurfaceXCone>(surf_node));
×
1222

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

1226
      } else if (surf_type == "z-cone") {
260✔
1227
        model::surfaces.push_back(make_unique<SurfaceZCone>(surf_node));
16✔
1228

1229
      } else if (surf_type == "quadric") {
244✔
1230
        model::surfaces.push_back(make_unique<SurfaceQuadric>(surf_node));
16✔
1231

1232
      } else if (surf_type == "x-torus") {
228✔
1233
        model::surfaces.push_back(std::make_unique<SurfaceXTorus>(surf_node));
60✔
1234

1235
      } else if (surf_type == "y-torus") {
168✔
1236
        model::surfaces.push_back(std::make_unique<SurfaceYTorus>(surf_node));
60✔
1237

1238
      } else if (surf_type == "z-torus") {
108!
1239
        model::surfaces.push_back(std::make_unique<SurfaceZTorus>(surf_node));
108✔
1240

1241
      } else {
1242
        fatal_error(fmt::format("Invalid surface type, \"{}\"", surf_type));
×
1243
      }
1244

1245
      // Check for a periodic surface
1246
      if (check_for_node(surf_node, "boundary")) {
41,503✔
1247
        std::string surf_bc = get_node_value(surf_node, "boundary", true, true);
24,533✔
1248
        if (surf_bc == "periodic") {
24,533✔
1249
          periodic_sense_map[model::surfaces.back()->id_] = 0;
568✔
1250
          // Check for surface albedo. Skip sanity check as it is already done
1251
          // in the Surface class's constructor.
1252
          if (check_for_node(surf_node, "albedo")) {
568!
1253
            albedo_map[model::surfaces.back()->id_] =
×
1254
              std::stod(get_node_value(surf_node, "albedo"));
×
1255
          }
1256
          if (check_for_node(surf_node, "periodic_surface_id")) {
568✔
1257
            int i_periodic =
1258
              std::stoi(get_node_value(surf_node, "periodic_surface_id"));
368✔
1259
            int lo_id = std::min(model::surfaces.back()->id_, i_periodic);
368✔
1260
            int hi_id = std::max(model::surfaces.back()->id_, i_periodic);
368✔
1261
            periodic_pairs.insert({lo_id, hi_id});
368✔
1262
          } else {
1263
            periodic_pairs.insert({model::surfaces.back()->id_, -1});
200✔
1264
          }
1265
        }
1266
      }
24,533✔
1267
    }
41,503✔
1268
  }
1269

1270
  // Fill the surface map
1271
  for (int i_surf = 0; i_surf < model::surfaces.size(); i_surf++) {
49,211✔
1272
    int id = model::surfaces[i_surf]->id_;
41,503✔
1273
    auto in_map = model::surface_map.find(id);
41,503✔
1274
    if (in_map == model::surface_map.end()) {
41,503!
1275
      model::surface_map[id] = i_surf;
41,503✔
1276
    } else {
1277
      fatal_error(
×
1278
        fmt::format("Two or more surfaces use the same unique ID: {}", id));
×
1279
    }
1280
  }
1281

1282
  // Fill the senses map for periodic surfaces
1283
  for (pugi::xml_node cell_node : node.children("cell")) {
41,014✔
1284

1285
    // Read the region specification.
1286
    std::string region_spec;
33,306✔
1287
    if (check_for_node(cell_node, "region")) {
33,306✔
1288
      region_spec = get_node_value(cell_node, "region");
24,419✔
1289

1290
      // Get a tokenized representation of the region specification and apply De
1291
      // Morgans law
1292
      Region region(region_spec, 0);
24,419✔
1293

1294
      for (auto s : region.surfaces()) {
90,206✔
1295
        auto id = model::surfaces[std::abs(s) - 1]->id_;
65,787✔
1296
        if (periodic_sense_map.find(id) != periodic_sense_map.end()) {
65,787✔
1297
          periodic_sense_map[id] = std::copysign(1, s);
744✔
1298
        }
1299
      }
24,419✔
1300
    }
24,419✔
1301
  }
33,306✔
1302

1303
  // Resolve unpaired periodic surfaces.  A lambda function is used with
1304
  // std::find_if to identify the unpaired surfaces.
1305
  auto is_unresolved_pair = [](const std::pair<int, int> p) {
384✔
1306
    return p.second == -1;
384✔
1307
  };
1308
  auto first_unresolved = std::find_if(
7,708✔
1309
    periodic_pairs.begin(), periodic_pairs.end(), is_unresolved_pair);
1310
  if (first_unresolved != periodic_pairs.end()) {
7,708✔
1311
    // Found one unpaired surface; search for a second one
1312
    auto next_elem = first_unresolved;
100✔
1313
    next_elem++;
100✔
1314
    auto second_unresolved =
1315
      std::find_if(next_elem, periodic_pairs.end(), is_unresolved_pair);
100✔
1316
    if (second_unresolved == periodic_pairs.end()) {
100!
UNCOV
1317
      fatal_error("Found only one periodic surface without a specified partner."
×
1318
                  " Please specify the partner for each periodic surface.");
1319
    }
1320

1321
    // Make sure there isn't a third unpaired surface
1322
    next_elem = second_unresolved;
100✔
1323
    next_elem++;
100✔
1324
    auto third_unresolved =
1325
      std::find_if(next_elem, periodic_pairs.end(), is_unresolved_pair);
100✔
1326
    if (third_unresolved != periodic_pairs.end()) {
100!
UNCOV
1327
      fatal_error(
×
1328
        "Found at least three periodic surfaces without a specified "
1329
        "partner. Please specify the partner for each periodic surface.");
1330
    }
1331

1332
    // Add the completed pair and remove the old, unpaired entries
1333
    int lo_id = std::min(first_unresolved->first, second_unresolved->first);
100✔
1334
    int hi_id = std::max(first_unresolved->first, second_unresolved->first);
100✔
1335
    periodic_pairs.insert({lo_id, hi_id});
100✔
1336
    periodic_pairs.erase(first_unresolved);
100✔
1337
    periodic_pairs.erase(second_unresolved);
100✔
1338
  }
1339

1340
  // Assign the periodic boundary conditions with albedos
1341
  for (auto periodic_pair : periodic_pairs) {
7,992✔
1342
    int i_surf = model::surface_map[periodic_pair.first];
284✔
1343
    int j_surf = model::surface_map[periodic_pair.second];
284✔
1344
    Surface& surf1 {*model::surfaces[i_surf]};
284✔
1345
    Surface& surf2 {*model::surfaces[j_surf]};
284✔
1346

1347
    // Compute the dot product of the surface normals
1348
    Direction norm1 = surf1.normal({0, 0, 0});
284✔
1349
    Direction norm2 = surf2.normal({0, 0, 0});
284✔
1350
    norm1 /= norm1.norm();
284✔
1351
    norm2 /= norm2.norm();
284✔
1352
    double dot_prod = norm1.dot(norm2);
284✔
1353

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

1405
    // If albedo data is present in albedo map, set the boundary albedo.
1406
    if (albedo_map.count(surf1.id_)) {
284!
UNCOV
1407
      surf1.bc_->set_albedo(albedo_map[surf1.id_]);
×
1408
    }
1409
    if (albedo_map.count(surf2.id_)) {
284!
UNCOV
1410
      surf2.bc_->set_albedo(albedo_map[surf2.id_]);
×
1411
    }
1412
  }
1413
}
7,708✔
1414

1415
void free_memory_surfaces()
7,826✔
1416
{
1417
  model::surfaces.clear();
7,826✔
1418
  model::surface_map.clear();
7,826✔
1419
}
7,826✔
1420

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

© 2025 Coveralls, Inc