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

openmc-dev / openmc / 38603847024

14 Oct 2026 05:06PM UTC coverage: 81.479% (+0.04%) from 81.444%
38603847024

Pull #4198

github

web-flow
Merge 7435e837d into 3aec0edef
Pull Request #4198: Bounding boxes for quadric surfaces

20173 of 29300 branches covered (68.85%)

Branch coverage included in aggregate %.

141 of 142 new or added lines in 3 files covered. (99.3%)

68 existing lines in 1 file now uncovered.

62846 of 72590 relevant lines covered (86.58%)

42639709.81 hits per line

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

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

3
#include <algorithm> // for max, min
4
#include <array>
5
#include <cmath>
6
#include <complex>
7
#include <initializer_list>
8
#include <iterator>
9
#include <set>
10
#include <utility>
11

12
#include <fmt/core.h>
13

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

26
namespace openmc {
27

28
//==============================================================================
29
// Global variables
30
//==============================================================================
31

32
namespace model {
33
std::unordered_map<int, int> surface_map;
34
vector<unique_ptr<Surface>> surfaces;
35
} // namespace model
36

37
//==============================================================================
38
// Helper functions for reading the "coeffs" node of an XML surface element
39
//==============================================================================
40

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

52
  // Copy the coefficients
53
  int i = 0;
36,521 ✔
54
  for (auto c : coeffs) {
108,455 ✔
55
    *c = coeffs_file[i++];
71,934 ✔
56
  }
57
}
36,521 ✔
58

59
//==============================================================================
60
// Surface implementation
61
//==============================================================================
62

63
Surface::Surface() {} // empty constructor
698 ✔
64

65
Surface::Surface(pugi::xml_node surf_node)
36,521 ✔
66
{
67
  if (check_for_node(surf_node, "id")) {
36,521 !
68
    id_ = std::stoi(get_node_value(surf_node, "id"));
73,042 ✔
69
    if (contains(settings::source_write_surf_id, id_) ||
73,042 ✔
70
        settings::source_write_surf_id.empty()) {
35,961 ✔
71
      surf_source_ = true;
35,782 ✔
72
    }
73
  } else {
74
    fatal_error("Must specify id of surface in geometry XML file.");
×
75
  }
76

77
  if (check_for_node(surf_node, "name")) {
36,521 ✔
78
    name_ = get_node_value(surf_node, "name", false);
8,258 ✔
79
  }
80

81
  if (check_for_node(surf_node, "boundary")) {
36,521 ✔
82
    std::string surf_bc = get_node_value(surf_node, "boundary", true, true);
21,320 ✔
83

84
    if (surf_bc == "transmission" || surf_bc == "transmit" || surf_bc.empty()) {
42,640 !
85
      // Leave the bc_ a nullptr
86
    } else if (surf_bc == "vacuum") {
21,320 ✔
87
      bc_ = make_unique<VacuumBC>();
10,560 ✔
88
    } else if (surf_bc == "reflective" || surf_bc == "reflect" ||
11,196 !
89
               surf_bc == "reflecting") {
436 !
90
      bc_ = make_unique<ReflectiveBC>();
10,324 ✔
91
    } else if (surf_bc == "white") {
436 ✔
92
      bc_ = make_unique<WhiteBC>();
60 ✔
93
    } else if (surf_bc == "periodic") {
376 !
94
      // Periodic BCs are handled separately
95
    } else {
96
      fatal_error(fmt::format("Unknown boundary condition \"{}\" specified "
×
97
                              "on surface {}",
98
        surf_bc, id_));
×
99
    }
100

101
    if (check_for_node(surf_node, "albedo") && bc_) {
21,320 !
102
      double surf_alb = std::stod(get_node_value(surf_node, "albedo"));
120 ✔
103

104
      if (surf_alb < 0.0)
60 !
105
        fatal_error(fmt::format("Surface {} has an albedo of {}. "
×
106
                                "Albedo values must be positive.",
107
          id_, surf_alb));
×
108

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

115
      bc_->set_albedo(surf_alb);
60 ✔
116
    }
117
  }
21,320 ✔
118
}
36,521 ✔
119

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

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

136
Direction Surface::reflect(Position r, Direction u, GeometryState* p) const
716,286,721 ✔
137
{
138
  // Determine projection of direction onto normal and squared magnitude of
139
  // normal.
140
  Direction n = normal(r);
716,286,721 ✔
141

142
  // Reflect direction according to normal.
143
  return u.reflect(n);
716,286,721 ✔
144
}
145

146
Direction Surface::diffuse_reflect(
732,472 ✔
147
  Position r, Direction u, uint64_t* seed) const
148
{
149
  // Diffuse reflect direction according to the normal.
150
  // cosine distribution
151

152
  Direction n = this->normal(r);
732,472 ✔
153
  n /= n.norm();
732,472 ✔
154
  const double projection = n.dot(u);
732,472 ✔
155

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

160
  // sample azimuthal distribution uniformly
161
  u = rotate_angle(n, mu, nullptr, seed);
732,472 ✔
162

163
  // normalize the direction
164
  return u / u.norm();
732,472 ✔
165
}
166

167
void Surface::to_hdf5(hid_t group_id) const
31,747 ✔
168
{
169
  hid_t surf_group = create_group(group_id, fmt::format("surface {}", id_));
31,747 ✔
170

171
  if (geom_type() == GeometryType::DAG) {
31,747 ✔
172
    write_string(surf_group, "geom_type", "dagmc", false);
1,268 !
173
  } else if (geom_type() == GeometryType::CSG) {
31,113 !
174
    write_string(surf_group, "geom_type", "csg", false);
31,113 ✔
175

176
    if (bc_) {
31,113 ✔
177
      write_string(surf_group, "boundary_type", bc_->type(), false);
18,538 ✔
178
      bc_->to_hdf5(surf_group);
18,538 ✔
179

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

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

197
  if (!name_.empty()) {
31,747 ✔
198
    write_string(surf_group, "name", name_, false);
7,178 ✔
199
  }
200

201
  to_hdf5_inner(surf_group);
31,747 ✔
202

203
  close_group(surf_group);
31,747 ✔
204
}
31,747 ✔
205

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

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

224
//==============================================================================
225
// SurfaceXPlane implementation
226
//==============================================================================
227

228
SurfaceXPlane::SurfaceXPlane(pugi::xml_node surf_node) : Surface(surf_node)
9,677 ✔
229
{
230
  read_coeffs(surf_node, id_, {&x0_});
9,677 ✔
231
}
9,677 ✔
232

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

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

243
Direction SurfaceXPlane::normal(Position r) const
278,613,696 ✔
244
{
245
  return {1., 0., 0.};
278,613,696 ✔
246
}
247

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

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

264
//==============================================================================
265
// SurfaceYPlane implementation
266
//==============================================================================
267

268
SurfaceYPlane::SurfaceYPlane(pugi::xml_node surf_node) : Surface(surf_node)
8,182 ✔
269
{
270
  read_coeffs(surf_node, id_, {&y0_});
8,182 ✔
271
}
8,182 ✔
272

273
double SurfaceYPlane::evaluate(Position r) const
1,979,755,598 ✔
274
{
275
  return r.y - y0_;
1,979,755,598 ✔
276
}
277

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

283
Direction SurfaceYPlane::normal(Position r) const
387,662,768 ✔
284
{
285
  return {0., 1., 0.};
387,662,768 ✔
286
}
287

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

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

304
//==============================================================================
305
// SurfaceZPlane implementation
306
//==============================================================================
307

308
SurfaceZPlane::SurfaceZPlane(pugi::xml_node surf_node) : Surface(surf_node)
6,064 ✔
309
{
310
  read_coeffs(surf_node, id_, {&z0_});
6,064 ✔
311
}
6,064 ✔
312

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

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

323
Direction SurfaceZPlane::normal(Position r) const
194,265,146 ✔
324
{
325
  return {0., 0., 1.};
194,265,146 ✔
326
}
327

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

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

344
//==============================================================================
345
// SurfacePlane implementation
346
//==============================================================================
347

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

353
double SurfacePlane::evaluate(Position r) const
262,893,468 ✔
354
{
355
  return A_ * r.x + B_ * r.y + C_ * r.z - D_;
262,893,468 ✔
356
}
357

358
BoundingBox SurfacePlane::bounding_box(bool pos_side) const
112 ✔
359
{
360
  // A general plane bounds a half-space in one direction only when its normal
361
  // is parallel to a coordinate axis; otherwise both half-spaces are unbounded
362
  // along every axis. This mirrors PlaneMixin.bounding_box on the Python side,
363
  // so that a plane whose off-axis coefficients are rotation-matrix roundoff
364
  // (e.g. B = 1 with A = C = 6.1e-17) yields the same box through both APIs.
365
  const Direction n = normal({});
112 ✔
366
  const double norm = n.norm();
112 ✔
367
  if (norm == 0.0)
112 ✔
368
    return {};
8 ✔
369

370
  int axis = -1;
288 ✔
371
  for (int i = 0; i < 3; ++i) {
288 ✔
372
    if (std::abs(std::abs(n[i] / norm) - 1.0) > PLANE_ALIGNMENT_TOL)
248 ✔
373
      continue;
168 ✔
374

375
    bool aligned = true;
288 ✔
376
    for (int j = 0; j < 3; ++j) {
288 ✔
377
      if (j != i && std::abs(n[j] / norm) > PLANE_ALIGNMENT_TOL) {
224 ✔
378
        aligned = false;
379
        break;
380
      }
381
    }
382
    if (aligned) {
80 ✔
383
      axis = i;
384
      break;
385
    }
386
  }
387
  if (axis == -1)
104 ✔
388
    return {};
40 ✔
389

390
  // The half-space is bounded below when the outward normal points along the
391
  // positive axis direction and we are on the positive side, or vice versa.
392
  BoundingBox bbox;
64 ✔
393
  const double intercept = D_ / n[axis];
64 ✔
394
  if (pos_side == (n[axis] > 0.0)) {
64 ✔
395
    bbox.min[axis] = intercept;
40 ✔
396
  } else {
397
    bbox.max[axis] = intercept;
24 ✔
398
  }
399
  return bbox;
64 ✔
400
}
401

402
double SurfacePlane::distance(Position r, Direction u, bool coincident) const
596,951,714 ✔
403
{
404
  const double f = A_ * r.x + B_ * r.y + C_ * r.z - D_;
596,951,714 ✔
405
  const double projection = A_ * u.x + B_ * u.y + C_ * u.z;
596,951,714 ✔
406
  if (coincident || std::abs(f) < FP_COINCIDENT || projection == 0.0) {
596,951,714 !
407
    return INFTY;
408
  } else {
409
    const double d = -f / projection;
547,622,960 ✔
410
    if (d < 0.0)
547,622,960 ✔
411
      return INFTY;
412
    return d;
298,436,432 ✔
413
  }
414
}
415

416
Direction SurfacePlane::normal(Position r) const
4,192,110 ✔
417
{
418
  return {A_, B_, C_};
4,192,110 ✔
419
}
420

421
void SurfacePlane::to_hdf5_inner(hid_t group_id) const
1,152 ✔
422
{
423
  write_string(group_id, "type", "plane", false);
1,152 ✔
424
  array<double, 4> coeffs {{A_, B_, C_, D_}};
1,152 ✔
425
  write_dataset(group_id, "coefficients", coeffs);
1,152 ✔
426
}
1,152 ✔
427

428
//==============================================================================
429
// Generic functions for x-, y-, and z-, cylinders
430
//==============================================================================
431

432
// The template parameters indicate the axes perpendicular to the axis of the
433
// cylinder.  offset1 and offset2 should correspond with i1 and i2,
434
// respectively.
435
template<int i1, int i2>
436
double axis_aligned_cylinder_evaluate(
2,147,483,647 ✔
437
  Position r, double offset1, double offset2, double radius)
438
{
439
  const double r1 = r.get<i1>() - offset1;
2,147,483,647 ✔
440
  const double r2 = r.get<i2>() - offset2;
2,147,483,647 ✔
441
  return r1 * r1 + r2 * r2 - radius * radius;
2,147,483,647 ✔
442
}
443

444
// The first template parameter indicates which axis the cylinder is aligned to.
445
// The other two parameters indicate the other two axes.  offset1 and offset2
446
// should correspond with i2 and i3, respectively.
447
template<int i1, int i2, int i3>
448
double axis_aligned_cylinder_distance(Position r, Direction u, bool coincident,
2,147,483,647 ✔
449
  double offset1, double offset2, double radius)
450
{
451
  const double a = 1.0 - u.get<i1>() * u.get<i1>(); // u^2 + v^2
2,147,483,647 ✔
452
  if (a == 0.0)
2,147,483,647 ✔
453
    return INFTY;
454

455
  const double r2 = r.get<i2>() - offset1;
2,147,483,647 ✔
456
  const double r3 = r.get<i3>() - offset2;
2,147,483,647 ✔
457
  const double k = r2 * u.get<i2>() + r3 * u.get<i3>();
2,147,483,647 ✔
458
  const double c = r2 * r2 + r3 * r3 - radius * radius;
2,147,483,647 ✔
459
  const double quad = k * k - a * c;
2,147,483,647 ✔
460

461
  if (quad < 0.0) {
2,147,483,647 ✔
462
    // No intersection with cylinder.
463
    return INFTY;
464

465
  } else if (coincident || std::abs(c) < FP_COINCIDENT) {
2,147,483,647 ✔
466
    // Particle is on the cylinder, thus one distance is positive/negative
467
    // and the other is zero. The sign of k determines if we are facing in or
468
    // out.
469
    if (k >= 0.0) {
1,395,065,465 ✔
470
      return INFTY;
471
    } else {
472
      return (-k + sqrt(quad)) / a;
796,635,412 ✔
473
    }
474

475
  } else if (c < 0.0) {
1,856,478,306 ✔
476
    // Particle is inside the cylinder, thus one distance must be negative
477
    // and one must be positive. The positive distance will be the one with
478
    // negative sign on sqrt(quad).
479
    return (-k + sqrt(quad)) / a;
785,151,821 ✔
480

481
  } else {
482
    // Particle is outside the cylinder, thus both distances are either
483
    // positive or negative. If positive, the smaller distance is the one
484
    // with positive sign on sqrt(quad).
485
    const double d = (-k - sqrt(quad)) / a;
1,071,326,485 ✔
486
    if (d < 0.0)
1,071,326,485 ✔
487
      return INFTY;
488
    return d;
835,327,937 ✔
489
  }
490
}
491

492
// The first template parameter indicates which axis the cylinder is aligned to.
493
// The other two parameters indicate the other two axes.  offset1 and offset2
494
// should correspond with i2 and i3, respectively.
495
template<int i1, int i2, int i3>
496
Direction axis_aligned_cylinder_normal(
204,403,025 ✔
497
  Position r, double offset1, double offset2)
498
{
499
  Direction u;
204,403,025 ✔
500
  u.get<i2>() = 2.0 * (r.get<i2>() - offset1);
204,403,025 ✔
501
  u.get<i3>() = 2.0 * (r.get<i3>() - offset2);
204,403,025 ✔
502
  u.get<i1>() = 0.0;
503
  return u;
504
}
505

506
//==============================================================================
507
// SurfaceXCylinder implementation
508
//==============================================================================
509

510
SurfaceXCylinder::SurfaceXCylinder(pugi::xml_node surf_node)
30 ✔
511
  : Surface(surf_node)
30 ✔
512
{
513
  read_coeffs(surf_node, id_, {&y0_, &z0_, &radius_});
30 ✔
514
}
30 ✔
515

516
double SurfaceXCylinder::evaluate(Position r) const
929,072 ✔
517
{
518
  return axis_aligned_cylinder_evaluate<1, 2>(r, y0_, z0_, radius_);
929,072 ✔
519
}
520

521
double SurfaceXCylinder::distance(
1,179,664 ✔
522
  Position r, Direction u, bool coincident) const
523
{
524
  return axis_aligned_cylinder_distance<0, 1, 2>(
1,179,664 ✔
525
    r, u, coincident, y0_, z0_, radius_);
1,179,664 ✔
526
}
527

528
Direction SurfaceXCylinder::normal(Position r) const
289,408 ✔
529
{
530
  return axis_aligned_cylinder_normal<0, 1, 2>(r, y0_, z0_);
289,408 ✔
531
}
532

533
void SurfaceXCylinder::to_hdf5_inner(hid_t group_id) const
24 ✔
534
{
535
  write_string(group_id, "type", "x-cylinder", false);
24 ✔
536
  array<double, 3> coeffs {{y0_, z0_, radius_}};
24 ✔
537
  write_dataset(group_id, "coefficients", coeffs);
24 ✔
538
}
24 ✔
539

540
BoundingBox SurfaceXCylinder::bounding_box(bool pos_side) const
×
541
{
542
  if (!pos_side) {
×
543
    return {{-INFTY, y0_ - radius_, z0_ - radius_},
×
544
      {INFTY, y0_ + radius_, z0_ + radius_}};
×
545
  } else {
546
    return {};
×
547
  }
548
}
549
//==============================================================================
550
// SurfaceYCylinder implementation
551
//==============================================================================
552

553
SurfaceYCylinder::SurfaceYCylinder(pugi::xml_node surf_node)
18 ✔
554
  : Surface(surf_node)
18 ✔
555
{
556
  read_coeffs(surf_node, id_, {&x0_, &z0_, &radius_});
18 ✔
557
}
18 ✔
558

559
double SurfaceYCylinder::evaluate(Position r) const
124,352 ✔
560
{
561
  return axis_aligned_cylinder_evaluate<0, 2>(r, x0_, z0_, radius_);
124,352 ✔
562
}
563

564
double SurfaceYCylinder::distance(
479,832 ✔
565
  Position r, Direction u, bool coincident) const
566
{
567
  return axis_aligned_cylinder_distance<1, 0, 2>(
479,832 ✔
568
    r, u, coincident, x0_, z0_, radius_);
479,832 ✔
569
}
570

571
Direction SurfaceYCylinder::normal(Position r) const
×
572
{
573
  return axis_aligned_cylinder_normal<1, 0, 2>(r, x0_, z0_);
×
574
}
575

576
void SurfaceYCylinder::to_hdf5_inner(hid_t group_id) const
16 ✔
577
{
578
  write_string(group_id, "type", "y-cylinder", false);
16 ✔
579
  array<double, 3> coeffs {{x0_, z0_, radius_}};
16 ✔
580
  write_dataset(group_id, "coefficients", coeffs);
16 ✔
581
}
16 ✔
582

583
BoundingBox SurfaceYCylinder::bounding_box(bool pos_side) const
8 ✔
584
{
585
  if (!pos_side) {
8 !
586
    return {{x0_ - radius_, -INFTY, z0_ - radius_},
8 ✔
587
      {x0_ + radius_, INFTY, z0_ + radius_}};
8 ✔
588
  } else {
589
    return {};
×
590
  }
591
}
592

593
//==============================================================================
594
// SurfaceZCylinder implementation
595
//==============================================================================
596

597
SurfaceZCylinder::SurfaceZCylinder(pugi::xml_node surf_node)
4,049 ✔
598
  : Surface(surf_node)
4,049 ✔
599
{
600
  read_coeffs(surf_node, id_, {&x0_, &y0_, &radius_});
4,049 ✔
601
}
4,049 ✔
602

603
double SurfaceZCylinder::evaluate(Position r) const
2,147,483,647 ✔
604
{
605
  return axis_aligned_cylinder_evaluate<0, 1>(r, x0_, y0_, radius_);
2,147,483,647 ✔
606
}
607

608
double SurfaceZCylinder::distance(
2,147,483,647 ✔
609
  Position r, Direction u, bool coincident) const
610
{
611
  return axis_aligned_cylinder_distance<2, 0, 1>(
2,147,483,647 ✔
612
    r, u, coincident, x0_, y0_, radius_);
2,147,483,647 ✔
613
}
614

615
Direction SurfaceZCylinder::normal(Position r) const
204,113,617 ✔
616
{
617
  return axis_aligned_cylinder_normal<2, 0, 1>(r, x0_, y0_);
204,113,617 ✔
618
}
619

620
void SurfaceZCylinder::to_hdf5_inner(hid_t group_id) const
3,373 ✔
621
{
622
  write_string(group_id, "type", "z-cylinder", false);
3,373 ✔
623
  array<double, 3> coeffs {{x0_, y0_, radius_}};
3,373 ✔
624
  write_dataset(group_id, "coefficients", coeffs);
3,373 ✔
625
}
3,373 ✔
626

627
BoundingBox SurfaceZCylinder::bounding_box(bool pos_side) const
32 ✔
628
{
629
  if (!pos_side) {
32 ✔
630
    return {{x0_ - radius_, y0_ - radius_, -INFTY},
16 ✔
631
      {x0_ + radius_, y0_ + radius_, INFTY}};
16 ✔
632
  } else {
633
    return {};
16 ✔
634
  }
635
}
636

637
//==============================================================================
638
// SurfaceSphere implementation
639
//==============================================================================
640

641
SurfaceSphere::SurfaceSphere(pugi::xml_node surf_node) : Surface(surf_node)
6,601 ✔
642
{
643
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &radius_});
6,601 ✔
644
}
6,601 ✔
645

646
double SurfaceSphere::evaluate(Position r) const
913,582,357 ✔
647
{
648
  const double x = r.x - x0_;
913,582,357 ✔
649
  const double y = r.y - y0_;
913,582,357 ✔
650
  const double z = r.z - z0_;
913,582,357 ✔
651
  return x * x + y * y + z * z - radius_ * radius_;
913,582,357 ✔
652
}
653

654
double SurfaceSphere::distance(Position r, Direction u, bool coincident) const
1,554,515,562 ✔
655
{
656
  const double x = r.x - x0_;
1,554,515,562 ✔
657
  const double y = r.y - y0_;
1,554,515,562 ✔
658
  const double z = r.z - z0_;
1,554,515,562 ✔
659
  const double k = x * u.x + y * u.y + z * u.z;
1,554,515,562 ✔
660
  const double c = x * x + y * y + z * z - radius_ * radius_;
1,554,515,562 ✔
661
  const double quad = k * k - c;
1,554,515,562 ✔
662

663
  if (quad < 0.0) {
1,554,515,562 ✔
664
    // No intersection with sphere.
665
    return INFTY;
666

667
  } else if (coincident || std::abs(c) < FP_COINCIDENT) {
1,417,566,182 !
668
    // Particle is on the sphere, thus one distance is positive/negative and
669
    // the other is zero. The sign of k determines if we are facing in or out.
670
    if (k >= 0.0) {
85,550,715 ✔
671
      return INFTY;
672
    } else {
673
      return -k + sqrt(quad);
48,778,208 ✔
674
    }
675

676
  } else if (c < 0.0) {
1,332,015,467 ✔
677
    // Particle is inside the sphere, thus one distance must be negative and
678
    // one must be positive. The positive distance will be the one with
679
    // negative sign on sqrt(quad)
680
    return -k + sqrt(quad);
1,282,841,461 ✔
681

682
  } else {
683
    // Particle is outside the sphere, thus both distances are either positive
684
    // or negative. If positive, the smaller distance is the one with positive
685
    // sign on sqrt(quad).
686
    const double d = -k - sqrt(quad);
49,174,006 ✔
687
    if (d < 0.0)
49,174,006 ✔
688
      return INFTY;
689
    return d;
38,420,412 ✔
690
  }
691
}
692

693
Direction SurfaceSphere::normal(Position r) const
283,437,680 ✔
694
{
695
  return {2.0 * (r.x - x0_), 2.0 * (r.y - y0_), 2.0 * (r.z - z0_)};
283,437,680 ✔
696
}
697

698
void SurfaceSphere::to_hdf5_inner(hid_t group_id) const
5,649 ✔
699
{
700
  write_string(group_id, "type", "sphere", false);
5,649 ✔
701
  array<double, 4> coeffs {{x0_, y0_, z0_, radius_}};
5,649 ✔
702
  write_dataset(group_id, "coefficients", coeffs);
5,649 ✔
703
}
5,649 ✔
704

705
BoundingBox SurfaceSphere::bounding_box(bool pos_side) const
×
706
{
707
  if (!pos_side) {
×
708
    return {{x0_ - radius_, y0_ - radius_, z0_ - radius_},
×
709
      {x0_ + radius_, y0_ + radius_, z0_ + radius_}};
×
710
  } else {
711
    return {};
×
712
  }
713
}
714

715
//==============================================================================
716
// Generic functions for x-, y-, and z-, cones
717
//==============================================================================
718

719
// The first template parameter indicates which axis the cone is aligned to.
720
// The other two parameters indicate the other two axes.  offset1, offset2,
721
// and offset3 should correspond with i1, i2, and i3, respectively.
722
template<int i1, int i2, int i3>
723
double axis_aligned_cone_evaluate(
236,984 ✔
724
  Position r, double offset1, double offset2, double offset3, double radius_sq)
725
{
726
  const double r1 = r.get<i1>() - offset1;
236,984 ✔
727
  const double r2 = r.get<i2>() - offset2;
236,984 ✔
728
  const double r3 = r.get<i3>() - offset3;
236,984 ✔
729
  return r2 * r2 + r3 * r3 - radius_sq * r1 * r1;
236,984 ✔
730
}
731

732
// The first template parameter indicates which axis the cone is aligned to.
733
// The other two parameters indicate the other two axes.  offset1, offset2,
734
// and offset3 should correspond with i1, i2, and i3, respectively.
735
template<int i1, int i2, int i3>
736
double axis_aligned_cone_distance(Position r, Direction u, bool coincident,
711,288 ✔
737
  double offset1, double offset2, double offset3, double radius_sq)
738
{
739
  const double r1 = r.get<i1>() - offset1;
711,288 ✔
740
  const double r2 = r.get<i2>() - offset2;
711,288 ✔
741
  const double r3 = r.get<i3>() - offset3;
711,288 ✔
742
  const double a = u.get<i2>() * u.get<i2>() + u.get<i3>() * u.get<i3>() -
711,288 ✔
743
                   radius_sq * u.get<i1>() * u.get<i1>();
711,288 ✔
744
  const double k =
711,288 ✔
745
    r2 * u.get<i2>() + r3 * u.get<i3>() - radius_sq * r1 * u.get<i1>();
711,288 ✔
746
  const double c = r2 * r2 + r3 * r3 - radius_sq * r1 * r1;
711,288 ✔
747
  double quad = k * k - a * c;
711,288 ✔
748

749
  double d;
750

751
  if (quad < 0.0) {
711,288 !
752
    // No intersection with cone.
753
    return INFTY;
754

755
  } else if (coincident || std::abs(c) < FP_COINCIDENT) {
711,288 !
756
    // Particle is on the cone, thus one distance is positive/negative
757
    // and the other is zero. The sign of k determines if we are facing in or
758
    // out.
759
    if (k >= 0.0) {
43,280 !
760
      d = (-k - sqrt(quad)) / a;
×
761
    } else {
762
      d = (-k + sqrt(quad)) / a;
43,280 ✔
763
    }
764

765
  } else {
766
    // Calculate both solutions to the quadratic.
767
    quad = sqrt(quad);
668,008 ✔
768
    d = (-k - quad) / a;
668,008 ✔
769
    const double b = (-k + quad) / a;
668,008 ✔
770

771
    // Determine the smallest positive solution.
772
    if (d < 0.0) {
668,008 ✔
773
      if (b > 0.0)
567,304 ✔
774
        d = b;
483,712 ✔
775
    } else {
776
      if (b > 0.0) {
100,704 !
777
        if (b < d)
100,704 !
778
          d = b;
100,704 ✔
779
      }
780
    }
781
  }
782

783
  // If the distance was negative, set boundary distance to infinity.
784
  if (d <= 0.0)
711,288 ✔
785
    return INFTY;
99,216 ✔
786
  return d;
787
}
788

789
// The first template parameter indicates which axis the cone is aligned to.
790
// The other two parameters indicate the other two axes.  offset1, offset2,
791
// and offset3 should correspond with i1, i2, and i3, respectively.
792
template<int i1, int i2, int i3>
793
Direction axis_aligned_cone_normal(
43,280 ✔
794
  Position r, double offset1, double offset2, double offset3, double radius_sq)
795
{
796
  Direction u;
797
  u.get<i1>() = -2.0 * radius_sq * (r.get<i1>() - offset1);
43,280 ✔
798
  u.get<i2>() = 2.0 * (r.get<i2>() - offset2);
43,280 ✔
799
  u.get<i3>() = 2.0 * (r.get<i3>() - offset3);
43,280 ✔
800
  return u;
801
}
802

803
//==============================================================================
804
// SurfaceXCone implementation
805
//==============================================================================
806

807
SurfaceXCone::SurfaceXCone(pugi::xml_node surf_node) : Surface(surf_node)
×
808
{
809
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &radius_sq_});
×
810
}
×
811

812
double SurfaceXCone::evaluate(Position r) const
×
813
{
814
  return axis_aligned_cone_evaluate<0, 1, 2>(r, x0_, y0_, z0_, radius_sq_);
×
815
}
816

817
double SurfaceXCone::distance(Position r, Direction u, bool coincident) const
×
818
{
819
  return axis_aligned_cone_distance<0, 1, 2>(
×
820
    r, u, coincident, x0_, y0_, z0_, radius_sq_);
×
821
}
822

823
Direction SurfaceXCone::normal(Position r) const
×
824
{
825
  return axis_aligned_cone_normal<0, 1, 2>(r, x0_, y0_, z0_, radius_sq_);
×
826
}
827

828
void SurfaceXCone::to_hdf5_inner(hid_t group_id) const
×
829
{
830
  write_string(group_id, "type", "x-cone", false);
×
831
  array<double, 4> coeffs {{x0_, y0_, z0_, radius_sq_}};
×
832
  write_dataset(group_id, "coefficients", coeffs);
×
833
}
×
834

835
//==============================================================================
836
// SurfaceYCone implementation
837
//==============================================================================
838

839
SurfaceYCone::SurfaceYCone(pugi::xml_node surf_node) : Surface(surf_node)
×
840
{
841
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &radius_sq_});
×
842
}
×
843

844
double SurfaceYCone::evaluate(Position r) const
×
845
{
846
  return axis_aligned_cone_evaluate<1, 0, 2>(r, y0_, x0_, z0_, radius_sq_);
×
847
}
848

849
double SurfaceYCone::distance(Position r, Direction u, bool coincident) const
×
850
{
851
  return axis_aligned_cone_distance<1, 0, 2>(
×
852
    r, u, coincident, y0_, x0_, z0_, radius_sq_);
×
853
}
854

855
Direction SurfaceYCone::normal(Position r) const
×
856
{
857
  return axis_aligned_cone_normal<1, 0, 2>(r, y0_, x0_, z0_, radius_sq_);
×
858
}
859

860
void SurfaceYCone::to_hdf5_inner(hid_t group_id) const
×
861
{
862
  write_string(group_id, "type", "y-cone", false);
×
863
  array<double, 4> coeffs {{x0_, y0_, z0_, radius_sq_}};
×
864
  write_dataset(group_id, "coefficients", coeffs);
×
865
}
×
866

867
//==============================================================================
868
// SurfaceZCone implementation
869
//==============================================================================
870

871
SurfaceZCone::SurfaceZCone(pugi::xml_node surf_node) : Surface(surf_node)
10 ✔
872
{
873
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &radius_sq_});
10 ✔
874
}
10 ✔
875

876
double SurfaceZCone::evaluate(Position r) const
236,984 ✔
877
{
878
  return axis_aligned_cone_evaluate<2, 0, 1>(r, z0_, x0_, y0_, radius_sq_);
236,984 ✔
879
}
880

881
double SurfaceZCone::distance(Position r, Direction u, bool coincident) const
711,288 ✔
882
{
883
  return axis_aligned_cone_distance<2, 0, 1>(
711,288 ✔
884
    r, u, coincident, z0_, x0_, y0_, radius_sq_);
711,288 ✔
885
}
886

887
Direction SurfaceZCone::normal(Position r) const
43,280 ✔
888
{
889
  return axis_aligned_cone_normal<2, 0, 1>(r, z0_, x0_, y0_, radius_sq_);
43,280 ✔
890
}
891

892
void SurfaceZCone::to_hdf5_inner(hid_t group_id) const
8 ✔
893
{
894
  write_string(group_id, "type", "z-cone", false);
8 ✔
895
  array<double, 4> coeffs {{x0_, y0_, z0_, radius_sq_}};
8 ✔
896
  write_dataset(group_id, "coefficients", coeffs);
8 ✔
897
}
8 ✔
898

899
//==============================================================================
900
// SurfaceQuadric implementation
901
//==============================================================================
902

903
SurfaceQuadric::SurfaceQuadric(pugi::xml_node surf_node) : Surface(surf_node)
226 ✔
904
{
905
  read_coeffs(
452 ✔
906
    surf_node, id_, {&A_, &B_, &C_, &D_, &E_, &F_, &G_, &H_, &J_, &K_});
226 ✔
907
}
226 ✔
908

909
double SurfaceQuadric::evaluate(Position r) const
125,976 ✔
910
{
911
  const double x = r.x;
125,976 ✔
912
  const double y = r.y;
125,976 ✔
913
  const double z = r.z;
125,976 ✔
914
  return x * (A_ * x + D_ * y + G_) + y * (B_ * y + E_ * z + H_) +
125,976 ✔
915
         z * (C_ * z + F_ * x + J_) + K_;
125,976 ✔
916
}
917

918
double SurfaceQuadric::distance(
230,320 ✔
919
  Position r, Direction ang, bool coincident) const
920
{
921
  const double& x = r.x;
230,320 ✔
922
  const double& y = r.y;
230,320 ✔
923
  const double& z = r.z;
230,320 ✔
924
  const double& u = ang.x;
230,320 ✔
925
  const double& v = ang.y;
230,320 ✔
926
  const double& w = ang.z;
230,320 ✔
927

928
  const double a =
230,320 ✔
929
    A_ * u * u + B_ * v * v + C_ * w * w + D_ * u * v + E_ * v * w + F_ * u * w;
230,320 ✔
930
  const double k = A_ * u * x + B_ * v * y + C_ * w * z +
230,320 ✔
931
                   0.5 * (D_ * (u * y + v * x) + E_ * (v * z + w * y) +
230,320 ✔
932
                           F_ * (w * x + u * z) + G_ * u + H_ * v + J_ * w);
230,320 ✔
933
  const double c = A_ * x * x + B_ * y * y + C_ * z * z + D_ * x * y +
230,320 ✔
934
                   E_ * y * z + F_ * x * z + G_ * x + H_ * y + J_ * z + K_;
230,320 ✔
935
  double quad = k * k - a * c;
230,320 ✔
936

937
  double d;
230,320 ✔
938

939
  if (quad < 0.0) {
230,320 !
940
    // No intersection with surface.
941
    return INFTY;
942

943
  } else if (coincident || std::abs(c) < FP_COINCIDENT) {
230,320 !
944
    // Particle is on the surface, thus one distance is positive/negative and
945
    // the other is zero. The sign of k determines which distance is zero and
946
    // which is not. Additionally, if a is zero, it means the particle is on
947
    // a plane-like surface.
948
    if (a == 0.0) {
26,224 !
949
      d = INFTY; // see the below explanation
950
    } else if (k >= 0.0) {
26,224 !
951
      d = (-k - sqrt(quad)) / a;
×
952
    } else {
953
      d = (-k + sqrt(quad)) / a;
26,224 ✔
954
    }
955

956
  } else if (a == 0.0) {
204,096 !
957
    // Given the orientation of the particle, the quadric looks like a plane in
958
    // this case, and thus we have only one solution despite potentially having
959
    // quad > 0.0. While the term under the square root may be real, in one
960
    // case of the +/- of the quadratic formula, 0/0 results, and in another, a
961
    // finite value over 0 results. Applying L'Hopital's to the 0/0 case gives
962
    // the below. Alternatively this can be found by simply putting a=0 in the
963
    // equation ax^2 + bx + c = 0.
964
    d = -0.5 * c / k;
×
965
  } else {
966
    // Calculate both solutions to the quadratic.
967
    quad = sqrt(quad);
204,096 ✔
968
    d = (-k - quad) / a;
204,096 ✔
969
    double b = (-k + quad) / a;
204,096 ✔
970

971
    // Determine the smallest positive solution.
972
    if (d < 0.0) {
204,096 ✔
973
      if (b > 0.0)
204,088 !
974
        d = b;
204,088 ✔
975
    } else {
976
      if (b > 0.0) {
8 !
977
        if (b < d)
8 !
978
          d = b;
×
979
      }
980
    }
981
  }
982

983
  // If the distance was negative, set boundary distance to infinity.
984
  if (d <= 0.0)
230,320 !
985
    return INFTY;
×
986
  return d;
987
}
988

989
Direction SurfaceQuadric::normal(Position r) const
26,224 ✔
990
{
991
  const double& x = r.x;
26,224 ✔
992
  const double& y = r.y;
26,224 ✔
993
  const double& z = r.z;
26,224 ✔
994
  return {2.0 * A_ * x + D_ * y + F_ * z + G_,
26,224 ✔
995
    2.0 * B_ * y + D_ * x + E_ * z + H_, 2.0 * C_ * z + E_ * y + F_ * x + J_};
26,224 ✔
996
}
997

998
BoundingBox SurfaceQuadric::bounding_box(bool pos_side) const
416 ✔
999
{
1000
  // The half-space is bounded in a direction only if the quadratic form is
1001
  // semidefinite with the sign of the half-space: (x - c)^T M (x - c) <= rho
1002
  // with M positive semidefinite, as for the inside of a cylinder along any
1003
  // axis or of an ellipsoid. Along coordinate i it then extends to
1004
  // c_i +- sqrt(rho M+_ii), unless the null space of M has a component along
1005
  // that coordinate. The same steps as _quadric_bounding_box in
1006
  // openmc/surface.py.
1007
  double sign = pos_side ? -1.0 : 1.0;
416 ✔
1008
  double m[3][3] = {{sign * A_, sign * 0.5 * D_, sign * 0.5 * F_},
416 ✔
1009
    {sign * 0.5 * D_, sign * B_, sign * 0.5 * E_},
416 ✔
1010
    {sign * 0.5 * F_, sign * 0.5 * E_, sign * C_}};
416 ✔
1011
  std::array<double, 3> lambda;
416 ✔
1012
  double vec[3][3];
416 ✔
1013
  symmetric_eigen(m, lambda, vec);
416 ✔
1014
  double scale =
416 !
1015
    std::max({std::abs(lambda[0]), std::abs(lambda[1]), std::abs(lambda[2])});
416 !
1016
  if (scale == 0.0 ||
416 !
1017
      std::min({lambda[0], lambda[1], lambda[2]}) < -1e-12 * scale)
416 ✔
1018
    return {};
208 ✔
1019
  std::array<bool, 3> null;
1020
  for (int n = 0; n < 3; ++n)
832 ✔
1021
    null[n] = lambda[n] <= 1e-12 * scale;
624 ✔
1022

1023
  // The linear term must lie in the range of M, otherwise the half-space is
1024
  // unbounded along the null space (a paraboloid)
1025
  double lin[3] = {sign * G_, sign * H_, sign * J_};
208 ✔
1026
  double lin_norm =
208 ✔
1027
    std::sqrt(lin[0] * lin[0] + lin[1] * lin[1] + lin[2] * lin[2]);
208 ✔
1028
  std::array<double, 3> proj;
208 ✔
1029
  for (int n = 0; n < 3; ++n) {
1,032 ✔
1030
    proj[n] = 0.0;
624 ✔
1031
    for (int i = 0; i < 3; ++i)
2,496 ✔
1032
      proj[n] += lin[i] * vec[i][n];
1,872 ✔
1033
    if (null[n] && std::abs(proj[n]) > 1e-12 * lin_norm)
624 ✔
1034
      return {};
8 ✔
1035
  }
1036

1037
  // Centre and size: (x - center)^T M (x - center) <= rho
1038
  double center[3];
1039
  for (int i = 0; i < 3; ++i) {
800 ✔
1040
    double sum = 0.0;
1041
    for (int n = 0; n < 3; ++n)
2,400 ✔
1042
      if (!null[n])
1,800 ✔
1043
        sum += proj[n] / lambda[n] * vec[i][n];
1,248 ✔
1044
    center[i] = -0.5 * sum;
600 ✔
1045
  }
1046
  double rho = 0.0;
1047
  for (int n = 0; n < 3; ++n)
800 ✔
1048
    if (!null[n])
600 ✔
1049
      rho += proj[n] * proj[n] / lambda[n];
416 ✔
1050
  rho = -sign * K_ + 0.25 * rho;
200 ✔
1051
  if (rho < 0.0)
200 !
NEW
1052
    return {};
×
1053

1054
  BoundingBox box;
200 ✔
1055
  for (int i = 0; i < 3; ++i) {
800 ✔
1056
    double in_null = 0.0;
1057
    double m_plus = 0.0;
1058
    for (int n = 0; n < 3; ++n) {
2,400 ✔
1059
      if (null[n]) {
1,800 ✔
1060
        in_null += vec[i][n] * vec[i][n];
552 ✔
1061
      } else {
1062
        m_plus += vec[i][n] * vec[i][n] / lambda[n];
1,248 ✔
1063
      }
1064
    }
1065
    if (in_null > 1e-24)
600 ✔
1066
      continue;
368 ✔
1067
    double w = std::sqrt(rho * m_plus);
232 ✔
1068
    box.min[i] = center[i] - w;
232 ✔
1069
    box.max[i] = center[i] + w;
232 ✔
1070
  }
1071
  return box;
200 ✔
1072
}
1073

1074
void SurfaceQuadric::to_hdf5_inner(hid_t group_id) const
216 ✔
1075
{
1076
  write_string(group_id, "type", "quadric", false);
216 ✔
1077
  array<double, 10> coeffs {{A_, B_, C_, D_, E_, F_, G_, H_, J_, K_}};
216 ✔
1078
  write_dataset(group_id, "coefficients", coeffs);
216 ✔
1079
}
216 ✔
1080

1081
//==============================================================================
1082
// Torus helper functions
1083
//==============================================================================
1084

1085
double torus_distance(double x1, double x2, double x3, double u1, double u2,
19,297,864 ✔
1086
  double u3, double A, double B, double C, bool coincident)
1087
{
1088
  // Coefficients for equation: (c2 t^2 + c1 t + c0)^2 = c2' t^2 + c1' t + c0'
1089
  double D = (C * C) / (B * B);
19,297,864 ✔
1090
  double c2 = u1 * u1 + u2 * u2 + D * u3 * u3;
19,297,864 ✔
1091
  double c1 = 2 * (u1 * x1 + u2 * x2 + D * u3 * x3);
19,297,864 ✔
1092
  double c0 = x1 * x1 + x2 * x2 + D * x3 * x3 + A * A - C * C;
19,297,864 ✔
1093
  double four_A2 = 4 * A * A;
19,297,864 ✔
1094
  double c2p = four_A2 * (u1 * u1 + u2 * u2);
19,297,864 ✔
1095
  double c1p = 2 * four_A2 * (u1 * x1 + u2 * x2);
19,297,864 ✔
1096
  double c0p = four_A2 * (x1 * x1 + x2 * x2);
19,297,864 ✔
1097

1098
  // Coefficient for equation: a t^4 + b t^3 + c t^2 + d t + e = 0. If the point
1099
  // is coincident, the 'e' coefficient should be zero. Explicitly setting it to
1100
  // zero helps avoid numerical issues below with root finding.
1101
  double coeff[5];
19,297,864 ✔
1102
  coeff[0] = coincident ? 0.0 : c0 * c0 - c0p;
19,297,864 ✔
1103
  coeff[1] = 2 * c0 * c1 - c1p;
19,297,864 ✔
1104
  coeff[2] = c1 * c1 + 2 * c0 * c2 - c2p;
19,297,864 ✔
1105
  coeff[3] = 2 * c1 * c2;
19,297,864 ✔
1106
  coeff[4] = c2 * c2;
19,297,864 ✔
1107

1108
  std::complex<double> roots[4];
19,297,864 ✔
1109
  oqs::quartic_solver(coeff, roots);
19,297,864 ✔
1110

1111
  // Find smallest positive, real root. In the case where the particle is
1112
  // coincident with the surface, we are sure to have one root very close to
1113
  // zero but possibly small and positive. A tolerance is set to discard that
1114
  // zero.
1115
  double distance = INFTY;
19,297,864 ✔
1116
  double cutoff = coincident ? TORUS_TOL : 0.0;
19,297,864 ✔
1117
  for (int i = 0; i < 4; ++i) {
96,489,320 ✔
1118
    if (roots[i].imag() == 0) {
77,191,456 ✔
1119
      double root = roots[i].real();
18,868,384 ✔
1120
      if (root > cutoff && root < distance) {
18,868,384 ✔
1121
        // Avoid roots corresponding to internal surfaces
1122
        double s1 = x1 + u1 * root;
7,572,360 ✔
1123
        double s2 = x2 + u2 * root;
7,572,360 ✔
1124
        double s3 = x3 + u3 * root;
7,572,360 ✔
1125
        double check = D * s3 * s3 + s1 * s1 + s2 * s2 + A * A - C * C;
7,572,360 ✔
1126
        if (check >= 0) {
7,572,360 !
1127
          distance = root;
7,572,360 ✔
1128
        }
1129
      }
1130
    }
1131
  }
1132
  return distance;
19,297,864 ✔
1133
}
1134

1135
//==============================================================================
1136
// SurfaceXTorus implementation
1137
//==============================================================================
1138

1139
SurfaceXTorus::SurfaceXTorus(pugi::xml_node surf_node) : Surface(surf_node)
50 ✔
1140
{
1141
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &A_, &B_, &C_});
50 ✔
1142
}
50 ✔
1143

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

1151
double SurfaceXTorus::evaluate(Position r) const
590,005 ✔
1152
{
1153
  double x = r.x - x0_;
590,005 ✔
1154
  double y = r.y - y0_;
590,005 ✔
1155
  double z = r.z - z0_;
590,005 ✔
1156
  return (x * x) / (B_ * B_) +
1,180,010 ✔
1157
         std::pow(std::sqrt(y * y + z * z) - A_, 2) / (C_ * C_) - 1.;
590,005 ✔
1158
}
1159

1160
BoundingBox SurfaceXTorus::bounding_box(bool pos_side) const
16 ✔
1161
{
1162
  // The torus interior is compact: it extends +/-B_ along the axis of
1163
  // revolution and +/-(A_ + C_) in the two perpendicular directions. Mirrors
1164
  // XTorus.bounding_box on the Python side.
1165
  if (pos_side)
16 ✔
1166
    return {};
8 ✔
1167
  return {{x0_ - B_, y0_ - A_ - C_, z0_ - A_ - C_},
8 ✔
1168
    {x0_ + B_, y0_ + A_ + C_, z0_ + A_ + C_}};
8 ✔
1169
}
1170

1171
double SurfaceXTorus::distance(Position r, Direction u, bool coincident) const
6,071,720 ✔
1172
{
1173
  double x = r.x - x0_;
6,071,720 ✔
1174
  double y = r.y - y0_;
6,071,720 ✔
1175
  double z = r.z - z0_;
6,071,720 ✔
1176
  return torus_distance(y, z, x, u.y, u.z, u.x, A_, B_, C_, coincident);
6,071,720 ✔
1177
}
1178

1179
Direction SurfaceXTorus::normal(Position r) const
×
1180
{
1181
  // reduce the expansion of the full form for torus
1182
  double x = r.x - x0_;
×
1183
  double y = r.y - y0_;
×
1184
  double z = r.z - z0_;
×
1185

1186
  // f(x,y,z) = x^2/B^2 + (sqrt(y^2 + z^2) - A)^2/C^2 - 1
1187
  // ∂f/∂x = 2x/B^2
1188
  // ∂f/∂y = 2y(g - A)/(g*C^2) where g = sqrt(y^2 + z^2)
1189
  // ∂f/∂z = 2z(g - A)/(g*C^2)
1190
  // Multiplying by g*C^2*B^2 / 2 gives:
1191
  double g = std::sqrt(y * y + z * z);
×
1192
  double nx = C_ * C_ * g * x;
×
1193
  double ny = y * (g - A_) * B_ * B_;
×
1194
  double nz = z * (g - A_) * B_ * B_;
×
1195
  Direction n(nx, ny, nz);
×
1196
  return n / n.norm();
×
1197
}
1198

1199
//==============================================================================
1200
// SurfaceYTorus implementation
1201
//==============================================================================
1202

1203
SurfaceYTorus::SurfaceYTorus(pugi::xml_node surf_node) : Surface(surf_node)
50 ✔
1204
{
1205
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &A_, &B_, &C_});
50 ✔
1206
}
50 ✔
1207

1208
void SurfaceYTorus::to_hdf5_inner(hid_t group_id) const
40 ✔
1209
{
1210
  write_string(group_id, "type", "y-torus", false);
40 ✔
1211
  std::array<double, 6> coeffs {{x0_, y0_, z0_, A_, B_, C_}};
40 ✔
1212
  write_dataset(group_id, "coefficients", coeffs);
40 ✔
1213
}
40 ✔
1214

1215
double SurfaceYTorus::evaluate(Position r) const
561,712 ✔
1216
{
1217
  double x = r.x - x0_;
561,712 ✔
1218
  double y = r.y - y0_;
561,712 ✔
1219
  double z = r.z - z0_;
561,712 ✔
1220
  return (y * y) / (B_ * B_) +
1,123,424 ✔
1221
         std::pow(std::sqrt(x * x + z * z) - A_, 2) / (C_ * C_) - 1.;
561,712 ✔
1222
}
1223

1224
BoundingBox SurfaceYTorus::bounding_box(bool pos_side) const
16 ✔
1225
{
1226
  if (pos_side)
16 ✔
1227
    return {};
8 ✔
1228
  return {{x0_ - A_ - C_, y0_ - B_, z0_ - A_ - C_},
8 ✔
1229
    {x0_ + A_ + C_, y0_ + B_, z0_ + A_ + C_}};
8 ✔
1230
}
1231

1232
double SurfaceYTorus::distance(Position r, Direction u, bool coincident) const
6,119,200 ✔
1233
{
1234
  double x = r.x - x0_;
6,119,200 ✔
1235
  double y = r.y - y0_;
6,119,200 ✔
1236
  double z = r.z - z0_;
6,119,200 ✔
1237
  return torus_distance(x, z, y, u.x, u.z, u.y, A_, B_, C_, coincident);
6,119,200 ✔
1238
}
1239

1240
Direction SurfaceYTorus::normal(Position r) const
×
1241
{
1242
  // reduce the expansion of the full form for torus
1243
  double x = r.x - x0_;
×
1244
  double y = r.y - y0_;
×
1245
  double z = r.z - z0_;
×
1246

1247
  // f(x,y,z) = y^2/B^2 + (sqrt(x^2 + z^2) - A)^2/C^2 - 1
1248
  // ∂f/∂x = 2x(g - A)/(g*C^2) where g = sqrt(x^2 + z^2)
1249
  // ∂f/∂y = 2y/B^2
1250
  // ∂f/∂z = 2z(g - A)/(g*C^2)
1251
  // Multiplying by g*C^2*B^2 / 2 gives:
1252
  double g = std::sqrt(x * x + z * z);
×
1253
  double nx = x * (g - A_) * B_ * B_;
×
1254
  double ny = C_ * C_ * g * y;
×
1255
  double nz = z * (g - A_) * B_ * B_;
×
1256
  Direction n(nx, ny, nz);
×
1257
  return n / n.norm();
×
1258
}
1259

1260
//==============================================================================
1261
// SurfaceZTorus implementation
1262
//==============================================================================
1263

1264
SurfaceZTorus::SurfaceZTorus(pugi::xml_node surf_node) : Surface(surf_node)
80 ✔
1265
{
1266
  read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &A_, &B_, &C_});
80 ✔
1267
}
80 ✔
1268

1269
void SurfaceZTorus::to_hdf5_inner(hid_t group_id) const
64 ✔
1270
{
1271
  write_string(group_id, "type", "z-torus", false);
64 ✔
1272
  std::array<double, 6> coeffs {{x0_, y0_, z0_, A_, B_, C_}};
64 ✔
1273
  write_dataset(group_id, "coefficients", coeffs);
64 ✔
1274
}
64 ✔
1275

1276
double SurfaceZTorus::evaluate(Position r) const
954,131 ✔
1277
{
1278
  double x = r.x - x0_;
954,131 ✔
1279
  double y = r.y - y0_;
954,131 ✔
1280
  double z = r.z - z0_;
954,131 ✔
1281
  return (z * z) / (B_ * B_) +
1,908,262 ✔
1282
         std::pow(std::sqrt(x * x + y * y) - A_, 2) / (C_ * C_) - 1.;
954,131 ✔
1283
}
1284

1285
BoundingBox SurfaceZTorus::bounding_box(bool pos_side) const
16 ✔
1286
{
1287
  if (pos_side)
16 ✔
1288
    return {};
8 ✔
1289
  return {{x0_ - A_ - C_, y0_ - A_ - C_, z0_ - B_},
8 ✔
1290
    {x0_ + A_ + C_, y0_ + A_ + C_, z0_ + B_}};
8 ✔
1291
}
1292

1293
double SurfaceZTorus::distance(Position r, Direction u, bool coincident) const
7,106,944 ✔
1294
{
1295
  double x = r.x - x0_;
7,106,944 ✔
1296
  double y = r.y - y0_;
7,106,944 ✔
1297
  double z = r.z - z0_;
7,106,944 ✔
1298
  return torus_distance(x, y, z, u.x, u.y, u.z, A_, B_, C_, coincident);
7,106,944 ✔
1299
}
1300

1301
Direction SurfaceZTorus::normal(Position r) const
×
1302
{
1303
  // reduce the expansion of the full form for torus
1304
  double x = r.x - x0_;
×
1305
  double y = r.y - y0_;
×
1306
  double z = r.z - z0_;
×
1307

1308
  // f(x,y,z) = z^2/B^2 + (sqrt(x^2 + y^2) - A)^2/C^2 - 1
1309
  // ∂f/∂x = 2x(g - A)/(g*C^2) where g = sqrt(x^2 + y^2)
1310
  // ∂f/∂y = 2y(g - A)/(g*C^2)
1311
  // ∂f/∂z = 2z/B^2
1312
  // Multiplying by g*C^2*B^2 / 2 gives:
1313
  double g = std::sqrt(x * x + y * y);
×
1314
  double nx = x * (g - A_) * B_ * B_;
×
1315
  double ny = y * (g - A_) * B_ * B_;
×
1316
  double nz = C_ * C_ * g * z;
×
1317
  Position n(nx, ny, nz);
×
1318
  return n / n.norm();
×
1319
}
1320

1321
//==============================================================================
1322

1323
void read_surfaces(pugi::xml_node node,
6,846 ✔
1324
  std::set<std::pair<int, int>>& periodic_pairs,
1325
  std::unordered_map<int, double>& albedo_map,
1326
  std::unordered_map<int, int>& periodic_sense_map)
1327
{
1328
  // Count the number of surfaces
1329
  auto surf_nodes = node.children("surface");
6,846 ✔
1330
  int n_surfaces = std::distance(surf_nodes.begin(), surf_nodes.end());
6,846 ✔
1331

1332
  // Loop over XML surface elements and populate the array.  Keep track of
1333
  // periodic surfaces and their albedos.
1334
  model::surfaces.reserve(n_surfaces);
6,846 ✔
1335
  {
6,846 ✔
1336
    pugi::xml_node surf_node;
6,846 ✔
1337
    int i_surf;
6,846 ✔
1338
    for (surf_node = node.child("surface"), i_surf = 0; surf_node;
42,559 ✔
1339
         surf_node = surf_node.next_sibling("surface"), i_surf++) {
35,713 ✔
1340
      std::string surf_type = get_node_value(surf_node, "type", true, true);
35,713 ✔
1341

1342
      // Allocate and initialize the new surface
1343

1344
      if (surf_type == "x-plane") {
35,713 ✔
1345
        model::surfaces.push_back(make_unique<SurfaceXPlane>(surf_node));
9,005 ✔
1346

1347
      } else if (surf_type == "y-plane") {
26,708 ✔
1348
        model::surfaces.push_back(make_unique<SurfaceYPlane>(surf_node));
8,182 ✔
1349

1350
      } else if (surf_type == "z-plane") {
18,526 ✔
1351
        model::surfaces.push_back(make_unique<SurfaceZPlane>(surf_node));
6,064 ✔
1352

1353
      } else if (surf_type == "plane") {
12,462 ✔
1354
        model::surfaces.push_back(make_unique<SurfacePlane>(surf_node));
1,420 ✔
1355

1356
      } else if (surf_type == "x-cylinder") {
11,042 ✔
1357
        model::surfaces.push_back(make_unique<SurfaceXCylinder>(surf_node));
30 ✔
1358

1359
      } else if (surf_type == "y-cylinder") {
11,012 ✔
1360
        model::surfaces.push_back(make_unique<SurfaceYCylinder>(surf_node));
18 ✔
1361

1362
      } else if (surf_type == "z-cylinder") {
10,994 ✔
1363
        model::surfaces.push_back(make_unique<SurfaceZCylinder>(surf_node));
4,041 ✔
1364

1365
      } else if (surf_type == "sphere") {
6,953 ✔
1366
        model::surfaces.push_back(make_unique<SurfaceSphere>(surf_node));
6,569 ✔
1367

1368
      } else if (surf_type == "x-cone") {
384 !
1369
        model::surfaces.push_back(make_unique<SurfaceXCone>(surf_node));
×
1370

1371
      } else if (surf_type == "y-cone") {
384 !
1372
        model::surfaces.push_back(make_unique<SurfaceYCone>(surf_node));
×
1373

1374
      } else if (surf_type == "z-cone") {
384 ✔
1375
        model::surfaces.push_back(make_unique<SurfaceZCone>(surf_node));
10 ✔
1376

1377
      } else if (surf_type == "quadric") {
374 ✔
1378
        model::surfaces.push_back(make_unique<SurfaceQuadric>(surf_node));
218 ✔
1379

1380
      } else if (surf_type == "x-torus") {
156 ✔
1381
        model::surfaces.push_back(std::make_unique<SurfaceXTorus>(surf_node));
42 ✔
1382

1383
      } else if (surf_type == "y-torus") {
114 ✔
1384
        model::surfaces.push_back(std::make_unique<SurfaceYTorus>(surf_node));
42 ✔
1385

1386
      } else if (surf_type == "z-torus") {
72 !
1387
        model::surfaces.push_back(std::make_unique<SurfaceZTorus>(surf_node));
72 ✔
1388

1389
      } else {
1390
        fatal_error(fmt::format("Invalid surface type, \"{}\"", surf_type));
×
1391
      }
1392

1393
      // Check for a periodic surface
1394
      if (check_for_node(surf_node, "boundary")) {
35,713 ✔
1395
        std::string surf_bc = get_node_value(surf_node, "boundary", true, true);
21,320 ✔
1396
        if (surf_bc == "periodic") {
21,320 ✔
1397
          periodic_sense_map[model::surfaces.back()->id_] = 0;
376 ✔
1398
          // Check for surface albedo. Skip sanity check as it is already done
1399
          // in the Surface class's constructor.
1400
          if (check_for_node(surf_node, "albedo")) {
376 !
1401
            albedo_map[model::surfaces.back()->id_] =
×
1402
              std::stod(get_node_value(surf_node, "albedo"));
×
1403
          }
1404
          if (check_for_node(surf_node, "periodic_surface_id")) {
376 ✔
1405
            int i_periodic =
248 ✔
1406
              std::stoi(get_node_value(surf_node, "periodic_surface_id"));
496 ✔
1407
            int lo_id = std::min(model::surfaces.back()->id_, i_periodic);
248 ✔
1408
            int hi_id = std::max(model::surfaces.back()->id_, i_periodic);
248 ✔
1409
            periodic_pairs.insert({lo_id, hi_id});
248 ✔
1410
          } else {
1411
            periodic_pairs.insert({model::surfaces.back()->id_, -1});
128 ✔
1412
          }
1413
        }
1414
      }
21,320 ✔
1415
    }
35,713 ✔
1416
  }
1417

1418
  // Fill the surface map
1419
  for (int i_surf = 0; i_surf < model::surfaces.size(); i_surf++) {
42,559 ✔
1420
    int id = model::surfaces[i_surf]->id_;
35,713 !
1421
    auto in_map = model::surface_map.find(id);
35,713 !
1422
    if (in_map == model::surface_map.end()) {
35,713 !
1423
      model::surface_map[id] = i_surf;
35,713 ✔
1424
    } else {
1425
      fatal_error(
×
1426
        fmt::format("Two or more surfaces use the same unique ID: {}", id));
×
1427
    }
1428
  }
1429
}
6,846 ✔
1430

1431
void prepare_boundary_conditions(std::set<std::pair<int, int>>& periodic_pairs,
6,846 ✔
1432
  std::unordered_map<int, double>& albedo_map,
1433
  std::unordered_map<int, int>& periodic_sense_map)
1434
{
1435
  // Fill the senses map for periodic surfaces
1436
  auto n_periodic = periodic_sense_map.size();
6,846 ✔
1437
  for (const auto& cell : model::cells) {
7,024 ✔
1438
    if (n_periodic == 0)
6,932 ✔
1439
      break; // Early exit once all periodic surfaces found
1440

1441
    for (auto s : cell->surfaces()) {
920 ✔
1442
      auto surf_idx = std::abs(s) - 1;
742 ✔
1443
      auto id = model::surfaces[surf_idx]->id_;
742 ✔
1444

1445
      if (periodic_sense_map.count(id)) {
742 ✔
1446
        periodic_sense_map[id] = std::copysign(1, s);
376 ✔
1447
        --n_periodic;
376 ✔
1448
      }
1449
    }
178 ✔
1450
  }
1451

1452
  // Resolve unpaired periodic surfaces.  A lambda function is used with
1453
  // std::find_if to identify the unpaired surfaces.
1454
  auto is_unresolved_pair = [](const std::pair<int, int> p) {
7,098 ✔
1455
    return p.second == -1;
252 ✔
1456
  };
1457
  auto first_unresolved = std::find_if(
6,846 ✔
1458
    periodic_pairs.begin(), periodic_pairs.end(), is_unresolved_pair);
1459
  if (first_unresolved != periodic_pairs.end()) {
6,846 ✔
1460
    // Found one unpaired surface; search for a second one
1461
    auto next_elem = first_unresolved;
64 ✔
1462
    next_elem++;
64 !
1463
    auto second_unresolved =
64 !
1464
      std::find_if(next_elem, periodic_pairs.end(), is_unresolved_pair);
64 !
1465
    if (second_unresolved == periodic_pairs.end()) {
64 !
1466
      fatal_error("Found only one periodic surface without a specified partner."
×
1467
                  " Please specify the partner for each periodic surface.");
1468
    }
1469

1470
    // Make sure there isn't a third unpaired surface
1471
    next_elem = second_unresolved;
64 ✔
1472
    next_elem++;
64 !
1473
    auto third_unresolved =
64 !
1474
      std::find_if(next_elem, periodic_pairs.end(), is_unresolved_pair);
64 !
1475
    if (third_unresolved != periodic_pairs.end()) {
64 !
1476
      fatal_error(
×
1477
        "Found at least three periodic surfaces without a specified "
1478
        "partner. Please specify the partner for each periodic surface.");
1479
    }
1480

1481
    // Add the completed pair and remove the old, unpaired entries
1482
    int lo_id = std::min(first_unresolved->first, second_unresolved->first);
64 !
1483
    int hi_id = std::max(first_unresolved->first, second_unresolved->first);
64 !
1484
    periodic_pairs.insert({lo_id, hi_id});
64 ✔
1485
    periodic_pairs.erase(first_unresolved);
64 ✔
1486
    periodic_pairs.erase(second_unresolved);
64 ✔
1487
  }
1488

1489
  // Assign the periodic boundary conditions with albedos
1490
  for (auto periodic_pair : periodic_pairs) {
7,034 ✔
1491
    int i_surf = model::surface_map[periodic_pair.first];
188 ✔
1492
    int j_surf = model::surface_map[periodic_pair.second];
188 ✔
1493
    Surface& surf1 {*model::surfaces[i_surf]};
188 ✔
1494
    Surface& surf2 {*model::surfaces[j_surf]};
188 ✔
1495

1496
    // Compute the dot product of the surface normals
1497
    Direction norm1 = surf1.normal({0, 0, 0});
188 ✔
1498
    Direction norm2 = surf2.normal({0, 0, 0});
188 ✔
1499
    norm1 /= norm1.norm();
188 ✔
1500
    norm2 /= norm2.norm();
188 ✔
1501
    double dot_prod = norm1.dot(norm2);
188 ✔
1502

1503
    // If the dot product is 1 (to within floating point precision) then the
1504
    // planes are parallel which indicates a translational periodic boundary
1505
    // condition.  Otherwise, it is a rotational periodic BC.
1506
    if (std::abs(1.0 - dot_prod) < FP_PRECISION) {
188 ✔
1507
      surf1.bc_ = make_unique<TranslationalPeriodicBC>(i_surf, j_surf);
78 ✔
1508
      surf2.bc_ = make_unique<TranslationalPeriodicBC>(j_surf, i_surf);
78 ✔
1509
    } else {
1510
      // check that both normals have at least one 0 component
1511
      if (std::abs(norm1.x) > FP_PRECISION &&
110 ✔
1512
          std::abs(norm1.y) > FP_PRECISION &&
110 !
1513
          std::abs(norm1.z) > FP_PRECISION) {
80 !
1514
        fatal_error(fmt::format(
×
1515
          "The normal ({}) of the periodic surface ({}) does not contain any "
1516
          "component with a zero value. A RotationalPeriodicBC requires one "
1517
          "component which is zero for both plane normals.",
1518
          norm1, i_surf));
1519
      }
1520
      if (std::abs(norm2.x) > FP_PRECISION &&
110 ✔
1521
          std::abs(norm2.y) > FP_PRECISION &&
110 !
1522
          std::abs(norm2.z) > FP_PRECISION) {
80 !
1523
        fatal_error(fmt::format(
×
1524
          "The normal ({}) of the periodic surface ({}) does not contain any "
1525
          "component with a zero value. A RotationalPeriodicBC requires one "
1526
          "component which is zero for both plane normals.",
1527
          norm2, j_surf));
1528
      }
1529
      // find common zero component, which indicates the periodic axis
1530
      RotationalPeriodicBC::PeriodicAxis axis;
110 ✔
1531
      if (std::abs(norm1.x) <= FP_PRECISION &&
110 ✔
1532
          std::abs(norm2.x) <= FP_PRECISION) {
10 !
1533
        axis = RotationalPeriodicBC::PeriodicAxis::x;
10 ✔
1534
      } else if (std::abs(norm1.y) <= FP_PRECISION &&
100 ✔
1535
                 std::abs(norm2.y) <= FP_PRECISION) {
20 ✔
1536
        axis = RotationalPeriodicBC::PeriodicAxis::y;
10 ✔
1537
      } else if (std::abs(norm1.z) <= FP_PRECISION &&
90 !
1538
                 std::abs(norm2.z) <= FP_PRECISION) {
90 !
1539
        axis = RotationalPeriodicBC::PeriodicAxis::z;
90 ✔
1540
      } else {
1541
        fatal_error(fmt::format(
×
1542
          "There is no component which is 0.0 in both normal vectors. This "
1543
          "indicates that the two planes are not periodic about the X, Y, or Z "
1544
          "axis, which is not supported."));
1545
      }
1546
      auto i_sign = periodic_sense_map[periodic_pair.first];
110 ✔
1547
      auto j_sign = periodic_sense_map[periodic_pair.second];
110 ✔
1548
      surf1.bc_ = make_unique<RotationalPeriodicBC>(
110 ✔
1549
        i_sign * (i_surf + 1), j_sign * (j_surf + 1), axis);
110 ✔
1550
      surf2.bc_ = make_unique<RotationalPeriodicBC>(
110 ✔
1551
        j_sign * (j_surf + 1), i_sign * (i_surf + 1), axis);
220 ✔
1552
    }
1553

1554
    // If albedo data is present in albedo map, set the boundary albedo.
1555
    if (albedo_map.count(surf1.id_)) {
188 !
1556
      surf1.bc_->set_albedo(albedo_map[surf1.id_]);
×
1557
    }
1558
    if (albedo_map.count(surf2.id_)) {
188 !
1559
      surf2.bc_->set_albedo(albedo_map[surf2.id_]);
188 ✔
1560
    }
1561
  }
1562
}
6,846 ✔
1563

1564
void free_memory_surfaces()
6,939 ✔
1565
{
1566
  model::surfaces.clear();
6,939 ✔
1567
  model::surface_map.clear();
6,939 ✔
1568
}
6,939 ✔
1569

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

© 2026 Coveralls, Inc