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

openmc-dev / openmc / 20197979645

13 Dec 2025 09:16PM UTC coverage: 82.118% (+0.008%) from 82.11%
20197979645

Pull #3675

github

web-flow
Merge 3fafce82c into bbfa18d72
Pull Request #3675: Extend level scattering to support incident photons

17013 of 23594 branches covered (72.11%)

Branch coverage included in aggregate %.

55 of 76 new or added lines in 4 files covered. (72.37%)

193 existing lines in 7 files now uncovered.

55125 of 64253 relevant lines covered (85.79%)

43453609.56 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/container_util.h"
13
#include "openmc/error.h"
14
#include "openmc/external/quartic_solver.h"
15
#include "openmc/hdf5_interface.h"
16
#include "openmc/math_functions.h"
17
#include "openmc/random_lcg.h"
18
#include "openmc/settings.h"
19
#include "openmc/string_utils.h"
20
#include "openmc/xml_interface.h"
21

22
namespace openmc {
23

24
//==============================================================================
25
// Global variables
26
//==============================================================================
27

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

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

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

48
  // Copy the coefficients
49
  int i = 0;
40,986✔
50
  for (auto c : coeffs) {
122,665✔
51
    *c = coeffs_file[i++];
81,679✔
52
  }
53
}
40,986✔
54

55
//==============================================================================
56
// Surface implementation
57
//==============================================================================
58

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

61
Surface::Surface(pugi::xml_node surf_node)
40,986✔
62
{
63
  if (check_for_node(surf_node, "id")) {
40,986!
64
    id_ = std::stoi(get_node_value(surf_node, "id"));
40,986✔
65
    if (contains(settings::source_write_surf_id, id_) ||
81,184✔
66
        settings::source_write_surf_id.empty()) {
40,198✔
67
      surf_source_ = true;
39,944✔
68
    }
69
  } else {
70
    fatal_error("Must specify id of surface in geometry XML file.");
×
71
  }
72

73
  if (check_for_node(surf_node, "name")) {
40,986✔
74
    name_ = get_node_value(surf_node, "name", false);
9,544✔
75
  }
76

77
  if (check_for_node(surf_node, "boundary")) {
40,986✔
78
    std::string surf_bc = get_node_value(surf_node, "boundary", true, true);
24,128✔
79

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

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

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

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

111
      bc_->set_albedo(surf_alb);
96✔
112
    }
113
  }
24,128✔
114
}
40,986✔
115

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

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

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

138
  // Reflect direction according to normal.
139
  return u.reflect(n);
1,275,942,770✔
140
}
141

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

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

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

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

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

163
void Surface::to_hdf5(hid_t group_id) const
32,543✔
164
{
165
  hid_t surf_group = create_group(group_id, fmt::format("surface {}", id_));
65,086✔
166

167
  if (geom_type() == GeometryType::DAG) {
32,543!
168
    write_string(surf_group, "geom_type", "dagmc", false);
602!
169
  } else if (geom_type() == GeometryType::CSG) {
31,941!
170
    write_string(surf_group, "geom_type", "csg", false);
31,941✔
171

172
    if (bc_) {
31,941✔
173
      write_string(surf_group, "boundary_type", bc_->type(), false);
19,073✔
174
      bc_->to_hdf5(surf_group);
19,073✔
175

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

182
        if (id_ == surf1.id_) {
282✔
183
          write_dataset(surf_group, "periodic_surface_id", surf2.id_);
141✔
184
        } else {
185
          write_dataset(surf_group, "periodic_surface_id", surf1.id_);
141✔
186
        }
187
      }
188
    } else {
189
      write_string(surf_group, "boundary_type", "transmission", false);
12,868✔
190
    }
191
  }
192

193
  if (!name_.empty()) {
32,543✔
194
    write_string(surf_group, "name", name_, false);
7,133✔
195
  }
196

197
  to_hdf5_inner(surf_group);
32,543✔
198

199
  close_group(surf_group);
32,543✔
200
}
32,543✔
201

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

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

220
//==============================================================================
221
// SurfaceXPlane implementation
222
//==============================================================================
223

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

229
double SurfaceXPlane::evaluate(Position r) const
1,595,086,404✔
230
{
231
  return r.x - x0_;
1,595,086,404✔
232
}
233

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

239
Direction SurfaceXPlane::normal(Position r) const
276,945,411✔
240
{
241
  return {1., 0., 0.};
276,945,411✔
242
}
243

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

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

260
//==============================================================================
261
// SurfaceYPlane implementation
262
//==============================================================================
263

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

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

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

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

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

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

300
//==============================================================================
301
// SurfaceZPlane implementation
302
//==============================================================================
303

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

309
double SurfaceZPlane::evaluate(Position r) const
492,471,659✔
310
{
311
  return r.z - z0_;
492,471,659✔
312
}
313

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

319
Direction SurfaceZPlane::normal(Position r) const
54,368,139✔
320
{
321
  return {0., 0., 1.};
54,368,139✔
322
}
323

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

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

340
//==============================================================================
341
// SurfacePlane implementation
342
//==============================================================================
343

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

349
double SurfacePlane::evaluate(Position r) const
198,411,446✔
350
{
351
  return A_ * r.x + B_ * r.y + C_ * r.z - D_;
198,411,446✔
352
}
353

354
double SurfacePlane::distance(Position r, Direction u, bool coincident) const
575,934,810✔
355
{
356
  const double f = A_ * r.x + B_ * r.y + C_ * r.z - D_;
575,934,810✔
357
  const double projection = A_ * u.x + B_ * u.y + C_ * u.z;
575,934,810✔
358
  if (coincident || std::abs(f) < FP_COINCIDENT || projection == 0.0) {
575,934,810!
359
    return INFTY;
35,190,970✔
360
  } else {
361
    const double d = -f / projection;
540,743,840✔
362
    if (d < 0.0)
540,743,840✔
363
      return INFTY;
252,756,846✔
364
    return d;
287,986,994✔
365
  }
366
}
367

368
Direction SurfacePlane::normal(Position r) const
5,701,865✔
369
{
370
  return {A_, B_, C_};
5,701,865✔
371
}
372

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

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

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

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

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

413
  if (quad < 0.0) {
1,720,044,181✔
414
    // No intersection with cylinder.
415
    return INFTY;
287,669,334✔
416

417
  } else if (coincident || std::abs(c) < FP_COINCIDENT) {
1,432,374,847✔
418
    // Particle is on the cylinder, thus one distance is positive/negative
419
    // and the other is zero. The sign of k determines if we are facing in or
420
    // out.
421
    if (k >= 0.0) {
665,730,816✔
422
      return INFTY;
333,948,780✔
423
    } else {
424
      return (-k + sqrt(quad)) / a;
331,782,036✔
425
    }
426

427
  } else if (c < 0.0) {
766,644,031✔
428
    // Particle is inside the cylinder, thus one distance must be negative
429
    // and one must be positive. The positive distance will be the one with
430
    // negative sign on sqrt(quad).
431
    return (-k + sqrt(quad)) / a;
301,468,609✔
432

433
  } else {
434
    // Particle is outside the cylinder, thus both distances are either
435
    // positive or negative. If positive, the smaller distance is the one
436
    // with positive sign on sqrt(quad).
437
    const double d = (-k - sqrt(quad)) / a;
465,175,422✔
438
    if (d < 0.0)
465,175,422✔
439
      return INFTY;
63,625,363✔
440
    return d;
401,550,059✔
441
  }
442
}
443

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

458
//==============================================================================
459
// SurfaceXCylinder implementation
460
//==============================================================================
461

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

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

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

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

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

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

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

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

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

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

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

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

545
//==============================================================================
546
// SurfaceZCylinder implementation
547
//==============================================================================
548

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

555
double SurfaceZCylinder::evaluate(Position r) const
1,450,962,988✔
556
{
557
  return axis_aligned_cylinder_evaluate<0, 1>(r, x0_, y0_, radius_);
1,450,962,988✔
558
}
559

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

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

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

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

589
//==============================================================================
590
// SurfaceSphere implementation
591
//==============================================================================
592

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

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

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

615
  if (quad < 0.0) {
697,025,705✔
616
    // No intersection with sphere.
617
    return INFTY;
120,980,097✔
618

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

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

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

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

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

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

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

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

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

701
  double d;
702

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

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

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

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

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

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

755
//==============================================================================
756
// SurfaceXCone implementation
757
//==============================================================================
758

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

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

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

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

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

787
//==============================================================================
788
// SurfaceYCone implementation
789
//==============================================================================
790

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

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

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

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

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

819
//==============================================================================
820
// SurfaceZCone implementation
821
//==============================================================================
822

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

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

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

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

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

851
//==============================================================================
852
// SurfaceQuadric implementation
853
//==============================================================================
854

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

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

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

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

889
  double d;
890

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

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

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

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

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

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

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

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

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

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

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

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

1011
//==============================================================================
1012
// SurfaceXTorus implementation
1013
//==============================================================================
1014

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

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

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

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

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

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

1064
//==============================================================================
1065
// SurfaceYTorus implementation
1066
//==============================================================================
1067

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

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

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

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

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

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

1117
//==============================================================================
1118
// SurfaceZTorus implementation
1119
//==============================================================================
1120

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

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

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

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

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

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

1170
//==============================================================================
1171

1172
void read_surfaces(pugi::xml_node node)
7,589✔
1173
{
1174
  // Count the number of surfaces
1175
  int n_surfaces = 0;
7,589✔
1176
  for (pugi::xml_node surf_node : node.children("surface")) {
48,575✔
1177
    n_surfaces++;
40,986✔
1178
  }
1179

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

1192
      // Allocate and initialize the new surface
1193

1194
      if (surf_type == "x-plane") {
40,986✔
1195
        model::surfaces.push_back(make_unique<SurfaceXPlane>(surf_node));
10,033✔
1196

1197
      } else if (surf_type == "y-plane") {
30,953✔
1198
        model::surfaces.push_back(make_unique<SurfaceYPlane>(surf_node));
9,001✔
1199

1200
      } else if (surf_type == "z-plane") {
21,952✔
1201
        model::surfaces.push_back(make_unique<SurfaceZPlane>(surf_node));
6,937✔
1202

1203
      } else if (surf_type == "plane") {
15,015✔
1204
        model::surfaces.push_back(make_unique<SurfacePlane>(surf_node));
1,520✔
1205

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

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

1212
      } else if (surf_type == "z-cylinder") {
13,431✔
1213
        model::surfaces.push_back(make_unique<SurfaceZCylinder>(surf_node));
4,840✔
1214

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

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

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

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

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

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

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

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

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

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

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

1279
  // Resolve unpaired periodic surfaces.  A lambda function is used with
1280
  // std::find_if to identify the unpaired surfaces.
1281
  auto is_unresolved_pair = [](const std::pair<int, int> p) {
233✔
1282
    return p.second == -1;
233✔
1283
  };
1284
  auto first_unresolved = std::find_if(
7,589✔
1285
    periodic_pairs.begin(), periodic_pairs.end(), is_unresolved_pair);
1286
  if (first_unresolved != periodic_pairs.end()) {
7,589✔
1287
    // Found one unpaired surface; search for a second one
1288
    auto next_elem = first_unresolved;
52✔
1289
    next_elem++;
52✔
1290
    auto second_unresolved =
1291
      std::find_if(next_elem, periodic_pairs.end(), is_unresolved_pair);
52✔
1292
    if (second_unresolved == periodic_pairs.end()) {
52!
1293
      fatal_error("Found only one periodic surface without a specified partner."
×
1294
                  " Please specify the partner for each periodic surface.");
1295
    }
1296

1297
    // Make sure there isn't a third unpaired surface
1298
    next_elem = second_unresolved;
52✔
1299
    next_elem++;
52✔
1300
    auto third_unresolved =
1301
      std::find_if(next_elem, periodic_pairs.end(), is_unresolved_pair);
52✔
1302
    if (third_unresolved != periodic_pairs.end()) {
52!
1303
      fatal_error(
×
1304
        "Found at least three periodic surfaces without a specified "
1305
        "partner. Please specify the partner for each periodic surface.");
1306
    }
1307

1308
    // Add the completed pair and remove the old, unpaired entries
1309
    int lo_id = std::min(first_unresolved->first, second_unresolved->first);
52✔
1310
    int hi_id = std::max(first_unresolved->first, second_unresolved->first);
52✔
1311
    periodic_pairs.insert({lo_id, hi_id});
52✔
1312
    periodic_pairs.erase(first_unresolved);
52✔
1313
    periodic_pairs.erase(second_unresolved);
52✔
1314
  }
1315

1316
  // Assign the periodic boundary conditions with albedos
1317
  for (auto periodic_pair : periodic_pairs) {
7,770✔
1318
    int i_surf = model::surface_map[periodic_pair.first];
181✔
1319
    int j_surf = model::surface_map[periodic_pair.second];
181✔
1320
    Surface& surf1 {*model::surfaces[i_surf]};
181✔
1321
    Surface& surf2 {*model::surfaces[j_surf]};
181✔
1322

1323
    // Compute the dot product of the surface normals
1324
    Direction norm1 = surf1.normal({0, 0, 0});
181✔
1325
    Direction norm2 = surf2.normal({0, 0, 0});
181✔
1326
    norm1 /= norm1.norm();
181✔
1327
    norm2 /= norm2.norm();
181✔
1328
    double dot_prod = norm1.dot(norm2);
181✔
1329

1330
    // If the dot product is 1 (to within floating point precision) then the
1331
    // planes are parallel which indicates a translational periodic boundary
1332
    // condition.  Otherwise, it is a rotational periodic BC.
1333
    if (std::abs(1.0 - dot_prod) < FP_PRECISION) {
181✔
1334
      surf1.bc_ = make_unique<TranslationalPeriodicBC>(i_surf, j_surf);
117✔
1335
      surf2.bc_ = make_unique<TranslationalPeriodicBC>(i_surf, j_surf);
117✔
1336
    } else {
1337
      // check that both normals have at least one 0 component
1338
      if (std::abs(norm1.x) > FP_PRECISION &&
112✔
1339
          std::abs(norm1.y) > FP_PRECISION &&
112!
1340
          std::abs(norm1.z) > FP_PRECISION) {
16✔
UNCOV
1341
        fatal_error(fmt::format(
×
1342
          "The normal ({}) of the periodic surface ({}) does not contain any "
1343
          "component with a zero value. A RotationalPeriodicBC requires one "
1344
          "component which is zero for both plane normals.",
1345
          norm1, i_surf));
1346
      }
1347
      if (std::abs(norm2.x) > FP_PRECISION &&
96✔
1348
          std::abs(norm2.y) > FP_PRECISION &&
96!
1349
          std::abs(norm2.z) > FP_PRECISION) {
16✔
UNCOV
1350
        fatal_error(fmt::format(
×
1351
          "The normal ({}) of the periodic surface ({}) does not contain any "
1352
          "component with a zero value. A RotationalPeriodicBC requires one "
1353
          "component which is zero for both plane normals.",
1354
          norm2, j_surf));
1355
      }
1356
      // find common zero component, which indicates the periodic axis
1357
      RotationalPeriodicBC::PeriodicAxis axis;
1358
      if (std::abs(norm1.x) <= FP_PRECISION &&
80!
1359
          std::abs(norm2.x) <= FP_PRECISION) {
16✔
1360
        axis = RotationalPeriodicBC::PeriodicAxis::x;
16✔
1361
      } else if (std::abs(norm1.y) <= FP_PRECISION &&
80✔
1362
                 std::abs(norm2.y) <= FP_PRECISION) {
32✔
1363
        axis = RotationalPeriodicBC::PeriodicAxis::y;
16✔
1364
      } else if (std::abs(norm1.z) <= FP_PRECISION &&
64!
1365
                 std::abs(norm2.z) <= FP_PRECISION) {
32✔
1366
        axis = RotationalPeriodicBC::PeriodicAxis::z;
32✔
1367
      } else {
UNCOV
1368
        fatal_error(fmt::format(
×
1369
          "There is no component which is 0.0 in both normal vectors. This "
1370
          "indicates that the two planes are not periodic about the X, Y, or Z "
1371
          "axis, which is not supported."));
1372
      }
1373
      surf1.bc_ = make_unique<RotationalPeriodicBC>(i_surf, j_surf, axis);
64✔
1374
      surf2.bc_ = make_unique<RotationalPeriodicBC>(i_surf, j_surf, axis);
64✔
1375
    }
1376

1377
    // If albedo data is present in albedo map, set the boundary albedo.
1378
    if (albedo_map.count(surf1.id_)) {
181!
UNCOV
1379
      surf1.bc_->set_albedo(albedo_map[surf1.id_]);
×
1380
    }
1381
    if (albedo_map.count(surf2.id_)) {
181!
UNCOV
1382
      surf2.bc_->set_albedo(albedo_map[surf2.id_]);
×
1383
    }
1384
  }
1385
}
7,589✔
1386

1387
void free_memory_surfaces()
7,707✔
1388
{
1389
  model::surfaces.clear();
7,707✔
1390
  model::surface_map.clear();
7,707✔
1391
}
7,707✔
1392

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