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

openmc-dev / openmc / 20368472998

19 Dec 2025 11:20AM UTC coverage: 82.143% (+0.004%) from 82.139%
20368472998

Pull #3566

github

web-flow
Merge a657bcb6c into a230b8612
Pull Request #3566: Resolve merge conflicts for PR #3516 - Implement Virtual Lattice Method for Efficient Simulation of Dispersed Fuels

17200 of 23810 branches covered (72.24%)

Branch coverage included in aggregate %.

266 of 322 new or added lines in 9 files covered. (82.61%)

2 existing lines in 1 file now uncovered.

55371 of 64537 relevant lines covered (85.8%)

43403485.97 hits per line

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

78.62
/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(
42,746✔
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");
42,746✔
42
  if (coeffs_file.size() != coeffs.size()) {
42,746!
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;
42,746✔
50
  for (auto c : coeffs) {
131,177✔
51
    *c = coeffs_file[i++];
88,431✔
52
  }
53
}
42,746✔
54

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

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

61
Surface::Surface(pugi::xml_node surf_node)
42,746✔
62
{
63
  if (check_for_node(surf_node, "id")) {
42,746!
64
    id_ = std::stoi(get_node_value(surf_node, "id"));
42,746✔
65
    if (contains(settings::source_write_surf_id, id_) ||
84,704✔
66
        settings::source_write_surf_id.empty()) {
41,958✔
67
      surf_source_ = true;
41,704✔
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")) {
42,746✔
74
    name_ = get_node_value(surf_node, "name", false);
9,544✔
75
  }
76

77
  if (check_for_node(surf_node, "boundary")) {
42,746✔
78
    std::string surf_bc = get_node_value(surf_node, "boundary", true, true);
24,224✔
79

80
    if (surf_bc == "transmission" || surf_bc == "transmit" || surf_bc.empty()) {
24,224!
81
      // Leave the bc_ a nullptr
82
    } else if (surf_bc == "vacuum") {
24,224✔
83
      bc_ = make_unique<VacuumBC>();
12,097✔
84
    } else if (surf_bc == "reflective" || surf_bc == "reflect" ||
12,585!
85
               surf_bc == "reflecting") {
458✔
86
      bc_ = make_unique<ReflectiveBC>();
11,669✔
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,224!
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,224✔
114
}
42,746✔
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,264✔
128
  }
129
  return f > 0.0;
2,147,483,647✔
130
}
131

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

138
  // Reflect direction according to normal.
139
  return u.reflect(n);
1,282,292,366✔
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
34,154✔
164
{
165
  hid_t surf_group = create_group(group_id, fmt::format("surface {}", id_));
68,308✔
166

167
  if (geom_type() == GeometryType::DAG) {
34,154!
168
    write_string(surf_group, "geom_type", "dagmc", false);
665!
169
  } else if (geom_type() == GeometryType::CSG) {
33,489!
170
    write_string(surf_group, "geom_type", "csg", false);
33,489✔
171

172
    if (bc_) {
33,489✔
173
      write_string(surf_group, "boundary_type", bc_->type(), false);
19,377✔
174
      bc_->to_hdf5(surf_group);
19,377✔
175

176
      // write periodic surface ID
177
      if (bc_->type() == "periodic") {
19,377✔
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);
14,112✔
190
    }
191
  }
192

193
  if (!name_.empty()) {
34,154✔
194
    write_string(surf_group, "name", name_, false);
7,331✔
195
  }
196

197
  to_hdf5_inner(surf_group);
34,154✔
198

199
  close_group(surf_group);
34,154✔
200
}
34,154✔
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;
632,109,359✔
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,065✔
225
{
226
  read_coeffs(surf_node, id_, {&x0_});
10,065✔
227
}
10,065✔
228

229
double SurfaceXPlane::evaluate(Position r) const
1,600,426,063✔
230
{
231
  return r.x - x0_;
1,600,426,063✔
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
278,007,835✔
240
{
241
  return {1., 0., 0.};
278,007,835✔
242
}
243

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

NEW
260
bool SurfaceXPlane::triso_in_mesh(
×
261
  vector<double> mesh_center, vector<double> lattice_pitch) const
262
{
NEW
263
  return false;
×
264
}
265

266
//==============================================================================
267
// SurfaceYPlane implementation
268
//==============================================================================
NEW
269
bool SurfaceYPlane::triso_in_mesh(
×
270
  vector<double> mesh_center, vector<double> lattice_pitch) const
271
{
NEW
272
  return false;
×
273
}
274
SurfaceYPlane::SurfaceYPlane(pugi::xml_node surf_node) : Surface(surf_node)
9,033✔
275
{
276
  read_coeffs(surf_node, id_, {&y0_});
9,033✔
277
}
9,033✔
278

279
double SurfaceYPlane::evaluate(Position r) const
1,589,424,927✔
280
{
281
  return r.y - y0_;
1,589,424,927✔
282
}
283

284
double SurfaceYPlane::distance(Position r, Direction u, bool coincident) const
2,147,483,647✔
285
{
286
  return axis_aligned_plane_distance<1>(r, u, coincident, y0_);
2,147,483,647✔
287
}
288

289
Direction SurfaceYPlane::normal(Position r) const
298,774,083✔
290
{
291
  return {0., 1., 0.};
298,774,083✔
292
}
293

294
void SurfaceYPlane::to_hdf5_inner(hid_t group_id) const
7,198✔
295
{
296
  write_string(group_id, "type", "y-plane", false);
7,198✔
297
  array<double, 1> coeffs {{y0_}};
7,198✔
298
  write_dataset(group_id, "coefficients", coeffs);
7,198✔
299
}
7,198✔
300

301
BoundingBox SurfaceYPlane::bounding_box(bool pos_side) const
242✔
302
{
303
  if (pos_side) {
242✔
304
    return {-INFTY, INFTY, y0_, INFTY, -INFTY, INFTY};
121✔
305
  } else {
306
    return {-INFTY, INFTY, -INFTY, y0_, -INFTY, INFTY};
121✔
307
  }
308
}
309

310
//==============================================================================
311
// SurfaceZPlane implementation
312
//==============================================================================
NEW
313
bool SurfaceZPlane::triso_in_mesh(
×
314
  vector<double> mesh_center, vector<double> lattice_pitch) const
315
{
NEW
316
  return false;
×
317
}
318
SurfaceZPlane::SurfaceZPlane(pugi::xml_node surf_node) : Surface(surf_node)
6,969✔
319
{
320
  read_coeffs(surf_node, id_, {&z0_});
6,969✔
321
}
6,969✔
322

323
double SurfaceZPlane::evaluate(Position r) const
497,797,228✔
324
{
325
  return r.z - z0_;
497,797,228✔
326
}
327

328
double SurfaceZPlane::distance(Position r, Direction u, bool coincident) const
2,147,483,647✔
329
{
330
  return axis_aligned_plane_distance<2>(r, u, coincident, z0_);
2,147,483,647✔
331
}
332

333
Direction SurfaceZPlane::normal(Position r) const
55,425,712✔
334
{
335
  return {0., 0., 1.};
55,425,712✔
336
}
337

338
void SurfaceZPlane::to_hdf5_inner(hid_t group_id) const
5,805✔
339
{
340
  write_string(group_id, "type", "z-plane", false);
5,805✔
341
  array<double, 1> coeffs {{z0_}};
5,805✔
342
  write_dataset(group_id, "coefficients", coeffs);
5,805✔
343
}
5,805✔
344

345
BoundingBox SurfaceZPlane::bounding_box(bool pos_side) const
×
346
{
347
  if (pos_side) {
×
348
    return {-INFTY, INFTY, -INFTY, INFTY, z0_, INFTY};
×
349
  } else {
350
    return {-INFTY, INFTY, -INFTY, INFTY, -INFTY, z0_};
×
351
  }
352
}
353

354
//==============================================================================
355
// SurfacePlane implementation
356
//==============================================================================
NEW
357
bool SurfacePlane::triso_in_mesh(
×
358
  vector<double> mesh_center, vector<double> lattice_pitch) const
359
{
NEW
360
  return false;
×
361
}
362

363
SurfacePlane::SurfacePlane(pugi::xml_node surf_node) : Surface(surf_node)
1,520✔
364
{
365
  read_coeffs(surf_node, id_, {&A_, &B_, &C_, &D_});
1,520✔
366
}
1,520✔
367

368
double SurfacePlane::evaluate(Position r) const
198,376,997✔
369
{
370
  return A_ * r.x + B_ * r.y + C_ * r.z - D_;
198,376,997✔
371
}
372

373
double SurfacePlane::distance(Position r, Direction u, bool coincident) const
575,934,810✔
374
{
375
  const double f = A_ * r.x + B_ * r.y + C_ * r.z - D_;
575,934,810✔
376
  const double projection = A_ * u.x + B_ * u.y + C_ * u.z;
575,934,810✔
377
  if (coincident || std::abs(f) < FP_COINCIDENT || projection == 0.0) {
575,934,810!
378
    return INFTY;
35,190,970✔
379
  } else {
380
    const double d = -f / projection;
540,743,840✔
381
    if (d < 0.0)
540,743,840✔
382
      return INFTY;
252,756,846✔
383
    return d;
287,986,994✔
384
  }
385
}
386

387
Direction SurfacePlane::normal(Position r) const
5,701,868✔
388
{
389
  return {A_, B_, C_};
5,701,868✔
390
}
391

392
void SurfacePlane::to_hdf5_inner(hid_t group_id) const
1,036✔
393
{
394
  write_string(group_id, "type", "plane", false);
1,036✔
395
  array<double, 4> coeffs {{A_, B_, C_, D_}};
1,036✔
396
  write_dataset(group_id, "coefficients", coeffs);
1,036✔
397
}
1,036✔
398

399
//==============================================================================
400
// Generic functions for x-, y-, and z-, cylinders
401
//==============================================================================
402

403
// The template parameters indicate the axes perpendicular to the axis of the
404
// cylinder.  offset1 and offset2 should correspond with i1 and i2,
405
// respectively.
406
template<int i1, int i2>
407
double axis_aligned_cylinder_evaluate(
1,452,405,073✔
408
  Position r, double offset1, double offset2, double radius)
409
{
410
  const double r1 = r.get<i1>() - offset1;
1,452,405,073✔
411
  const double r2 = r.get<i2>() - offset2;
1,452,405,073✔
412
  return r1 * r1 + r2 * r2 - radius * radius;
1,452,405,073✔
413
}
414

415
// The first template parameter indicates which axis the cylinder is aligned to.
416
// The other two parameters indicate the other two axes.  offset1 and offset2
417
// should correspond with i2 and i3, respectively.
418
template<int i1, int i2, int i3>
419
double axis_aligned_cylinder_distance(Position r, Direction u, bool coincident,
1,720,854,177✔
420
  double offset1, double offset2, double radius)
421
{
422
  const double a = 1.0 - u.get<i1>() * u.get<i1>(); // u^2 + v^2
1,720,854,177✔
423
  if (a == 0.0)
1,720,854,177✔
424
    return INFTY;
809,996✔
425

426
  const double r2 = r.get<i2>() - offset1;
1,720,044,181✔
427
  const double r3 = r.get<i3>() - offset2;
1,720,044,181✔
428
  const double k = r2 * u.get<i2>() + r3 * u.get<i3>();
1,720,044,181✔
429
  const double c = r2 * r2 + r3 * r3 - radius * radius;
1,720,044,181✔
430
  const double quad = k * k - a * c;
1,720,044,181✔
431

432
  if (quad < 0.0) {
1,720,044,181✔
433
    // No intersection with cylinder.
434
    return INFTY;
287,669,334✔
435

436
  } else if (coincident || std::abs(c) < FP_COINCIDENT) {
1,432,374,847✔
437
    // Particle is on the cylinder, thus one distance is positive/negative
438
    // and the other is zero. The sign of k determines if we are facing in or
439
    // out.
440
    if (k >= 0.0) {
665,730,816✔
441
      return INFTY;
333,948,780✔
442
    } else {
443
      return (-k + sqrt(quad)) / a;
331,782,036✔
444
    }
445

446
  } else if (c < 0.0) {
766,644,031✔
447
    // Particle is inside the cylinder, thus one distance must be negative
448
    // and one must be positive. The positive distance will be the one with
449
    // negative sign on sqrt(quad).
450
    return (-k + sqrt(quad)) / a;
301,468,609✔
451

452
  } else {
453
    // Particle is outside the cylinder, thus both distances are either
454
    // positive or negative. If positive, the smaller distance is the one
455
    // with positive sign on sqrt(quad).
456
    const double d = (-k - sqrt(quad)) / a;
465,175,422✔
457
    if (d < 0.0)
465,175,422✔
458
      return INFTY;
63,625,363✔
459
    return d;
401,550,059✔
460
  }
461
}
462

463
// The first template parameter indicates which axis the cylinder is aligned to.
464
// The other two parameters indicate the other two axes.  offset1 and offset2
465
// should correspond with i2 and i3, respectively.
466
template<int i1, int i2, int i3>
467
Direction axis_aligned_cylinder_normal(
1,409,879✔
468
  Position r, double offset1, double offset2)
469
{
470
  Direction u;
1,409,879✔
471
  u.get<i2>() = 2.0 * (r.get<i2>() - offset1);
1,409,879✔
472
  u.get<i3>() = 2.0 * (r.get<i3>() - offset2);
1,409,879✔
473
  u.get<i1>() = 0.0;
1,409,879✔
474
  return u;
1,409,879✔
475
}
476

477
//==============================================================================
478
// SurfaceXCylinder implementation
479
//==============================================================================
480

NEW
481
bool SurfaceXCylinder::triso_in_mesh(
×
482
  vector<double> mesh_center, vector<double> lattice_pitch) const
483
{
NEW
484
  return false;
×
485
}
486

487
SurfaceXCylinder::SurfaceXCylinder(pugi::xml_node surf_node)
48✔
488
  : Surface(surf_node)
48✔
489
{
490
  read_coeffs(surf_node, id_, {&y0_, &z0_, &radius_});
48✔
491
}
48✔
492

493
double SurfaceXCylinder::evaluate(Position r) const
1,526,134✔
494
{
495
  return axis_aligned_cylinder_evaluate<1, 2>(r, y0_, z0_, radius_);
1,526,134✔
496
}
497

498
double SurfaceXCylinder::distance(
1,746,932✔
499
  Position r, Direction u, bool coincident) const
500
{
501
  return axis_aligned_cylinder_distance<0, 1, 2>(
1,746,932✔
502
    r, u, coincident, y0_, z0_, radius_);
1,746,932✔
503
}
504

505
Direction SurfaceXCylinder::normal(Position r) const
×
506
{
507
  return axis_aligned_cylinder_normal<0, 1, 2>(r, y0_, z0_);
×
508
}
509

510
void SurfaceXCylinder::to_hdf5_inner(hid_t group_id) const
33✔
511
{
512
  write_string(group_id, "type", "x-cylinder", false);
33✔
513
  array<double, 3> coeffs {{y0_, z0_, radius_}};
33✔
514
  write_dataset(group_id, "coefficients", coeffs);
33✔
515
}
33✔
516

517
BoundingBox SurfaceXCylinder::bounding_box(bool pos_side) const
×
518
{
519
  if (!pos_side) {
×
520
    return {-INFTY, INFTY, y0_ - radius_, y0_ + radius_, z0_ - radius_,
×
521
      z0_ + radius_};
×
522
  } else {
523
    return {};
×
524
  }
525
}
526
//==============================================================================
527
// SurfaceYCylinder implementation
528
//==============================================================================
529

NEW
530
bool SurfaceYCylinder::triso_in_mesh(
×
531
  vector<double> mesh_center, vector<double> lattice_pitch) const
532
{
NEW
533
  return false;
×
534
}
535

536
SurfaceYCylinder::SurfaceYCylinder(pugi::xml_node surf_node)
16✔
537
  : Surface(surf_node)
16✔
538
{
539
  read_coeffs(surf_node, id_, {&x0_, &z0_, &radius_});
16✔
540
}
16✔
541

542
double SurfaceYCylinder::evaluate(Position r) const
170,984✔
543
{
544
  return axis_aligned_cylinder_evaluate<0, 2>(r, x0_, z0_, radius_);
170,984✔
545
}
546

547
double SurfaceYCylinder::distance(
659,769✔
548
  Position r, Direction u, bool coincident) const
549
{
550
  return axis_aligned_cylinder_distance<1, 0, 2>(
659,769✔
551
    r, u, coincident, x0_, z0_, radius_);
659,769✔
552
}
553

554
Direction SurfaceYCylinder::normal(Position r) const
×
555
{
556
  return axis_aligned_cylinder_normal<1, 0, 2>(r, x0_, z0_);
×
557
}
558

559
void SurfaceYCylinder::to_hdf5_inner(hid_t group_id) const
11✔
560
{
561
  write_string(group_id, "type", "y-cylinder", false);
11✔
562
  array<double, 3> coeffs {{x0_, z0_, radius_}};
11✔
563
  write_dataset(group_id, "coefficients", coeffs);
11✔
564
}
11✔
565

566
BoundingBox SurfaceYCylinder::bounding_box(bool pos_side) const
×
567
{
568
  if (!pos_side) {
×
569
    return {x0_ - radius_, x0_ + radius_, -INFTY, INFTY, z0_ - radius_,
×
570
      z0_ + radius_};
×
571
  } else {
572
    return {};
×
573
  }
574
}
575

576
//==============================================================================
577
// SurfaceZCylinder implementation
578
//==============================================================================
579

NEW
580
bool SurfaceZCylinder::triso_in_mesh(
×
581
  vector<double> mesh_center, vector<double> lattice_pitch) const
582
{
NEW
583
  return false;
×
584
}
585

586
SurfaceZCylinder::SurfaceZCylinder(pugi::xml_node surf_node)
4,840✔
587
  : Surface(surf_node)
4,840✔
588
{
589
  read_coeffs(surf_node, id_, {&x0_, &y0_, &radius_});
4,840✔
590
}
4,840✔
591

592
double SurfaceZCylinder::evaluate(Position r) const
1,450,707,955✔
593
{
594
  return axis_aligned_cylinder_evaluate<0, 1>(r, x0_, y0_, radius_);
1,450,707,955✔
595
}
596

597
double SurfaceZCylinder::distance(
1,718,447,476✔
598
  Position r, Direction u, bool coincident) const
599
{
600
  return axis_aligned_cylinder_distance<2, 0, 1>(
1,718,447,476✔
601
    r, u, coincident, x0_, y0_, radius_);
1,718,447,476✔
602
}
603

604
Direction SurfaceZCylinder::normal(Position r) const
1,409,879✔
605
{
606
  return axis_aligned_cylinder_normal<2, 0, 1>(r, x0_, y0_);
1,409,879✔
607
}
608

609
void SurfaceZCylinder::to_hdf5_inner(hid_t group_id) const
3,579✔
610
{
611
  write_string(group_id, "type", "z-cylinder", false);
3,579✔
612
  array<double, 3> coeffs {{x0_, y0_, radius_}};
3,579✔
613
  write_dataset(group_id, "coefficients", coeffs);
3,579✔
614
}
3,579✔
615

616
BoundingBox SurfaceZCylinder::bounding_box(bool pos_side) const
44✔
617
{
618
  if (!pos_side) {
44✔
619
    return {x0_ - radius_, x0_ + radius_, y0_ - radius_, y0_ + radius_, -INFTY,
22✔
620
      INFTY};
22✔
621
  } else {
622
    return {};
22✔
623
  }
624
}
625

626
//==============================================================================
627
// SurfaceSphere implementation
628
//==============================================================================
629

630
SurfaceSphere::SurfaceSphere(pugi::xml_node surf_node) : Surface(surf_node)
9,995✔
631
{
632
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &radius_});
9,995✔
633
}
9,995✔
634

635
double SurfaceSphere::evaluate(Position r) const
590,573,248✔
636
{
637
  const double x = r.x - x0_;
590,573,248✔
638
  const double y = r.y - y0_;
590,573,248✔
639
  const double z = r.z - z0_;
590,573,248✔
640
  return x * x + y * y + z * z - radius_ * radius_;
590,573,248✔
641
}
642

643
double SurfaceSphere::distance(Position r, Direction u, bool coincident) const
780,256,479✔
644
{
645
  const double x = r.x - x0_;
780,256,479✔
646
  const double y = r.y - y0_;
780,256,479✔
647
  const double z = r.z - z0_;
780,256,479✔
648
  const double k = x * u.x + y * u.y + z * u.z;
780,256,479✔
649
  const double c = x * x + y * y + z * z - radius_ * radius_;
780,256,479✔
650
  const double quad = k * k - c;
780,256,479✔
651

652
  if (quad < 0.0) {
780,256,479✔
653
    // No intersection with sphere.
654
    return INFTY;
185,411,687✔
655

656
  } else if (coincident || std::abs(c) < FP_COINCIDENT) {
594,844,792!
657
    // Particle is on the sphere, thus one distance is positive/negative and
658
    // the other is zero. The sign of k determines if we are facing in or out.
659
    if (k >= 0.0) {
111,301,168✔
660
      return INFTY;
48,106,623✔
661
    } else {
662
      return -k + sqrt(quad);
63,194,545✔
663
    }
664

665
  } else if (c < 0.0) {
483,543,624✔
666
    // Particle is inside the sphere, thus one distance must be negative and
667
    // one must be positive. The positive distance will be the one with
668
    // negative sign on sqrt(quad)
669
    return -k + sqrt(quad);
423,339,072✔
670

671
  } else {
672
    // Particle is outside the sphere, thus both distances are either positive
673
    // or negative. If positive, the smaller distance is the one with positive
674
    // sign on sqrt(quad).
675
    const double d = -k - sqrt(quad);
60,204,552✔
676
    if (d < 0.0)
60,204,552✔
677
      return INFTY;
10,915,774✔
678
    return d;
49,288,778✔
679
  }
680
}
681

682
Direction SurfaceSphere::normal(Position r) const
21,468,436✔
683
{
684
  return {2.0 * (r.x - x0_), 2.0 * (r.y - y0_), 2.0 * (r.z - z0_)};
21,468,436✔
685
}
686

687
void SurfaceSphere::to_hdf5_inner(hid_t group_id) const
7,718✔
688
{
689
  write_string(group_id, "type", "sphere", false);
7,718✔
690
  array<double, 4> coeffs {{x0_, y0_, z0_, radius_}};
7,718✔
691
  write_dataset(group_id, "coefficients", coeffs);
7,718✔
692
}
7,718✔
693

694
BoundingBox SurfaceSphere::bounding_box(bool pos_side) const
×
695
{
696
  if (!pos_side) {
×
697
    return {x0_ - radius_, x0_ + radius_, y0_ - radius_, y0_ + radius_,
×
698
      z0_ - radius_, z0_ + radius_};
×
699
  } else {
700
    return {};
×
701
  }
702
}
703

704
void SurfaceSphere::connect_to_triso_base(int triso_index, std::string key)
4,176✔
705
{
706
  if (key == "base") {
4,176✔
707
    triso_base_index_ = triso_index;
2,576✔
708
    is_triso_surface_ = true;
2,576✔
709
  } else if (key == "particle") {
1,600!
710
    triso_particle_index_ = triso_index;
1,600✔
711
  }
712
}
4,176✔
713

714
vector<double> SurfaceSphere::get_center() const
18,014,551✔
715
{
716
  return {x0_, y0_, z0_};
18,014,551✔
717
}
718

719
double SurfaceSphere::get_radius() const
18,012,951✔
720
{
721
  return radius_;
18,012,951✔
722
}
723

724
bool SurfaceSphere::triso_in_mesh(
20,416✔
725
  vector<double> mesh_center, vector<double> lattice_pitch) const
726
{
727
  double dis_x;
728
  double dis_y;
729
  double dis_z;
730
  double x_min = mesh_center[0] - lattice_pitch[0] / 2;
20,416✔
731
  double x_max = mesh_center[0] + lattice_pitch[0] / 2;
20,416✔
732
  double y_min = mesh_center[1] - lattice_pitch[1] / 2;
20,416✔
733
  double y_max = mesh_center[1] + lattice_pitch[1] / 2;
20,416✔
734
  double z_min = mesh_center[2] - lattice_pitch[2] / 2;
20,416✔
735
  double z_max = mesh_center[2] + lattice_pitch[2] / 2;
20,416✔
736
  if (x0_ >= x_min && x0_ <= x_max) {
20,416✔
737
    dis_x = 0;
8,672✔
738
  } else if (x0_ < x_min) {
11,744✔
739
    dis_x = pow(x_min - x0_, 2);
6,208✔
740
  } else {
741
    dis_x = pow(x_max - x0_, 2);
5,536✔
742
  }
743

744
  if (y0_ >= y_min && y0_ <= y_max) {
20,416✔
745
    dis_y = 0;
8,800✔
746
  } else if (y0_ < y_min) {
11,616✔
747
    dis_y = pow(y_min - y0_, 2);
5,632✔
748
  } else {
749
    dis_y = pow(y_max - y0_, 2);
5,984✔
750
  }
751

752
  if (z0_ >= z_min && z0_ <= z_max) {
20,416✔
753
    dis_z = 0;
8,736✔
754
  } else if (z0_ < z_min) {
11,680✔
755
    dis_z = pow(z_min - z0_, 2);
6,352✔
756
  } else {
757
    dis_z = pow(z_max - z0_, 2);
5,328✔
758
  }
759

760
  if (sqrt(dis_x + dis_y + dis_z) < radius_) {
20,416✔
761
    return true;
2,576✔
762
  } else {
763
    return false;
17,840✔
764
  }
765
}
766

767
//==============================================================================
768
// Generic functions for x-, y-, and z-, cones
769
//==============================================================================
770

771
// The first template parameter indicates which axis the cone is aligned to.
772
// The other two parameters indicate the other two axes.  offset1, offset2,
773
// and offset3 should correspond with i1, i2, and i3, respectively.
774
template<int i1, int i2, int i3>
775
double axis_aligned_cone_evaluate(
325,853✔
776
  Position r, double offset1, double offset2, double offset3, double radius_sq)
777
{
778
  const double r1 = r.get<i1>() - offset1;
325,853✔
779
  const double r2 = r.get<i2>() - offset2;
325,853✔
780
  const double r3 = r.get<i3>() - offset3;
325,853✔
781
  return r2 * r2 + r3 * r3 - radius_sq * r1 * r1;
325,853✔
782
}
783

784
// The first template parameter indicates which axis the cone is aligned to.
785
// The other two parameters indicate the other two axes.  offset1, offset2,
786
// and offset3 should correspond with i1, i2, and i3, respectively.
787
template<int i1, int i2, int i3>
788
double axis_aligned_cone_distance(Position r, Direction u, bool coincident,
978,021✔
789
  double offset1, double offset2, double offset3, double radius_sq)
790
{
791
  const double r1 = r.get<i1>() - offset1;
978,021✔
792
  const double r2 = r.get<i2>() - offset2;
978,021✔
793
  const double r3 = r.get<i3>() - offset3;
978,021✔
794
  const double a = u.get<i2>() * u.get<i2>() + u.get<i3>() * u.get<i3>() -
978,021✔
795
                   radius_sq * u.get<i1>() * u.get<i1>();
978,021✔
796
  const double k =
978,021✔
797
    r2 * u.get<i2>() + r3 * u.get<i3>() - radius_sq * r1 * u.get<i1>();
978,021✔
798
  const double c = r2 * r2 + r3 * r3 - radius_sq * r1 * r1;
978,021✔
799
  double quad = k * k - a * c;
978,021✔
800

801
  double d;
802

803
  if (quad < 0.0) {
978,021!
804
    // No intersection with cone.
805
    return INFTY;
×
806

807
  } else if (coincident || std::abs(c) < FP_COINCIDENT) {
978,021!
808
    // Particle is on the cone, thus one distance is positive/negative
809
    // and the other is zero. The sign of k determines if we are facing in or
810
    // out.
811
    if (k >= 0.0) {
59,510!
812
      d = (-k - sqrt(quad)) / a;
×
813
    } else {
814
      d = (-k + sqrt(quad)) / a;
59,510✔
815
    }
816

817
  } else {
818
    // Calculate both solutions to the quadratic.
819
    quad = sqrt(quad);
918,511✔
820
    d = (-k - quad) / a;
918,511✔
821
    const double b = (-k + quad) / a;
918,511✔
822

823
    // Determine the smallest positive solution.
824
    if (d < 0.0) {
918,511✔
825
      if (b > 0.0)
780,043✔
826
        d = b;
665,104✔
827
    } else {
828
      if (b > 0.0) {
138,468!
829
        if (b < d)
138,468!
830
          d = b;
138,468✔
831
      }
832
    }
833
  }
834

835
  // If the distance was negative, set boundary distance to infinity.
836
  if (d <= 0.0)
978,021✔
837
    return INFTY;
136,422✔
838
  return d;
841,599✔
839
}
840

841
// The first template parameter indicates which axis the cone is aligned to.
842
// The other two parameters indicate the other two axes.  offset1, offset2,
843
// and offset3 should correspond with i1, i2, and i3, respectively.
844
template<int i1, int i2, int i3>
845
Direction axis_aligned_cone_normal(
59,510✔
846
  Position r, double offset1, double offset2, double offset3, double radius_sq)
847
{
848
  Direction u;
59,510✔
849
  u.get<i1>() = -2.0 * radius_sq * (r.get<i1>() - offset1);
59,510✔
850
  u.get<i2>() = 2.0 * (r.get<i2>() - offset2);
59,510✔
851
  u.get<i3>() = 2.0 * (r.get<i3>() - offset3);
59,510✔
852
  return u;
59,510✔
853
}
854

855
//==============================================================================
856
// SurfaceXCone implementation
857
//==============================================================================
NEW
858
bool SurfaceXCone::triso_in_mesh(
×
859
  vector<double> mesh_center, vector<double> lattice_pitch) const
860
{
NEW
861
  return false;
×
862
}
UNCOV
863
SurfaceXCone::SurfaceXCone(pugi::xml_node surf_node) : Surface(surf_node)
×
864
{
865
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &radius_sq_});
×
866
}
×
867

868
double SurfaceXCone::evaluate(Position r) const
×
869
{
870
  return axis_aligned_cone_evaluate<0, 1, 2>(r, x0_, y0_, z0_, radius_sq_);
×
871
}
872

873
double SurfaceXCone::distance(Position r, Direction u, bool coincident) const
×
874
{
875
  return axis_aligned_cone_distance<0, 1, 2>(
×
876
    r, u, coincident, x0_, y0_, z0_, radius_sq_);
×
877
}
878

879
Direction SurfaceXCone::normal(Position r) const
×
880
{
881
  return axis_aligned_cone_normal<0, 1, 2>(r, x0_, y0_, z0_, radius_sq_);
×
882
}
883

884
void SurfaceXCone::to_hdf5_inner(hid_t group_id) const
×
885
{
886
  write_string(group_id, "type", "x-cone", false);
×
887
  array<double, 4> coeffs {{x0_, y0_, z0_, radius_sq_}};
×
888
  write_dataset(group_id, "coefficients", coeffs);
×
889
}
×
890

891
//==============================================================================
892
// SurfaceYCone implementation
893
//==============================================================================
NEW
894
bool SurfaceYCone::triso_in_mesh(
×
895
  vector<double> mesh_center, vector<double> lattice_pitch) const
896
{
NEW
897
  return false;
×
898
}
UNCOV
899
SurfaceYCone::SurfaceYCone(pugi::xml_node surf_node) : Surface(surf_node)
×
900
{
901
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &radius_sq_});
×
902
}
×
903

904
double SurfaceYCone::evaluate(Position r) const
×
905
{
906
  return axis_aligned_cone_evaluate<1, 0, 2>(r, y0_, x0_, z0_, radius_sq_);
×
907
}
908

909
double SurfaceYCone::distance(Position r, Direction u, bool coincident) const
×
910
{
911
  return axis_aligned_cone_distance<1, 0, 2>(
×
912
    r, u, coincident, y0_, x0_, z0_, radius_sq_);
×
913
}
914

915
Direction SurfaceYCone::normal(Position r) const
×
916
{
917
  return axis_aligned_cone_normal<1, 0, 2>(r, y0_, x0_, z0_, radius_sq_);
×
918
}
919

920
void SurfaceYCone::to_hdf5_inner(hid_t group_id) const
×
921
{
922
  write_string(group_id, "type", "y-cone", false);
×
923
  array<double, 4> coeffs {{x0_, y0_, z0_, radius_sq_}};
×
924
  write_dataset(group_id, "coefficients", coeffs);
×
925
}
×
926

927
//==============================================================================
928
// SurfaceZCone implementation
929
//==============================================================================
NEW
930
bool SurfaceZCone::triso_in_mesh(
×
931
  vector<double> mesh_center, vector<double> lattice_pitch) const
932
{
NEW
933
  return false;
×
934
}
935

936
SurfaceZCone::SurfaceZCone(pugi::xml_node surf_node) : Surface(surf_node)
16✔
937
{
938
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &radius_sq_});
16✔
939
}
16✔
940

941
double SurfaceZCone::evaluate(Position r) const
325,853✔
942
{
943
  return axis_aligned_cone_evaluate<2, 0, 1>(r, z0_, x0_, y0_, radius_sq_);
325,853✔
944
}
945

946
double SurfaceZCone::distance(Position r, Direction u, bool coincident) const
978,021✔
947
{
948
  return axis_aligned_cone_distance<2, 0, 1>(
978,021✔
949
    r, u, coincident, z0_, x0_, y0_, radius_sq_);
978,021✔
950
}
951

952
Direction SurfaceZCone::normal(Position r) const
59,510✔
953
{
954
  return axis_aligned_cone_normal<2, 0, 1>(r, z0_, x0_, y0_, radius_sq_);
59,510✔
955
}
956

957
void SurfaceZCone::to_hdf5_inner(hid_t group_id) const
11✔
958
{
959
  write_string(group_id, "type", "z-cone", false);
11✔
960
  array<double, 4> coeffs {{x0_, y0_, z0_, radius_sq_}};
11✔
961
  write_dataset(group_id, "coefficients", coeffs);
11✔
962
}
11✔
963

964
//==============================================================================
965
// SurfaceQuadric implementation
966
//==============================================================================
NEW
967
bool SurfaceQuadric::triso_in_mesh(
×
968
  vector<double> mesh_center, vector<double> lattice_pitch) const
969
{
NEW
970
  return false;
×
971
}
972
SurfaceQuadric::SurfaceQuadric(pugi::xml_node surf_node) : Surface(surf_node)
16✔
973
{
974
  read_coeffs(
×
975
    surf_node, id_, {&A_, &B_, &C_, &D_, &E_, &F_, &G_, &H_, &J_, &K_});
16✔
976
}
16✔
977

978
double SurfaceQuadric::evaluate(Position r) const
179,786✔
979
{
980
  const double x = r.x;
179,786✔
981
  const double y = r.y;
179,786✔
982
  const double z = r.z;
179,786✔
983
  return x * (A_ * x + D_ * y + G_) + y * (B_ * y + E_ * z + H_) +
179,786✔
984
         z * (C_ * z + F_ * x + J_) + K_;
179,786✔
985
}
986

987
double SurfaceQuadric::distance(
316,679✔
988
  Position r, Direction ang, bool coincident) const
989
{
990
  const double& x = r.x;
316,679✔
991
  const double& y = r.y;
316,679✔
992
  const double& z = r.z;
316,679✔
993
  const double& u = ang.x;
316,679✔
994
  const double& v = ang.y;
316,679✔
995
  const double& w = ang.z;
316,679✔
996

997
  const double a =
316,679✔
998
    A_ * u * u + B_ * v * v + C_ * w * w + D_ * u * v + E_ * v * w + F_ * u * w;
316,679✔
999
  const double k = A_ * u * x + B_ * v * y + C_ * w * z +
316,679✔
1000
                   0.5 * (D_ * (u * y + v * x) + E_ * (v * z + w * y) +
316,679✔
1001
                           F_ * (w * x + u * z) + G_ * u + H_ * v + J_ * w);
316,679✔
1002
  const double c = A_ * x * x + B_ * y * y + C_ * z * z + D_ * x * y +
316,679✔
1003
                   E_ * y * z + F_ * x * z + G_ * x + H_ * y + J_ * z + K_;
316,679✔
1004
  double quad = k * k - a * c;
316,679✔
1005

1006
  double d;
1007

1008
  if (quad < 0.0) {
316,679!
1009
    // No intersection with surface.
1010
    return INFTY;
×
1011

1012
  } else if (coincident || std::abs(c) < FP_COINCIDENT) {
316,679!
1013
    // Particle is on the surface, thus one distance is positive/negative and
1014
    // the other is zero. The sign of k determines which distance is zero and
1015
    // which is not. Additionally, if a is zero, it means the particle is on
1016
    // a plane-like surface.
1017
    if (a == 0.0) {
36,058!
1018
      d = INFTY; // see the below explanation
×
1019
    } else if (k >= 0.0) {
36,058!
1020
      d = (-k - sqrt(quad)) / a;
×
1021
    } else {
1022
      d = (-k + sqrt(quad)) / a;
36,058✔
1023
    }
1024

1025
  } else if (a == 0.0) {
280,621!
1026
    // Given the orientation of the particle, the quadric looks like a plane in
1027
    // this case, and thus we have only one solution despite potentially having
1028
    // quad > 0.0. While the term under the square root may be real, in one
1029
    // case of the +/- of the quadratic formula, 0/0 results, and in another, a
1030
    // finite value over 0 results. Applying L'Hopital's to the 0/0 case gives
1031
    // the below. Alternatively this can be found by simply putting a=0 in the
1032
    // equation ax^2 + bx + c = 0.
1033
    d = -0.5 * c / k;
×
1034
  } else {
1035
    // Calculate both solutions to the quadratic.
1036
    quad = sqrt(quad);
280,621✔
1037
    d = (-k - quad) / a;
280,621✔
1038
    double b = (-k + quad) / a;
280,621✔
1039

1040
    // Determine the smallest positive solution.
1041
    if (d < 0.0) {
280,621!
1042
      if (b > 0.0)
280,621!
1043
        d = b;
280,621✔
1044
    } else {
1045
      if (b > 0.0) {
×
1046
        if (b < d)
×
1047
          d = b;
×
1048
      }
1049
    }
1050
  }
1051

1052
  // If the distance was negative, set boundary distance to infinity.
1053
  if (d <= 0.0)
316,679!
1054
    return INFTY;
×
1055
  return d;
316,679✔
1056
}
1057

1058
Direction SurfaceQuadric::normal(Position r) const
36,058✔
1059
{
1060
  const double& x = r.x;
36,058✔
1061
  const double& y = r.y;
36,058✔
1062
  const double& z = r.z;
36,058✔
1063
  return {2.0 * A_ * x + D_ * y + F_ * z + G_,
36,058✔
1064
    2.0 * B_ * y + D_ * x + E_ * z + H_, 2.0 * C_ * z + E_ * y + F_ * x + J_};
36,058✔
1065
}
1066

1067
void SurfaceQuadric::to_hdf5_inner(hid_t group_id) const
11✔
1068
{
1069
  write_string(group_id, "type", "quadric", false);
11✔
1070
  array<double, 10> coeffs {{A_, B_, C_, D_, E_, F_, G_, H_, J_, K_}};
11✔
1071
  write_dataset(group_id, "coefficients", coeffs);
11✔
1072
}
11✔
1073

1074
//==============================================================================
1075
// Torus helper functions
1076
//==============================================================================
1077

1078
double torus_distance(double x1, double x2, double x3, double u1, double u2,
26,534,563✔
1079
  double u3, double A, double B, double C, bool coincident)
1080
{
1081
  // Coefficients for equation: (c2 t^2 + c1 t + c0)^2 = c2' t^2 + c1' t + c0'
1082
  double D = (C * C) / (B * B);
26,534,563✔
1083
  double c2 = u1 * u1 + u2 * u2 + D * u3 * u3;
26,534,563✔
1084
  double c1 = 2 * (u1 * x1 + u2 * x2 + D * u3 * x3);
26,534,563✔
1085
  double c0 = x1 * x1 + x2 * x2 + D * x3 * x3 + A * A - C * C;
26,534,563✔
1086
  double four_A2 = 4 * A * A;
26,534,563✔
1087
  double c2p = four_A2 * (u1 * u1 + u2 * u2);
26,534,563✔
1088
  double c1p = 2 * four_A2 * (u1 * x1 + u2 * x2);
26,534,563✔
1089
  double c0p = four_A2 * (x1 * x1 + x2 * x2);
26,534,563✔
1090

1091
  // Coefficient for equation: a t^4 + b t^3 + c t^2 + d t + e = 0. If the point
1092
  // is coincident, the 'e' coefficient should be zero. Explicitly setting it to
1093
  // zero helps avoid numerical issues below with root finding.
1094
  double coeff[5];
1095
  coeff[0] = coincident ? 0.0 : c0 * c0 - c0p;
26,534,563✔
1096
  coeff[1] = 2 * c0 * c1 - c1p;
26,534,563✔
1097
  coeff[2] = c1 * c1 + 2 * c0 * c2 - c2p;
26,534,563✔
1098
  coeff[3] = 2 * c1 * c2;
26,534,563✔
1099
  coeff[4] = c2 * c2;
26,534,563✔
1100

1101
  std::complex<double> roots[4];
26,534,563✔
1102
  oqs::quartic_solver(coeff, roots);
26,534,563✔
1103

1104
  // Find smallest positive, real root. In the case where the particle is
1105
  // coincident with the surface, we are sure to have one root very close to
1106
  // zero but possibly small and positive. A tolerance is set to discard that
1107
  // zero.
1108
  double distance = INFTY;
26,534,563✔
1109
  double cutoff = coincident ? TORUS_TOL : 0.0;
26,534,563✔
1110
  for (int i = 0; i < 4; ++i) {
132,672,815✔
1111
    if (roots[i].imag() == 0) {
106,138,252✔
1112
      double root = roots[i].real();
25,944,028✔
1113
      if (root > cutoff && root < distance) {
25,944,028✔
1114
        // Avoid roots corresponding to internal surfaces
1115
        double s1 = x1 + u1 * root;
10,411,995✔
1116
        double s2 = x2 + u2 * root;
10,411,995✔
1117
        double s3 = x3 + u3 * root;
10,411,995✔
1118
        double check = D * s3 * s3 + s1 * s1 + s2 * s2 + A * A - C * C;
10,411,995✔
1119
        if (check >= 0) {
10,411,995!
1120
          distance = root;
10,411,995✔
1121
        }
1122
      }
1123
    }
1124
  }
1125
  return distance;
26,534,563✔
1126
}
1127

1128
//==============================================================================
1129
// SurfaceXTorus implementation
1130
//==============================================================================
NEW
1131
bool SurfaceXTorus::triso_in_mesh(
×
1132
  vector<double> mesh_center, vector<double> lattice_pitch) const
1133
{
NEW
1134
  return false;
×
1135
}
1136
SurfaceXTorus::SurfaceXTorus(pugi::xml_node surf_node) : Surface(surf_node)
60✔
1137
{
1138
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &A_, &B_, &C_});
60✔
1139
}
60✔
1140

1141
void SurfaceXTorus::to_hdf5_inner(hid_t group_id) const
55✔
1142
{
1143
  write_string(group_id, "type", "x-torus", false);
55✔
1144
  std::array<double, 6> coeffs {{x0_, y0_, z0_, A_, B_, C_}};
55✔
1145
  write_dataset(group_id, "coefficients", coeffs);
55✔
1146
}
55✔
1147

1148
double SurfaceXTorus::evaluate(Position r) const
811,272✔
1149
{
1150
  double x = r.x - x0_;
811,272✔
1151
  double y = r.y - y0_;
811,272✔
1152
  double z = r.z - z0_;
811,272✔
1153
  return (x * x) / (B_ * B_) +
1,622,544✔
1154
         std::pow(std::sqrt(y * y + z * z) - A_, 2) / (C_ * C_) - 1.;
811,272✔
1155
}
1156

1157
double SurfaceXTorus::distance(Position r, Direction u, bool coincident) const
8,348,615✔
1158
{
1159
  double x = r.x - x0_;
8,348,615✔
1160
  double y = r.y - y0_;
8,348,615✔
1161
  double z = r.z - z0_;
8,348,615✔
1162
  return torus_distance(y, z, x, u.y, u.z, u.x, A_, B_, C_, coincident);
8,348,615✔
1163
}
1164

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

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

1185
//==============================================================================
1186
// SurfaceYTorus implementation
1187
//==============================================================================
NEW
1188
bool SurfaceYTorus::triso_in_mesh(
×
1189
  vector<double> mesh_center, vector<double> lattice_pitch) const
1190
{
NEW
1191
  return false;
×
1192
}
1193
SurfaceYTorus::SurfaceYTorus(pugi::xml_node surf_node) : Surface(surf_node)
60✔
1194
{
1195
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &A_, &B_, &C_});
60✔
1196
}
60✔
1197

1198
void SurfaceYTorus::to_hdf5_inner(hid_t group_id) const
55✔
1199
{
1200
  write_string(group_id, "type", "y-torus", false);
55✔
1201
  std::array<double, 6> coeffs {{x0_, y0_, z0_, A_, B_, C_}};
55✔
1202
  write_dataset(group_id, "coefficients", coeffs);
55✔
1203
}
55✔
1204

1205
double SurfaceYTorus::evaluate(Position r) const
777,539✔
1206
{
1207
  double x = r.x - x0_;
777,539✔
1208
  double y = r.y - y0_;
777,539✔
1209
  double z = r.z - z0_;
777,539✔
1210
  return (y * y) / (B_ * B_) +
1,555,078✔
1211
         std::pow(std::sqrt(x * x + z * z) - A_, 2) / (C_ * C_) - 1.;
777,539✔
1212
}
1213

1214
double SurfaceYTorus::distance(Position r, Direction u, bool coincident) const
8,413,900✔
1215
{
1216
  double x = r.x - x0_;
8,413,900✔
1217
  double y = r.y - y0_;
8,413,900✔
1218
  double z = r.z - z0_;
8,413,900✔
1219
  return torus_distance(x, z, y, u.x, u.z, u.y, A_, B_, C_, coincident);
8,413,900✔
1220
}
1221

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

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

1242
//==============================================================================
1243
// SurfaceZTorus implementation
1244
//==============================================================================
NEW
1245
bool SurfaceZTorus::triso_in_mesh(
×
1246
  vector<double> mesh_center, vector<double> lattice_pitch) const
1247
{
NEW
1248
  return false;
×
1249
}
1250

1251
SurfaceZTorus::SurfaceZTorus(pugi::xml_node surf_node) : Surface(surf_node)
108✔
1252
{
1253
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &A_, &B_, &C_});
108✔
1254
}
108✔
1255

1256
void SurfaceZTorus::to_hdf5_inner(hid_t group_id) const
88✔
1257
{
1258
  write_string(group_id, "type", "z-torus", false);
88✔
1259
  std::array<double, 6> coeffs {{x0_, y0_, z0_, A_, B_, C_}};
88✔
1260
  write_dataset(group_id, "coefficients", coeffs);
88✔
1261
}
88✔
1262

1263
double SurfaceZTorus::evaluate(Position r) const
1,308,998✔
1264
{
1265
  double x = r.x - x0_;
1,308,998✔
1266
  double y = r.y - y0_;
1,308,998✔
1267
  double z = r.z - z0_;
1,308,998✔
1268
  return (z * z) / (B_ * B_) +
2,617,996✔
1269
         std::pow(std::sqrt(x * x + y * y) - A_, 2) / (C_ * C_) - 1.;
1,308,998✔
1270
}
1271

1272
double SurfaceZTorus::distance(Position r, Direction u, bool coincident) const
9,772,048✔
1273
{
1274
  double x = r.x - x0_;
9,772,048✔
1275
  double y = r.y - y0_;
9,772,048✔
1276
  double z = r.z - z0_;
9,772,048✔
1277
  return torus_distance(x, y, z, u.x, u.y, u.z, A_, B_, C_, coincident);
9,772,048✔
1278
}
1279

1280
Direction SurfaceZTorus::normal(Position r) const
×
1281
{
1282
  // reduce the expansion of the full form for torus
1283
  double x = r.x - x0_;
×
1284
  double y = r.y - y0_;
×
1285
  double z = r.z - z0_;
×
1286

1287
  // f(x,y,z) = z^2/B^2 + (sqrt(x^2 + y^2) - A)^2/C^2 - 1
1288
  // ∂f/∂x = 2x(g - A)/(g*C^2) where g = sqrt(x^2 + y^2)
1289
  // ∂f/∂y = 2y(g - A)/(g*C^2)
1290
  // ∂f/∂z = 2z/B^2
1291
  // Multiplying by g*C^2*B^2 / 2 gives:
1292
  double g = std::sqrt(x * x + y * y);
×
1293
  double nx = x * (g - A_) * B_ * B_;
×
1294
  double ny = y * (g - A_) * B_ * B_;
×
1295
  double nz = C_ * C_ * g * z;
×
1296
  Position n(nx, ny, nz);
×
1297
  return n / n.norm();
×
1298
}
1299

1300
//==============================================================================
1301

1302
void read_surfaces(pugi::xml_node node)
7,605✔
1303
{
1304
  // Count the number of surfaces
1305
  int n_surfaces = 0;
7,605✔
1306
  for (pugi::xml_node surf_node : node.children("surface")) {
50,351✔
1307
    n_surfaces++;
42,746✔
1308
  }
1309

1310
  // Loop over XML surface elements and populate the array.  Keep track of
1311
  // periodic surfaces and their albedos.
1312
  model::surfaces.reserve(n_surfaces);
7,605✔
1313
  std::set<std::pair<int, int>> periodic_pairs;
7,605✔
1314
  std::unordered_map<int, double> albedo_map;
7,605✔
1315
  {
1316
    pugi::xml_node surf_node;
7,605✔
1317
    int i_surf;
1318
    for (surf_node = node.child("surface"), i_surf = 0; surf_node;
50,351✔
1319
         surf_node = surf_node.next_sibling("surface"), i_surf++) {
42,746✔
1320
      std::string surf_type = get_node_value(surf_node, "type", true, true);
42,746✔
1321

1322
      // Allocate and initialize the new surface
1323

1324
      if (surf_type == "x-plane") {
42,746✔
1325
        model::surfaces.push_back(make_unique<SurfaceXPlane>(surf_node));
10,065✔
1326

1327
      } else if (surf_type == "y-plane") {
32,681✔
1328
        model::surfaces.push_back(make_unique<SurfaceYPlane>(surf_node));
9,033✔
1329

1330
      } else if (surf_type == "z-plane") {
23,648✔
1331
        model::surfaces.push_back(make_unique<SurfaceZPlane>(surf_node));
6,969✔
1332

1333
      } else if (surf_type == "plane") {
16,679✔
1334
        model::surfaces.push_back(make_unique<SurfacePlane>(surf_node));
1,520✔
1335

1336
      } else if (surf_type == "x-cylinder") {
15,159✔
1337
        model::surfaces.push_back(make_unique<SurfaceXCylinder>(surf_node));
48✔
1338

1339
      } else if (surf_type == "y-cylinder") {
15,111✔
1340
        model::surfaces.push_back(make_unique<SurfaceYCylinder>(surf_node));
16✔
1341

1342
      } else if (surf_type == "z-cylinder") {
15,095✔
1343
        model::surfaces.push_back(make_unique<SurfaceZCylinder>(surf_node));
4,840✔
1344

1345
      } else if (surf_type == "sphere") {
10,255✔
1346
        model::surfaces.push_back(make_unique<SurfaceSphere>(surf_node));
9,995✔
1347

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

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

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

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

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

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

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

1369
      } else {
1370
        fatal_error(fmt::format("Invalid surface type, \"{}\"", surf_type));
×
1371
      }
1372

1373
      // Check for a periodic surface
1374
      if (check_for_node(surf_node, "boundary")) {
42,746✔
1375
        std::string surf_bc = get_node_value(surf_node, "boundary", true, true);
24,224✔
1376
        if (surf_bc == "periodic") {
24,224✔
1377
          // Check for surface albedo. Skip sanity check as it is already done
1378
          // in the Surface class's constructor.
1379
          if (check_for_node(surf_node, "albedo")) {
362!
1380
            albedo_map[model::surfaces.back()->id_] =
×
1381
              std::stod(get_node_value(surf_node, "albedo"));
×
1382
          }
1383
          if (check_for_node(surf_node, "periodic_surface_id")) {
362✔
1384
            int i_periodic =
1385
              std::stoi(get_node_value(surf_node, "periodic_surface_id"));
258✔
1386
            int lo_id = std::min(model::surfaces.back()->id_, i_periodic);
258✔
1387
            int hi_id = std::max(model::surfaces.back()->id_, i_periodic);
258✔
1388
            periodic_pairs.insert({lo_id, hi_id});
258✔
1389
          } else {
1390
            periodic_pairs.insert({model::surfaces.back()->id_, -1});
104✔
1391
          }
1392
        }
1393
      }
24,224✔
1394
    }
42,746✔
1395
  }
1396

1397
  // Fill the surface map
1398
  for (int i_surf = 0; i_surf < model::surfaces.size(); i_surf++) {
50,351✔
1399
    int id = model::surfaces[i_surf]->id_;
42,746✔
1400
    auto in_map = model::surface_map.find(id);
42,746✔
1401
    if (in_map == model::surface_map.end()) {
42,746!
1402
      model::surface_map[id] = i_surf;
42,746✔
1403
    } else {
1404
      fatal_error(
×
1405
        fmt::format("Two or more surfaces use the same unique ID: {}", id));
×
1406
    }
1407
  }
1408

1409
  // Resolve unpaired periodic surfaces.  A lambda function is used with
1410
  // std::find_if to identify the unpaired surfaces.
1411
  auto is_unresolved_pair = [](const std::pair<int, int> p) {
233✔
1412
    return p.second == -1;
233✔
1413
  };
1414
  auto first_unresolved = std::find_if(
7,605✔
1415
    periodic_pairs.begin(), periodic_pairs.end(), is_unresolved_pair);
1416
  if (first_unresolved != periodic_pairs.end()) {
7,605✔
1417
    // Found one unpaired surface; search for a second one
1418
    auto next_elem = first_unresolved;
52✔
1419
    next_elem++;
52✔
1420
    auto second_unresolved =
1421
      std::find_if(next_elem, periodic_pairs.end(), is_unresolved_pair);
52✔
1422
    if (second_unresolved == periodic_pairs.end()) {
52!
1423
      fatal_error("Found only one periodic surface without a specified partner."
×
1424
                  " Please specify the partner for each periodic surface.");
1425
    }
1426

1427
    // Make sure there isn't a third unpaired surface
1428
    next_elem = second_unresolved;
52✔
1429
    next_elem++;
52✔
1430
    auto third_unresolved =
1431
      std::find_if(next_elem, periodic_pairs.end(), is_unresolved_pair);
52✔
1432
    if (third_unresolved != periodic_pairs.end()) {
52!
1433
      fatal_error(
×
1434
        "Found at least three periodic surfaces without a specified "
1435
        "partner. Please specify the partner for each periodic surface.");
1436
    }
1437

1438
    // Add the completed pair and remove the old, unpaired entries
1439
    int lo_id = std::min(first_unresolved->first, second_unresolved->first);
52✔
1440
    int hi_id = std::max(first_unresolved->first, second_unresolved->first);
52✔
1441
    periodic_pairs.insert({lo_id, hi_id});
52✔
1442
    periodic_pairs.erase(first_unresolved);
52✔
1443
    periodic_pairs.erase(second_unresolved);
52✔
1444
  }
1445

1446
  // Assign the periodic boundary conditions with albedos
1447
  for (auto periodic_pair : periodic_pairs) {
7,786✔
1448
    int i_surf = model::surface_map[periodic_pair.first];
181✔
1449
    int j_surf = model::surface_map[periodic_pair.second];
181✔
1450
    Surface& surf1 {*model::surfaces[i_surf]};
181✔
1451
    Surface& surf2 {*model::surfaces[j_surf]};
181✔
1452

1453
    // Compute the dot product of the surface normals
1454
    Direction norm1 = surf1.normal({0, 0, 0});
181✔
1455
    Direction norm2 = surf2.normal({0, 0, 0});
181✔
1456
    norm1 /= norm1.norm();
181✔
1457
    norm2 /= norm2.norm();
181✔
1458
    double dot_prod = norm1.dot(norm2);
181✔
1459

1460
    // If the dot product is 1 (to within floating point precision) then the
1461
    // planes are parallel which indicates a translational periodic boundary
1462
    // condition.  Otherwise, it is a rotational periodic BC.
1463
    if (std::abs(1.0 - dot_prod) < FP_PRECISION) {
181✔
1464
      surf1.bc_ = make_unique<TranslationalPeriodicBC>(i_surf, j_surf);
117✔
1465
      surf2.bc_ = make_unique<TranslationalPeriodicBC>(i_surf, j_surf);
117✔
1466
    } else {
1467
      // check that both normals have at least one 0 component
1468
      if (std::abs(norm1.x) > FP_PRECISION &&
112✔
1469
          std::abs(norm1.y) > FP_PRECISION &&
112!
1470
          std::abs(norm1.z) > FP_PRECISION) {
16✔
1471
        fatal_error(fmt::format(
×
1472
          "The normal ({}) of the periodic surface ({}) does not contain any "
1473
          "component with a zero value. A RotationalPeriodicBC requires one "
1474
          "component which is zero for both plane normals.",
1475
          norm1, i_surf));
1476
      }
1477
      if (std::abs(norm2.x) > FP_PRECISION &&
96✔
1478
          std::abs(norm2.y) > FP_PRECISION &&
96!
1479
          std::abs(norm2.z) > FP_PRECISION) {
16✔
1480
        fatal_error(fmt::format(
×
1481
          "The normal ({}) of the periodic surface ({}) does not contain any "
1482
          "component with a zero value. A RotationalPeriodicBC requires one "
1483
          "component which is zero for both plane normals.",
1484
          norm2, j_surf));
1485
      }
1486
      // find common zero component, which indicates the periodic axis
1487
      RotationalPeriodicBC::PeriodicAxis axis;
1488
      if (std::abs(norm1.x) <= FP_PRECISION &&
80!
1489
          std::abs(norm2.x) <= FP_PRECISION) {
16✔
1490
        axis = RotationalPeriodicBC::PeriodicAxis::x;
16✔
1491
      } else if (std::abs(norm1.y) <= FP_PRECISION &&
80✔
1492
                 std::abs(norm2.y) <= FP_PRECISION) {
32✔
1493
        axis = RotationalPeriodicBC::PeriodicAxis::y;
16✔
1494
      } else if (std::abs(norm1.z) <= FP_PRECISION &&
64!
1495
                 std::abs(norm2.z) <= FP_PRECISION) {
32✔
1496
        axis = RotationalPeriodicBC::PeriodicAxis::z;
32✔
1497
      } else {
1498
        fatal_error(fmt::format(
×
1499
          "There is no component which is 0.0 in both normal vectors. This "
1500
          "indicates that the two planes are not periodic about the X, Y, or Z "
1501
          "axis, which is not supported."));
1502
      }
1503
      surf1.bc_ = make_unique<RotationalPeriodicBC>(i_surf, j_surf, axis);
64✔
1504
      surf2.bc_ = make_unique<RotationalPeriodicBC>(i_surf, j_surf, axis);
64✔
1505
    }
1506

1507
    // If albedo data is present in albedo map, set the boundary albedo.
1508
    if (albedo_map.count(surf1.id_)) {
181!
1509
      surf1.bc_->set_albedo(albedo_map[surf1.id_]);
×
1510
    }
1511
    if (albedo_map.count(surf2.id_)) {
181!
1512
      surf2.bc_->set_albedo(albedo_map[surf2.id_]);
×
1513
    }
1514
  }
1515
}
7,605✔
1516

1517
void free_memory_surfaces()
7,723✔
1518
{
1519
  model::surfaces.clear();
7,723✔
1520
  model::surface_map.clear();
7,723✔
1521
}
7,723✔
1522

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