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

openmc-dev / openmc / 29167401541

11 Jul 2026 08:38PM UTC coverage: 80.762% (-0.5%) from 81.295%
29167401541

Pull #4010

github

web-flow
Merge 612540872 into e783e0147
Pull Request #4010: Implementation of surfaces with custom implicit function

18489 of 27369 branches covered (67.55%)

Branch coverage included in aggregate %.

1041 of 1330 new or added lines in 13 files covered. (78.27%)

76 existing lines in 3 files now uncovered.

60486 of 70418 relevant lines covered (85.9%)

53343422.58 hits per line

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

81.05
/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(
47,709✔
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");
47,709✔
43
  if (coeffs_file.size() != coeffs.size()) {
47,709!
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;
47,709✔
51
  for (auto c : coeffs) {
142,421✔
52
    *c = coeffs_file[i++];
94,712✔
53
  }
54
}
47,709✔
55

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

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

62
Surface::Surface(pugi::xml_node surf_node)
47,709✔
63
{
64
  if (check_for_node(surf_node, "id")) {
47,709!
65
    id_ = std::stoi(get_node_value(surf_node, "id"));
95,418✔
66
    if (contains(settings::source_write_surf_id, id_) ||
95,418✔
67
        settings::source_write_surf_id.empty()) {
46,921✔
68
      surf_source_ = true;
46,667✔
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")) {
47,709✔
75
    name_ = get_node_value(surf_node, "name", false);
11,962✔
76
  }
77

78
  if (check_for_node(surf_node, "boundary")) {
47,709✔
79
    std::string surf_bc = get_node_value(surf_node, "boundary", true, true);
28,527✔
80

81
    if (surf_bc == "transmission" || surf_bc == "transmit" || surf_bc.empty()) {
57,054!
82
      // Leave the bc_ a nullptr
83
    } else if (surf_bc == "vacuum") {
28,527✔
84
      bc_ = make_unique<VacuumBC>();
14,180✔
85
    } else if (surf_bc == "reflective" || surf_bc == "reflect" ||
14,983!
86
               surf_bc == "reflecting") {
636!
87
      bc_ = make_unique<ReflectiveBC>();
13,711✔
88
    } else if (surf_bc == "white") {
636✔
89
      bc_ = make_unique<WhiteBC>();
90✔
90
    } else if (surf_bc == "periodic") {
546!
91
      // Periodic BCs are handled separately
92
    } else {
93
      fatal_error(fmt::format("Unknown boundary condition \"{}\" specified "
×
94
                              "on surface {}",
95
        surf_bc, id_));
×
96
    }
97

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

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

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

112
      bc_->set_albedo(surf_alb);
90✔
113
    }
114
  }
28,527✔
115
}
47,709✔
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;
10,834,237✔
129
  }
130
  return f > 0.0;
2,147,483,647✔
131
}
132

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

139
  // Reflect direction according to normal.
140
  return u.reflect(n);
969,425,684✔
141
}
142

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

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

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

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

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

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

168
  if (geom_type() == GeometryType::DAG) {
39,098✔
169
    write_string(surf_group, "geom_type", "dagmc", false);
1,220!
170
  } else if (geom_type() == GeometryType::CSG ||
38,576!
171
             geom_type() == GeometryType::IMP) {
88✔
172
    write_string(surf_group, "geom_type",
76,888✔
173
      geom_type() == GeometryType::IMP ? "imp" : "csg", false);
38,488✔
174

175
    if (bc_) {
38,488✔
176
      write_string(surf_group, "boundary_type", bc_->type(), false);
23,738✔
177
      bc_->to_hdf5(surf_group);
23,738✔
178

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

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

196
  if (!name_.empty()) {
39,098✔
197
    write_string(surf_group, "name", name_, false);
9,740✔
198
  }
199

200
  to_hdf5_inner(surf_group);
39,098✔
201

202
  close_group(surf_group);
39,098✔
203
}
39,098✔
204

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

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

223
//==============================================================================
224
// SurfaceXPlane implementation
225
//==============================================================================
226

227
SurfaceXPlane::SurfaceXPlane(pugi::xml_node surf_node) : Surface(surf_node)
12,543✔
228
{
229
  read_coeffs(surf_node, id_, {&x0_});
12,543✔
230
}
12,543✔
231

232
double SurfaceXPlane::evaluate(Position r) const
2,147,483,647✔
233
{
234
  return r.x - x0_;
2,147,483,647✔
235
}
236

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

242
Direction SurfaceXPlane::normal(Position r) const
308,609,368✔
243
{
244
  return {1., 0., 0.};
308,609,368✔
245
}
246

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

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

263
//==============================================================================
264
// SurfaceYPlane implementation
265
//==============================================================================
266

267
SurfaceYPlane::SurfaceYPlane(pugi::xml_node surf_node) : Surface(surf_node)
10,471✔
268
{
269
  read_coeffs(surf_node, id_, {&y0_});
10,471✔
270
}
10,471✔
271

272
double SurfaceYPlane::evaluate(Position r) const
2,147,483,647✔
273
{
274
  return r.y - y0_;
2,147,483,647✔
275
}
276

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

282
Direction SurfaceYPlane::normal(Position r) const
327,381,692✔
283
{
284
  return {0., 1., 0.};
327,381,692✔
285
}
286

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

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

303
//==============================================================================
304
// SurfaceZPlane implementation
305
//==============================================================================
306

307
SurfaceZPlane::SurfaceZPlane(pugi::xml_node surf_node) : Surface(surf_node)
7,612✔
308
{
309
  read_coeffs(surf_node, id_, {&z0_});
7,612✔
310
}
7,612✔
311

312
double SurfaceZPlane::evaluate(Position r) const
2,147,483,647✔
313
{
314
  return r.z - z0_;
2,147,483,647✔
315
}
316

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

322
Direction SurfaceZPlane::normal(Position r) const
61,822,347✔
323
{
324
  return {0., 0., 1.};
61,822,347✔
325
}
326

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

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

343
//==============================================================================
344
// SurfacePlane implementation
345
//==============================================================================
346

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

352
double SurfacePlane::evaluate(Position r) const
347,716,036✔
353
{
354
  return A_ * r.x + B_ * r.y + C_ * r.z - D_;
347,716,036✔
355
}
356

357
double SurfacePlane::distance(Position r, Direction u, bool coincident) const
804,028,670✔
358
{
359
  const double f = A_ * r.x + B_ * r.y + C_ * r.z - D_;
804,028,670✔
360
  const double projection = A_ * u.x + B_ * u.y + C_ * u.z;
804,028,670✔
361
  if (coincident || std::abs(f) < FP_COINCIDENT || projection == 0.0) {
804,028,670!
362
    return INFTY;
363
  } else {
364
    const double d = -f / projection;
738,335,333✔
365
    if (d < 0.0)
738,335,333✔
366
      return INFTY;
367
    return d;
401,800,211✔
368
  }
369
}
370

371
Direction SurfacePlane::normal(Position r) const
5,745,398✔
372
{
373
  return {A_, B_, C_};
5,745,398✔
374
}
375

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

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

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

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

410
  const double r2 = r.get<i2>() - offset1;
2,147,483,647✔
411
  const double r3 = r.get<i3>() - offset2;
2,147,483,647✔
412
  const double k = r2 * u.get<i2>() + r3 * u.get<i3>();
2,147,483,647✔
413
  const double c = r2 * r2 + r3 * r3 - radius * radius;
2,147,483,647✔
414
  const double quad = k * k - a * c;
2,147,483,647✔
415

416
  if (quad < 0.0) {
2,147,483,647✔
417
    // No intersection with cylinder.
418
    return INFTY;
419

420
  } else if (coincident || std::abs(c) < FP_COINCIDENT) {
2,147,483,647✔
421
    // Particle is on the cylinder, thus one distance is positive/negative
422
    // and the other is zero. The sign of k determines if we are facing in or
423
    // out.
424
    if (k >= 0.0) {
1,895,231,041✔
425
      return INFTY;
426
    } else {
427
      return (-k + sqrt(quad)) / a;
1,084,473,109✔
428
    }
429

430
  } else if (c < 0.0) {
2,147,483,647✔
431
    // Particle is inside the cylinder, thus one distance must be negative
432
    // and one must be positive. The positive distance will be the one with
433
    // negative sign on sqrt(quad).
434
    return (-k + sqrt(quad)) / a;
1,068,242,093✔
435

436
  } else {
437
    // Particle is outside the cylinder, thus both distances are either
438
    // positive or negative. If positive, the smaller distance is the one
439
    // with positive sign on sqrt(quad).
440
    const double d = (-k - sqrt(quad)) / a;
1,457,156,383✔
441
    if (d < 0.0)
1,457,156,383✔
442
      return INFTY;
443
    return d;
1,135,033,372✔
444
  }
445
}
446

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

461
//==============================================================================
462
// SurfaceXCylinder implementation
463
//==============================================================================
464

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

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

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

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

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

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

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

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

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

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

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

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

548
//==============================================================================
549
// SurfaceZCylinder implementation
550
//==============================================================================
551

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

558
double SurfaceZCylinder::evaluate(Position r) const
2,147,483,647✔
559
{
560
  return axis_aligned_cylinder_evaluate<0, 1>(r, x0_, y0_, radius_);
2,147,483,647✔
561
}
562

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

570
Direction SurfaceZCylinder::normal(Position r) const
280,838,901✔
571
{
572
  return axis_aligned_cylinder_normal<2, 0, 1>(r, x0_, y0_);
280,838,901✔
573
}
574

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

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

592
//==============================================================================
593
// SurfaceSphere implementation
594
//==============================================================================
595

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

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

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

618
  if (quad < 0.0) {
981,904,712✔
619
    // No intersection with sphere.
620
    return INFTY;
621

622
  } else if (coincident || std::abs(c) < FP_COINCIDENT) {
798,390,769!
623
    // Particle is on the sphere, thus one distance is positive/negative and
624
    // the other is zero. The sign of k determines if we are facing in or out.
625
    if (k >= 0.0) {
105,535,799✔
626
      return INFTY;
627
    } else {
628
      return -k + sqrt(quad);
60,082,828✔
629
    }
630

631
  } else if (c < 0.0) {
692,854,970✔
632
    // Particle is inside the sphere, thus one distance must be negative and
633
    // one must be positive. The positive distance will be the one with
634
    // negative sign on sqrt(quad)
635
    return -k + sqrt(quad);
629,188,024✔
636

637
  } else {
638
    // Particle is outside the sphere, thus both distances are either positive
639
    // or negative. If positive, the smaller distance is the one with positive
640
    // sign on sqrt(quad).
641
    const double d = -k - sqrt(quad);
63,666,946✔
642
    if (d < 0.0)
63,666,946✔
643
      return INFTY;
644
    return d;
49,160,647✔
645
  }
646
}
647

648
Direction SurfaceSphere::normal(Position r) const
54,528,727✔
649
{
650
  return {2.0 * (r.x - x0_), 2.0 * (r.y - y0_), 2.0 * (r.z - z0_)};
54,528,727✔
651
}
652

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

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

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

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

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

704
  double d;
705

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

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

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

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

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

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

758
//==============================================================================
759
// SurfaceXCone implementation
760
//==============================================================================
761

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

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

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

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

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

790
//==============================================================================
791
// SurfaceYCone implementation
792
//==============================================================================
793

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

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

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

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

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

822
//==============================================================================
823
// SurfaceZCone implementation
824
//==============================================================================
825

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

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

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

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

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

854
//==============================================================================
855
// SurfaceQuadric implementation
856
//==============================================================================
857

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

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

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

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

892
  double d;
316,679✔
893

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

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

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

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

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

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

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

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

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

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

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

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

1014
//==============================================================================
1015
// SurfaceXTorus implementation
1016
//==============================================================================
1017

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

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

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

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

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

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

1067
//==============================================================================
1068
// SurfaceYTorus implementation
1069
//==============================================================================
1070

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

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

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

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

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

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

1120
//==============================================================================
1121
// SurfaceZTorus implementation
1122
//==============================================================================
1123

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

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

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

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

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

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

1173
//==============================================================================
1174
// SurfaceImplicit implementation
1175
//==============================================================================
1176

1177
SurfaceImplicit::SurfaceImplicit(pugi::xml_node surf_node) : Surface(surf_node)
120✔
1178
{
1179
  read_coeffs(surf_node, id_,
240✔
1180
    {&x0_, &y0_, &z0_, &A_, &B_, &C_, &D_, &E_, &F_, &G_, &H_, &I_});
120✔
1181
  isovalue_ = surf_node.attribute("isovalue").as_double();
120✔
1182
  auto func_node = surf_node.child("function");
120✔
1183
  if (!func_node)
120!
NEW
1184
    fatal_error(fmt::format("Surface {} missing <function> element.", id_));
×
1185
  function_ = Implicit::from_xml_element(func_node.first_child());
120!
1186
  solver_ = ImplicitSolver::create(settings::implicit_solver,
120✔
1187
    settings::implicit_maxiter, settings::implicit_atol,
1188
    settings::implicit_ftol);
120✔
1189
}
120!
1190
void SurfaceImplicit::to_hdf5_inner(hid_t group_id) const
88✔
1191
{
1192
  write_string(group_id, "type", "implicit", false);
88✔
1193
  std::array<double, 12> coeffs {
88✔
1194
    {x0_, y0_, z0_, A_, B_, C_, D_, E_, F_, G_, H_, I_}};
88✔
1195
  write_dataset(group_id, "coefficients", coeffs);
88✔
1196
  write_dataset(group_id, "isovalue", isovalue_);
88✔
1197
  write_string(group_id, "function_xml", function_->to_xml_string(), false);
88✔
1198
}
88✔
1199
Position SurfaceImplicit::transform(Position r) const
31,782,425✔
1200
{
1201
  double dx = r.x - x0_, dy = r.y - y0_, dz = r.z - z0_;
31,782,425✔
1202
  return Position(A_ * dx + B_ * dy + C_ * dz, D_ * dx + E_ * dy + F_ * dz,
31,782,425✔
1203
    G_ * dx + H_ * dy + I_ * dz);
31,782,425✔
1204
}
1205
Direction SurfaceImplicit::transform_dir(Direction u) const
29,493,563✔
1206
{
1207
  // Rotation only — no translation for directions
1208
  return Direction(A_ * u.x + B_ * u.y + C_ * u.z,
29,493,563✔
1209
    D_ * u.x + E_ * u.y + F_ * u.z, G_ * u.x + H_ * u.y + I_ * u.z);
29,493,563✔
1210
}
1211
double SurfaceImplicit::evaluate(Position r) const
2,212,170✔
1212
{
1213
  return function_->evaluate(transform(r)) - isovalue_;
2,212,170✔
1214
}
1215
double SurfaceImplicit::distance_finite(
29,493,563✔
1216
  Position r, Direction u, bool coincident, double distance_max) const
1217
{
1218
  double f0 = function_->evaluate(r);
29,493,563✔
1219
  double t0 = (std::abs(f0) <= settings::implicit_ftol || coincident)
29,493,563✔
1220
                ? settings::implicit_margin
52,493,595✔
1221
                : 0.0;
29,493,563✔
1222
  Position r_tr = transform(r);
29,493,563✔
1223
  Direction u_tr = transform_dir(u);
29,493,563✔
1224
  return solver_->solve(*function_, r_tr, u_tr, t0, distance_max, isovalue_) +
29,493,563✔
1225
         settings::implicit_margin;
29,493,563✔
1226
}
NEW
1227
double SurfaceImplicit::distance(Position r, Direction u, bool coincident) const
×
1228
{
NEW
1229
  fatal_error("SurfaceImplicit::distance: shouldn't be called.");
×
1230
}
1231
Direction SurfaceImplicit::normal(Position r) const
76,692✔
1232
{
1233
  Position r_tr = transform(r);
76,692✔
1234
  Gradient g = function_->gradient(r_tr);
76,692✔
1235
  double nx = A_ * g.x + D_ * g.y + G_ * g.z;
76,692✔
1236
  double ny = B_ * g.x + E_ * g.y + H_ * g.z;
76,692✔
1237
  double nz = C_ * g.x + F_ * g.y + I_ * g.z;
76,692✔
1238
  double len = std::sqrt(nx * nx + ny * ny + nz * nz);
76,692✔
1239
  return {nx / len, ny / len, nz / len};
76,692✔
1240
}
1241

1242
//==============================================================================
1243

1244
void read_surfaces(pugi::xml_node node,
9,021✔
1245
  std::set<std::pair<int, int>>& periodic_pairs,
1246
  std::unordered_map<int, double>& albedo_map,
1247
  std::unordered_map<int, int>& periodic_sense_map)
1248
{
1249
  // Count the number of surfaces
1250
  int n_surfaces = 0;
9,021✔
1251
  for (pugi::xml_node surf_node : node.children("surface")) {
55,839✔
1252
    n_surfaces++;
46,818✔
1253
  }
1254

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

1265
      // Allocate and initialize the new surface
1266

1267
      if (surf_type == "x-plane") {
46,818✔
1268
        model::surfaces.push_back(make_unique<SurfaceXPlane>(surf_node));
11,652✔
1269

1270
      } else if (surf_type == "y-plane") {
35,166✔
1271
        model::surfaces.push_back(make_unique<SurfaceYPlane>(surf_node));
10,471✔
1272

1273
      } else if (surf_type == "z-plane") {
24,695✔
1274
        model::surfaces.push_back(make_unique<SurfaceZPlane>(surf_node));
7,612✔
1275

1276
      } else if (surf_type == "plane") {
17,083✔
1277
        model::surfaces.push_back(make_unique<SurfacePlane>(surf_node));
1,964✔
1278

1279
      } else if (surf_type == "x-cylinder") {
15,119✔
1280
        model::surfaces.push_back(make_unique<SurfaceXCylinder>(surf_node));
45✔
1281

1282
      } else if (surf_type == "y-cylinder") {
15,074✔
1283
        model::surfaces.push_back(make_unique<SurfaceYCylinder>(surf_node));
15✔
1284

1285
      } else if (surf_type == "z-cylinder") {
15,059✔
1286
        model::surfaces.push_back(make_unique<SurfaceZCylinder>(surf_node));
5,680✔
1287

1288
      } else if (surf_type == "sphere") {
9,379✔
1289
        model::surfaces.push_back(make_unique<SurfaceSphere>(surf_node));
9,007✔
1290

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

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

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

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

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

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

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

1312
      } else if (surf_type == "implicit") {
120!
1313
        model::surfaces.push_back(std::make_unique<SurfaceImplicit>(surf_node));
120✔
1314

1315
      } else {
1316
        fatal_error(fmt::format("Invalid surface type, \"{}\"", surf_type));
×
1317
      }
1318

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

1344
  // Fill the surface map
1345
  for (int i_surf = 0; i_surf < model::surfaces.size(); i_surf++) {
55,839✔
1346
    int id = model::surfaces[i_surf]->id_;
46,818!
1347
    auto in_map = model::surface_map.find(id);
46,818!
1348
    if (in_map == model::surface_map.end()) {
46,818!
1349
      model::surface_map[id] = i_surf;
46,818✔
1350
    } else {
1351
      fatal_error(
×
1352
        fmt::format("Two or more surfaces use the same unique ID: {}", id));
×
1353
    }
1354
  }
1355
}
9,021✔
1356

1357
void prepare_boundary_conditions(std::set<std::pair<int, int>>& periodic_pairs,
9,019✔
1358
  std::unordered_map<int, double>& albedo_map,
1359
  std::unordered_map<int, int>& periodic_sense_map)
1360
{
1361
  // Fill the senses map for periodic surfaces
1362
  auto n_periodic = periodic_sense_map.size();
9,019✔
1363
  for (const auto& cell : model::cells) {
9,276✔
1364
    if (n_periodic == 0)
9,145✔
1365
      break; // Early exit once all periodic surfaces found
1366

1367
    for (auto s : cell->surfaces()) {
1,335✔
1368
      auto surf_idx = std::abs(s) - 1;
1,078✔
1369
      auto id = model::surfaces[surf_idx]->id_;
1,078✔
1370

1371
      if (periodic_sense_map.count(id)) {
1,078✔
1372
        periodic_sense_map[id] = std::copysign(1, s);
546✔
1373
        --n_periodic;
546✔
1374
      }
1375
    }
257✔
1376
  }
1377

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

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

1407
    // Add the completed pair and remove the old, unpaired entries
1408
    int lo_id = std::min(first_unresolved->first, second_unresolved->first);
95!
1409
    int hi_id = std::max(first_unresolved->first, second_unresolved->first);
95!
1410
    periodic_pairs.insert({lo_id, hi_id});
95✔
1411
    periodic_pairs.erase(first_unresolved);
95✔
1412
    periodic_pairs.erase(second_unresolved);
95✔
1413
  }
1414

1415
  // Assign the periodic boundary conditions with albedos
1416
  for (auto periodic_pair : periodic_pairs) {
9,292✔
1417
    int i_surf = model::surface_map[periodic_pair.first];
273✔
1418
    int j_surf = model::surface_map[periodic_pair.second];
273✔
1419
    Surface& surf1 {*model::surfaces[i_surf]};
273✔
1420
    Surface& surf2 {*model::surfaces[j_surf]};
273✔
1421

1422
    // Compute the dot product of the surface normals
1423
    Direction norm1 = surf1.normal({0, 0, 0});
273✔
1424
    Direction norm2 = surf2.normal({0, 0, 0});
273✔
1425
    norm1 /= norm1.norm();
273✔
1426
    norm2 /= norm2.norm();
273✔
1427
    double dot_prod = norm1.dot(norm2);
273✔
1428

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

1480
    // If albedo data is present in albedo map, set the boundary albedo.
1481
    if (albedo_map.count(surf1.id_)) {
273!
1482
      surf1.bc_->set_albedo(albedo_map[surf1.id_]);
×
1483
    }
1484
    if (albedo_map.count(surf2.id_)) {
273!
1485
      surf2.bc_->set_albedo(albedo_map[surf2.id_]);
273✔
1486
    }
1487
  }
1488
}
9,019✔
1489

1490
void free_memory_surfaces()
9,147✔
1491
{
1492
  model::surfaces.clear();
9,147✔
1493
  model::surface_map.clear();
9,147✔
1494
}
9,147✔
1495

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

© 2026 Coveralls, Inc