EGSnrc C++ class library  Report PIRS-898 (2021)
Iwan Kawrakow, Ernesto Mainegra-Hing, Frederic Tessier, Reid Townson and Blake Walters
egs_mesh.cpp
1 /*
2 ###############################################################################
3 #
4 # EGSnrc egs++ mesh geometry library implementation.
5 # Copyright (C) 2020 Mevex Corporation
6 #
7 # This file is part of EGSnrc.
8 
9 # EGSnrc is free software: you can redistribute it and/or modify it under
10 # the terms of the GNU Affero General Public License as published by the
11 # Free Software Foundation, either version 3 of the License, or (at your
12 # option) any later version.
13 #
14 # EGSnrc is distributed in the hope that it will be useful, but WITHOUT ANY
15 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16 # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
17 # more details.
18 #
19 # You should have received a copy of the GNU Affero General Public License
20 # along with EGSnrc. If not, see <http://www.gnu.org/licenses/>.
21 #
22 ###############################################################################
23 #
24 # Authors: Dave Macrillo, 2020
25 # Matt Ronan,
26 # Nigel Vezeau,
27 # Lou Thompson,
28 # Max Orok
29 #
30 # Contributors: Pascal Michaud
31 #
32 ###############################################################################
33 #
34 # The original EGS_Mesh geometry started as a product of many people's hard
35 # work at Mevex Corporation. Thank you to the entire team. Special thanks to
36 # Dave Brown and Emily Craven for their support in releasing this work
37 # under a public licence.
38 #
39 # The EGS_Mesh library was then significantly extended and refactored by
40 # Max Orok, as the subject of his Masters degree in Applied Science at the
41 # University of Ottawa, in the department of Mechanical Engineering and under
42 # the supervision of Professor James McDonald.
43 #
44 # Parts of this file, namely, the closest_point_triangle and
45 # closest_point_tetrahedron functions, are adapted from Chapter 5 of
46 # "Real-Time Collision Detection" by Christer Ericson with the permission
47 # from the author and the publisher.
48 #
49 ###############################################################################
50 */
51 
52 
53 #include "egs_input.h"
54 #include "egs_mesh.h"
55 #include "egs_vector.h"
56 
57 #include "mesh_neighbours.h"
58 #include "msh_parser.h"
59 #include "tetgen_parser.h"
60 
61 #include <cassert>
62 #include <chrono>
63 #include <deque>
64 #include <limits>
65 #include <stdexcept>
66 #include <unordered_map>
67 
68 #ifndef WIN32
69  #include <unistd.h> // isatty
70 #else
71  #include <io.h> // _isatty
72 #endif
73 
74 // Have to define the move constructor, move assignment operator and destructor
75 // here instead of in egs_mesh.h because of the unique_ptr to forward declared
76 // EGS_Mesh_Octree members.
77 EGS_Mesh::~EGS_Mesh() = default;
78 EGS_Mesh::EGS_Mesh(EGS_Mesh &&) = default;
79 EGS_Mesh &EGS_Mesh::operator=(EGS_Mesh &&) = default;
80 
81 static bool EGS_MESH_LOCAL inputSet = false;
82 
84  std::size_t n_max = std::numeric_limits<int>::max();
85  if (this->elements.size() >= n_max) {
86  throw std::runtime_error("maximum number of elements (" +
87  std::to_string(n_max) + ") exceeded (" +
88  std::to_string(this->elements.size()) + ")");
89  }
90  if (this->nodes.size() >= n_max) {
91  throw std::runtime_error("maximum number of nodes (" +
92  std::to_string(n_max) + ") exceeded (" +
93  std::to_string(this->nodes.size()) + ")");
94  }
95 }
96 
97 // exclude from doxygen
99 namespace egs_mesh {
100 namespace internal {
101 
102 PercentCounter::PercentCounter(EGS_InfoFunction info, const std::string &msg)
103  : info_(info), msg_(msg) {}
104 
105 void PercentCounter::start(EGS_Float goal) {
106  goal_ = goal;
107  t_start_ = std::chrono::system_clock::now();
108 #ifndef WIN32
109  interactive_ = isatty(STDOUT_FILENO);
110 #else
111  interactive_ = _isatty(STDOUT_FILENO);
112 #endif
113  if (!info_ || !interactive_) {
114  return;
115  }
116  info_("\r%s (0%%)", msg_.c_str());
117 }
118 
119 // Assumes delta is positive
120 void PercentCounter::step(EGS_Float delta) {
121  if (!info_ || !interactive_) {
122  return;
123  }
124  progress_ += delta;
125  const int percent = static_cast<int>((progress_ / goal_) * 100.0);
126  if (percent > old_percent_) {
127  info_("\r%s (%d%%)", msg_.c_str(), percent);
128  old_percent_ = percent;
129  }
130 }
131 
132 void PercentCounter::finish(const std::string &end_msg) {
133  if (!info_) {
134  return;
135  }
136  const auto t_end = std::chrono::system_clock::now();
137  const std::chrono::duration<float> elapsed = t_end - t_start_;
138  if (!interactive_) {
139  info_("%s in %0.3fs\n", end_msg.c_str(), elapsed.count());
140  return;
141  }
142  // overwrite any remaining text
143  info_("\r ");
144  info_("\r%s in %0.3fs\n", end_msg.c_str(), elapsed.count());
145 }
146 
147 } // namespace internal
148 } // namespace egs_mesh
149 
151 
152 // anonymous namespace
153 namespace {
154 
155 const EGS_Float eps = 1e-8;
156 
157 inline bool approx_eq(double a, double b, double e = eps) {
158  return (std::abs(a - b) <= e * (std::abs(a) + std::abs(b) + 1.0));
159 }
160 
161 inline bool is_zero(const EGS_Vector &v) {
162  return approx_eq(0.0, v.length(), eps);
163 }
164 
165 inline EGS_Float min3(EGS_Float a, EGS_Float b, EGS_Float c) {
166  return std::min(std::min(a, b), c);
167 }
168 
169 inline EGS_Float max3(EGS_Float a, EGS_Float b, EGS_Float c) {
170  return std::max(std::max(a, b), c);
171 }
172 
173 inline EGS_Float dot(const EGS_Vector &x, const EGS_Vector &y) {
174  return x * y;
175 }
176 
177 inline EGS_Vector cross(const EGS_Vector &x, const EGS_Vector &y) {
178  return x.times(y);
179 }
180 
181 inline EGS_Float distance2(const EGS_Vector &x, const EGS_Vector &y) {
182  return (x - y).length2();
183 }
184 
185 inline EGS_Float distance(const EGS_Vector &x, const EGS_Vector &y) {
186  return std::sqrt(distance2(x, y));
187 }
188 
189 EGS_Vector closest_point_triangle(const EGS_Vector &P, const EGS_Vector &A, const EGS_Vector &B, const EGS_Vector &C) {
190  // vertex region A
191  EGS_Vector ab = B - A;
192  EGS_Vector ac = C - A;
193  EGS_Vector ao = P - A;
194 
195  EGS_Float d1 = dot(ab, ao);
196  EGS_Float d2 = dot(ac, ao);
197  if (d1 <= 0.0 && d2 <= 0.0) {
198  return A;
199  }
200 
201  // vertex region B
202  EGS_Vector bo = P - B;
203  EGS_Float d3 = dot(ab, bo);
204  EGS_Float d4 = dot(ac, bo);
205  if (d3 >= 0.0 && d4 <= d3) {
206  return B;
207  }
208 
209  // edge region AB
210  EGS_Float vc = d1 * d4 - d3 * d2;
211  if (vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0) {
212  EGS_Float v = d1 / (d1 - d3);
213  return A + v * ab;
214  }
215 
216  // vertex region C
217  EGS_Vector co = P - C;
218  EGS_Float d5 = dot(ab, co);
219  EGS_Float d6 = dot(ac, co);
220  if (d6 >= 0.0 && d5 <= d6) {
221  return C;
222  }
223 
224  // edge region AC
225  EGS_Float vb = d5 * d2 - d1 * d6;
226  if (vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0) {
227  EGS_Float w = d2 / (d2 - d6);
228  return A + w * ac;
229  }
230 
231  // edge region BC
232  EGS_Float va = d3 * d6 - d5 * d4;
233  if (va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0) {
234  EGS_Float w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
235  return B + w * (C - B);
236  }
237 
238  // inside the face
239  EGS_Float denom = 1.0 / (va + vb + vc);
240  EGS_Float v = vb * denom;
241  EGS_Float w = vc * denom;
242  return A + v * ab + w * ac;
243 }
244 
245 // Returns true if the point is on the outside of the plane defined by ABC using
246 // reference point D, i.e. if D and P are on opposite sides of the plane of ABC.
247 inline bool point_outside_of_plane(EGS_Vector P, EGS_Vector A, EGS_Vector B, EGS_Vector C, EGS_Vector D) {
248  return dot(P - A, cross(B - A, C - A)) * dot(D - A, cross(B - A, C - A)) < 0.0;
249 }
250 
251 EGS_Vector closest_point_tetrahedron(const EGS_Vector &P, const EGS_Vector &A, const EGS_Vector &B, const EGS_Vector &C, const EGS_Vector &D) {
252  EGS_Vector min_point = P;
253  EGS_Float min = std::numeric_limits<EGS_Float>::max();
254 
255  auto maybe_update_min_point = [&](const EGS_Vector& A, const EGS_Vector& B, const EGS_Vector& C) {
256  EGS_Vector q = closest_point_triangle(P, A, B, C);
257  EGS_Float dis = distance2(q, P);
258  if (dis < min) {
259  min = dis;
260  min_point = q;
261  }
262  };
263 
264  if (point_outside_of_plane(P, A, B, C, D)) {
265  maybe_update_min_point(A, B, C);
266  }
267 
268  if (point_outside_of_plane(P, A, C, D, B)) {
269  maybe_update_min_point(A, C, D);
270  }
271 
272  if (point_outside_of_plane(P, A, B, D, C)) {
273  maybe_update_min_point(A, B, D);
274  }
275  if (point_outside_of_plane(P, B, D, C, A)) {
276  maybe_update_min_point(B, D, C);
277  }
278 
279  return min_point;
280 }
281 
296 int exterior_triangle_ray_intersection(const EGS_Vector &p,
297  const EGS_Vector &v_norm, const EGS_Vector &a, const EGS_Vector &b,
298  const EGS_Vector &c, EGS_Float &dist) {
299  const EGS_Float eps = 1e-10;
300  EGS_Vector ab = b - a;
301  EGS_Vector ac = c - a;
302 
303  EGS_Vector pvec = cross(v_norm, ac);
304  EGS_Float det = dot(ab, pvec);
305 
306  if (det > -eps && det < eps) {
307  return 0;
308  }
309  EGS_Float inv_det = 1.0 / det;
310  EGS_Vector tvec = p - a;
311  EGS_Float u = dot(tvec, pvec) * inv_det;
312  if (u < 0.0 || u > 1.0) {
313  return 0;
314  }
315  EGS_Vector qvec = cross(tvec, ab);
316  EGS_Float v = dot(v_norm, qvec) * inv_det;
317  if (v < 0.0 || u + v > 1.0) {
318  return 0;
319  }
320  // intersection found
321  dist = dot(ac, qvec) * inv_det;
322  if (dist < 0.0) {
323  return 0;
324  }
325  return 1;
326 }
327 
328 } // anonymous namespace
329 
330 // exclude from doxygen
332 class EGS_Mesh_Octree {
333 private:
334  static double tet_min_x(const EGS_Mesh::Nodes &n) {
335  return std::min(n.A.x, std::min(n.B.x, std::min(n.C.x, n.D.x)));
336  }
337  static double tet_max_x(const EGS_Mesh::Nodes &n) {
338  return std::max(n.A.x, std::max(n.B.x, std::max(n.C.x, n.D.x)));
339  }
340  static double tet_min_y(const EGS_Mesh::Nodes &n) {
341  return std::min(n.A.y, std::min(n.B.y, std::min(n.C.y, n.D.y)));
342  }
343  static double tet_max_y(const EGS_Mesh::Nodes &n) {
344  return std::max(n.A.y, std::max(n.B.y, std::max(n.C.y, n.D.y)));
345  }
346  static double tet_min_z(const EGS_Mesh::Nodes &n) {
347  return std::min(n.A.z, std::min(n.B.z, std::min(n.C.z, n.D.z)));
348  }
349  static double tet_max_z(const EGS_Mesh::Nodes &n) {
350  return std::max(n.A.z, std::max(n.B.z, std::max(n.C.z, n.D.z)));
351  }
352 
353  // An axis-aligned bounding box.
354  struct BoundingBox {
355  double min_x = 0.0;
356  double max_x = 0.0;
357  double min_y = 0.0;
358  double max_y = 0.0;
359  double min_z = 0.0;
360  double max_z = 0.0;
361  BoundingBox() = default;
362  BoundingBox(double min_x, double max_x, double min_y, double max_y,
363  double min_z, double max_z) : min_x(min_x), max_x(max_x),
364  min_y(min_y), max_y(max_y), min_z(min_z), max_z(max_z) {}
365  double mid_x() const {
366  return (min_x + max_x) / 2.0;
367  }
368  double mid_y() const {
369  return (min_y + max_y) / 2.0;
370  }
371  double mid_z() const {
372  return (min_z + max_z) / 2.0;
373  }
374  double volume() const {
375  return (max_x - min_x) * (max_y - min_y) * (max_z - min_z);
376  }
377  void expand(double delta) {
378  min_x -= delta;
379  min_y -= delta;
380  min_z -= delta;
381  max_x += delta;
382  max_y += delta;
383  max_z += delta;
384  }
385  void print(std::ostream &out = std::cout) const {
386  out <<
387  std::setprecision(std::numeric_limits<double>::max_digits10) <<
388  "min_x: " << min_x << "\n";
389  out <<
390  std::setprecision(std::numeric_limits<double>::max_digits10) <<
391  "max_x: " << max_x << "\n";
392  out <<
393  std::setprecision(std::numeric_limits<double>::max_digits10) <<
394  "min_y: " << min_y << "\n";
395  out <<
396  std::setprecision(std::numeric_limits<double>::max_digits10) <<
397  "max_y: " << max_y << "\n";
398  out <<
399  std::setprecision(std::numeric_limits<double>::max_digits10) <<
400  "min_z: " << min_z << "\n";
401  out <<
402  std::setprecision(std::numeric_limits<double>::max_digits10) <<
403  "max_z: " << max_z << "\n";
404  }
405 
406  // Adapted from Ericson section 5.2.9 "Testing AABB Against Triangle".
407  // Uses a separating axis approach, as originally presented in Akenine-
408  // Möller's "Fast 3D Triangle-Box Overlap Testing" with 13 axes checked
409  // in total. There are three axis categories, and it is suggested the
410  // fastest way to check is 3, 1, 2.
411  //
412  // We use a more straightforward but less optimized formulation of the
413  // separating axis test than Ericson presents, because this test is
414  // intended to be done as part of the octree setup but not during the
415  // actual simulation.
416  //
417  // This routine should be robust for ray edges parallel with bounding
418  // box edges (category 3) but does not attempt to be robust for the case
419  // of degenerate triangle face normals (category 2). See Ericson 5.2.1.1
420  //
421  // The non-robustness of some cases should not be an issue for the most
422  // part as these will likely be false positives (harmless extra checks)
423  // instead of false negatives (missed intersections, a huge problem if
424  // present).
425  bool intersects_triangle(const EGS_Vector &a, const EGS_Vector &b,
426  const EGS_Vector &c) const {
427  if (min3(a.x, b.x, c.x) >= max_x ||
428  min3(a.y, b.y, c.y) >= max_y ||
429  min3(a.z, b.z, c.z) >= max_z ||
430  max3(a.x, b.x, c.x) <= min_x ||
431  max3(a.y, b.y, c.y) <= min_y ||
432  max3(a.z, b.z, c.z) <= min_z) {
433  return false;
434  }
435 
436  EGS_Vector centre(mid_x(), mid_y(), mid_z());
437  // extents
438  EGS_Float ex = (max_x - min_x) / 2.0;
439  EGS_Float ey = (max_y - min_y) / 2.0;
440  EGS_Float ez = (max_z - min_z) / 2.0;
441 
442  // move triangle to bounding box origin
443  EGS_Vector v0 = a - centre;
444  EGS_Vector v1 = b - centre;
445  EGS_Vector v2 = c - centre;
446 
447  // find triangle edge vectors
448  const std::array<EGS_Vector, 3> edge_vecs { v1-v0, v2-v1, v0-v2 };
449 
450  // Test the 9 category 3 axes (cross products between axis-aligned
451  // bounding box unit vectors and triangle edge vectors)
452  const EGS_Vector ux {1, 0, 0}, uy {0, 1, 0}, uz {0, 0, 1};
453  const std::array<EGS_Vector, 3> unit_vecs { ux, uy, uz};
454  for (const EGS_Vector &u : unit_vecs) {
455  for (const EGS_Vector &f : edge_vecs) {
456  const EGS_Vector a = cross(u, f);
457  if (is_zero(a)) {
458  // Ignore testing this axis, likely won't be a separating
459  // axis. This may lead to false positives, but not false
460  // negatives.
461  continue;
462  }
463  // find box projection radius
464  const EGS_Float r = ex * std::abs(dot(ux, a)) +
465  ey * std::abs(dot(uy, a)) + ez * std::abs(dot(uz, a));
466  // find three projections onto axis a
467  const EGS_Float p0 = dot(v0, a);
468  const EGS_Float p1 = dot(v1, a);
469  const EGS_Float p2 = dot(v2, a);
470  if (std::max(-max3(p0, p1, p2), min3(p0, p1, p2)) + eps > r) {
471  return false;
472  }
473  }
474  }
475  // category 1 - test overlap with AABB face normals
476  if (max3(v0.x, v1.x, v2.x) <= -ex || min3(v0.x, v1.x, v2.x) >= ex ||
477  max3(v0.y, v1.y, v2.y) <= -ey || min3(v0.y, v1.y, v2.y) >= ey ||
478  max3(v0.z, v1.z, v2.z) <= -ez || min3(v0.z, v1.z, v2.z) >= ez) {
479  return false;
480  }
481 
482  // category 2 - test overlap with triangle face normal using AABB
483  // plane test (5.2.3)
484 
485  // Cross product robustness issues are ignored here (assume
486  // non-degenerate and non-oversize triangles)
487  const EGS_Vector n = cross(edge_vecs[0], edge_vecs[1]);
488  // projection radius
489  const EGS_Float r = ex * std::abs(n.x) + ey * std::abs(n.y) +
490  ez * std::abs(n.z);
491  // distance from box centre to plane
492  //
493  // We have to use `a` here and not `v0` as in my printing since the
494  // bounding box was not translated to the origin. This is a known
495  // erratum, see http://realtimecollisiondetection.net/books/rtcd/errata/
496  const EGS_Float s = dot(n, centre) - dot(n, a);
497  // intersection if s falls within projection radius
498  return std::abs(s) <= r;
499  }
500 
501  bool intersects_tetrahedron(const EGS_Mesh::Nodes &tet) const {
502  return intersects_triangle(tet.A, tet.B, tet.C) ||
503  intersects_triangle(tet.A, tet.C, tet.D) ||
504  intersects_triangle(tet.A, tet.B, tet.D) ||
505  intersects_triangle(tet.B, tet.C, tet.D);
506  }
507 
508  // Adapted from Ericson section 5.3.3 "Intersecting Ray or Segment
509  // Against Box".
510  //
511  // Returns 1 if there is an intersection and 0 if not. If there is an
512  // intersection, the out parameter dist will be the distance along v to
513  // the intersection point q.
514  int ray_intersection(const EGS_Vector &p, const EGS_Vector &v,
515  EGS_Float &dist, EGS_Vector &q) const {
516  // check intersection of ray with three bounding box slabs
517  EGS_Float tmin = 0.0;
518  EGS_Float tmax = std::numeric_limits<EGS_Float>::max();
519  std::array<EGS_Float, 3> p_vec {p.x, p.y, p.z};
520  std::array<EGS_Float, 3> v_vec {v.x, v.y, v.z};
521  std::array<EGS_Float, 3> mins {min_x, min_y, min_z};
522  std::array<EGS_Float, 3> maxs {max_x, max_y, max_z};
523  for (std::size_t i = 0; i < 3; i++) {
524  // Parallel to slab. Point must be within slab bounds to hit
525  // the bounding box
526  if (std::abs(v_vec[i]) < eps) {
527  // Outside slab bounds
528  if (p_vec[i] < mins[i] || p_vec[i] > maxs[i]) {
529  return 0;
530  }
531  }
532  else {
533  // intersect ray with slab planes
534  EGS_Float inv_vel = 1.0 / v_vec[i];
535  EGS_Float t1 = (mins[i] - p_vec[i]) * inv_vel;
536  EGS_Float t2 = (maxs[i] - p_vec[i]) * inv_vel;
537  // convention is t1 is near plane, t2 is far plane
538  if (t1 > t2) {
539  std::swap(t1, t2);
540  }
541  tmin = std::max(tmin, t1);
542  tmax = std::min(tmax, t2);
543  if (tmin > tmax) {
544  return 0;
545  }
546  }
547  }
548  q = p + v * tmin;
549  dist = tmin;
550  return 1;
551  }
552 
553  // Given an interior point, return the minimum distance to a boundary.
554  // This is a helper method for hownear, as the minimum of the boundary
555  // distance and tetrahedron distance will be hownear's result.
556  //
557  // TODO maybe need to clamp to 0.0 here for results slightly under zero?
558  EGS_Float min_interior_distance(const EGS_Vector &point) const {
559  return std::min(point.x - min_x, std::min(point.y - min_y,
560  std::min(point.z - min_z, std::min(max_x - point.x,
561  std::min(max_y - point.y, max_z - point.z)))));
562  }
563 
564  // Returns the closest point on the bounding box to the given point.
565  // If the given point is inside the bounding box, it is considered the
566  // closest point. This method is intended for use by hownear, to decide
567  // where to search first.
568  //
569  // See section 5.1.3 Ericson.
570  EGS_Vector closest_point(const EGS_Vector &point) const {
571  std::array<EGS_Float, 3> p = {point.x, point.y, point.z};
572  std::array<EGS_Float, 3> mins = {min_x, min_y, min_z};
573  std::array<EGS_Float, 3> maxs = {max_x, max_y, max_z};
574  // set q to p, then clamp it to min/max bounds as needed
575  std::array<EGS_Float, 3> q = p;
576  for (int i = 0; i < 3; i++) {
577  if (p[i] < mins[i]) {
578  q[i] = mins[i];
579  }
580  if (p[i] > maxs[i]) {
581  q[i] = maxs[i];
582  }
583  }
584  return EGS_Vector(q[0], q[1], q[2]);
585  }
586 
587  bool contains(const EGS_Vector &point) const {
588  // Inclusive at the lower bound, non-inclusive at the upper bound,
589  // so points on the interface between two bounding boxes only belong
590  // to one of them:
591  //
592  // +---+---+
593  // | x |
594  // +---+---+
595  // ^ belongs here
596  //
597  return point.x >= min_x && point.x < max_x &&
598  point.y >= min_y && point.y < max_y &&
599  point.z >= min_z && point.z < max_z;
600  }
601 
602  bool is_indivisible() const {
603  // check if we're running up against precision limits
604  return approx_eq(min_x, mid_x()) ||
605  approx_eq(max_x, mid_x()) ||
606  approx_eq(min_y, mid_y()) ||
607  approx_eq(max_y, mid_y()) ||
608  approx_eq(min_z, mid_z()) ||
609  approx_eq(max_z, mid_z());
610 
611  }
612 
613  // Split into 8 equal octants. Octant numbering follows an S, i.e:
614  //
615  // -z +z
616  // +---+---+ +---+---+
617  // | 2 | 3 | | 6 | 7 |
618  // y +---+---+ +---+---+
619  // ^ | 0 | 1 | | 4 | 5 |
620  // | +---+---+ +---+---+
621  // + -- > x
622  //
623  std::array<BoundingBox, 8> divide8() const {
624  return {
625  BoundingBox(
626  min_x, mid_x(),
627  min_y, mid_y(),
628  min_z, mid_z()
629  ),
630  BoundingBox(
631  mid_x(), max_x,
632  min_y, mid_y(),
633  min_z, mid_z()
634  ),
635  BoundingBox(
636  min_x, mid_x(),
637  mid_y(), max_y,
638  min_z, mid_z()
639  ),
640  BoundingBox(
641  mid_x(), max_x,
642  mid_y(), max_y,
643  min_z, mid_z()
644  ),
645  BoundingBox(
646  min_x, mid_x(),
647  min_y, mid_y(),
648  mid_z(), max_z
649  ),
650  BoundingBox(
651  mid_x(), max_x,
652  min_y, mid_y(),
653  mid_z(), max_z
654  ),
655  BoundingBox(
656  min_x, mid_x(),
657  mid_y(), max_y,
658  mid_z(), max_z
659  ),
660  BoundingBox(
661  mid_x(), max_x,
662  mid_y(), max_y,
663  mid_z(), max_z
664  )
665  };
666  }
667  };
668  struct Node {
669  std::vector<int> elts_;
670  std::vector<Node> children_;
671  BoundingBox bbox_;
672 
673  Node() = default;
674  Node(const std::vector<int> &elts, const BoundingBox &bbox,
675  std::size_t n_max, const EGS_Mesh &mesh,
676  egs_mesh::internal::PercentCounter &progress) : bbox_(bbox) {
677  // TODO: max level and precision warning
678  if (bbox_.is_indivisible() || elts.size() < n_max) {
679  elts_ = elts;
680  progress.step(bbox_.volume());
681  return;
682  }
683 
684  std::array<std::vector<int>, 8> octants;
685  std::array<BoundingBox, 8> bbs = bbox_.divide8();
686 
687  // elements may be in more than one bounding box
688  for (const auto &e : elts) {
689  for (int i = 0; i < 8; i++) {
690  if (bbs[i].intersects_tetrahedron(mesh.element_nodes(e))) {
691  octants[i].push_back(e);
692  }
693  }
694  }
695  for (int i = 0; i < 8; i++) {
696  children_.push_back(Node(
697  std::move(octants[i]), bbs[i], n_max, mesh, progress
698  ));
699  }
700  }
701 
702  bool isLeaf() const {
703  return children_.empty();
704  }
705 
706  void print(std::ostream &out, int level) const {
707  out << "Level " << level << "\n";
708  bbox_.print(out);
709  if (children_.empty()) {
710  out << "num_elts: " << elts_.size() << "\n";
711  for (const auto &e: elts_) {
712  out << e << " ";
713  }
714  out << "\n";
715  return;
716  }
717  for (int i = 0; i < 8; i++) {
718  children_.at(i).print(out, level + 1);
719  }
720  }
721 
722  int findOctant(const EGS_Vector &p) const {
723  // Our choice of octant ordering (see BoundingBox.divide8) means we
724  // can determine the correct octant with three checks. E.g. octant 0
725  // is (-x, -y, -z), octant 1 is (+x, -y, -z), octant 4 is (-x, -y, +z)
726  // octant 7 is (+x, +y, +z), etc.
727  std::size_t octant = 0;
728  if (p.x >= bbox_.mid_x()) {
729  octant += 1;
730  };
731  if (p.y >= bbox_.mid_y()) {
732  octant += 2;
733  };
734  if (p.z >= bbox_.mid_z()) {
735  octant += 4;
736  };
737  return octant;
738  }
739 
740  // Octants are returned ordered by minimum intersection distance
741  std::vector<int> findOtherIntersectedOctants(const EGS_Vector &p,
742  const EGS_Vector &v, int exclude_octant) const {
743  if (isLeaf()) {
744  throw std::runtime_error(
745  "findOtherIntersectedOctants called on leaf node");
746  }
747  // Perf note: this function was changed to use std::array, but there
748  // wasn't any observed performance change during benchmarking. Since
749  // the std::array logic was more complicated, the std::vector impl
750  // was kept.
751  std::vector<std::pair<EGS_Float, int>> intersections;
752  for (int i = 0; i < 8; i++) {
753  if (i == exclude_octant) {
754  continue;
755  }
756  EGS_Vector intersection;
757  EGS_Float dist;
758  if (children_[i].bbox_.ray_intersection(p, v, dist, intersection)) {
759  intersections.push_back({dist, i});
760  }
761  }
762  std::sort(intersections.begin(), intersections.end());
763  std::vector<int> octants;
764  for (const auto &i : intersections) {
765  octants.push_back(i.second);
766  }
767  return octants;
768  }
769 
770  // Leaf node: search all bounded elements, returning the minimum
771  // distance to a boundary tetrahedron or a bounding box surface.
772  EGS_Float hownear_leaf_search(const EGS_Vector &p, EGS_Mesh &mesh) const {
773  const EGS_Float best_dist = bbox_.min_interior_distance(p);
774  // Use squared distance to avoid computing square roots in the
775  // loop. This has the added bonus of ridding ourselves of any
776  // negatives from near-zero floating-point issues
777  EGS_Float best_dist2 = best_dist * best_dist;
778  for (const auto &e: elts_) {
779  const auto &n = mesh.element_nodes(e);
780  best_dist2 = std::min(best_dist2, distance2(p,
781  closest_point_tetrahedron(p, n.A, n.B, n.C, n.D)));
782  }
783  return std::sqrt(best_dist2);
784  }
785 
786  EGS_Float hownear_exterior(const EGS_Vector &p, EGS_Mesh &mesh) const {
787  // Leaf node: find a lower bound on the mesh exterior distance
788  // closest distance
789  if (isLeaf()) {
790  return hownear_leaf_search(p, mesh);
791  }
792  // Parent node: decide which octant to search and descend the tree
793  const auto octant = findOctant(p);
794  return children_[octant].hownear_exterior(p, mesh);
795  }
796 
797  // Does not mutate the EGS_Mesh.
798  int isWhere(const EGS_Vector &p, /*const*/ EGS_Mesh &mesh) const {
799  // Leaf node: search all bounded elements, returning -1 if the
800  // element wasn't found.
801  if (isLeaf()) {
802  for (const auto &e: elts_) {
803  if (mesh.insideElement(e, p)) {
804  return e;
805  }
806  }
807  return -1;
808  }
809 
810  // Parent node: decide which octant to search and descend the tree
811  return children_[findOctant(p)].isWhere(p, mesh);
812  }
813 
814  // TODO split into two functions
815  int howfar_exterior(const EGS_Vector &p, const EGS_Vector &v,
816  const EGS_Float &max_dist, EGS_Float &t, /* const */ EGS_Mesh &mesh)
817  const {
818  // Leaf node: check for intersection with any boundary elements
819  EGS_Float min_dist = std::numeric_limits<EGS_Float>::max();
820  int min_elt = -1;
821  if (isLeaf()) {
822  for (const auto &e: elts_) {
823  if (!mesh.is_boundary(e)) {
824  continue;
825  }
826  // closest_boundary_face only counts intersections where the
827  // point is on the outside of the face, when it's possible
828  // to intersect the boundary face directly
829  auto intersection = mesh.closest_boundary_face(e, p, v);
830  if (intersection.dist < min_dist) {
831  min_elt = e;
832  min_dist = intersection.dist;
833  }
834  }
835  t = min_dist;
836  return min_elt; // min_elt may be -1 if there is no intersection
837  }
838  // Parent node: decide which octant to search and descend the tree
839  EGS_Vector intersection;
840  EGS_Float dist;
841  auto hit = bbox_.ray_intersection(p, v, dist, intersection);
842  // case 1: there's no intersection with this bounding box, return
843  if (!hit) {
844  return -1;
845  }
846  // case 2: we have a hit. Descend into the most likely intersecting
847  // child octant's bounding box to find any intersecting elements
848  auto octant = findOctant(intersection);
849  auto elt = children_[octant].howfar_exterior(
850  p, v, max_dist, t, mesh
851  );
852  // If we find a valid element, return it
853  if (elt != -1) {
854  return elt;
855  }
856  // Otherwise, if there was no intersection in the most likely
857  // octant, examine the other octants that are intersected by
858  // the ray:
859  for (const auto &o : findOtherIntersectedOctants(p, v, octant)) {
860  auto elt = children_[o].howfar_exterior(
861  p, v, max_dist, t, mesh
862  );
863  // If we find a valid element, return it
864  if (elt != -1) {
865  return elt;
866  }
867  }
868  return -1;
869  }
870  };
871 
872  Node root_;
873 public:
874  EGS_Mesh_Octree() = default;
875  EGS_Mesh_Octree(const std::vector<int> &elts, std::size_t n_max,
876  const EGS_Mesh &mesh, egs_mesh::internal::PercentCounter &progress) {
877  if (elts.empty()) {
878  throw std::runtime_error("EGS_Mesh_Octree: empty elements vector");
879  }
880  if (elts.size() > std::numeric_limits<int>::max()) {
881  throw std::runtime_error("EGS_Mesh_Octree: num elts must fit into an int");
882  }
883 
884  const EGS_Float INF = std::numeric_limits<EGS_Float>::infinity();
885  BoundingBox g_bounds(INF, -INF, INF, -INF, INF, -INF);
886  for (const auto &e : elts) {
887  const auto &nodes = mesh.element_nodes(e);
888  g_bounds.min_x = std::min(g_bounds.min_x, tet_min_x(nodes));
889  g_bounds.max_x = std::max(g_bounds.max_x, tet_max_x(nodes));
890  g_bounds.min_y = std::min(g_bounds.min_y, tet_min_y(nodes));
891  g_bounds.max_y = std::max(g_bounds.max_y, tet_max_y(nodes));
892  g_bounds.min_z = std::min(g_bounds.min_z, tet_min_z(nodes));
893  g_bounds.max_z = std::max(g_bounds.max_z, tet_max_z(nodes));
894  }
895  // Add a small delta around the bounding box to avoid numerical problems
896  // at the boundary
897  g_bounds.expand(1e-8);
898 
899  // Track progress using how much volume has been covered
900  progress.start(g_bounds.volume());
901  root_ = Node(elts, g_bounds, n_max, mesh, progress);
902  }
903 
904  int isWhere(const EGS_Vector &p, /*const*/ EGS_Mesh &mesh) const {
905  if (!root_.bbox_.contains(p)) {
906  return -1;
907  }
908  return root_.isWhere(p, mesh);
909  }
910 
911  void print(std::ostream &out) const {
912  root_.print(out, 0);
913  }
914 
915  int howfar_exterior(const EGS_Vector &p, const EGS_Vector &v,
916  const EGS_Float &max_dist, EGS_Float &t, EGS_Mesh &mesh) const {
917  EGS_Vector intersection;
918  EGS_Float dist;
919  auto hit = root_.bbox_.ray_intersection(p, v, dist, intersection);
920  if (!hit || dist > max_dist) {
921  return -1;
922  }
923  return root_.howfar_exterior(p, v, max_dist, t, mesh);
924  }
925 
926  // Returns a lower bound on the distance to the mesh exterior boundary.
927  // The actual distance to the mesh may be larger, i.e. a distance to an
928  // axis-aligned bounding box might be returned instead. This is allowed by
929  // the HOWNEAR spec, PIRS-701 section 3.6, "Specifications for HOWNEAR":
930  //
931  // > In complex geometries, the mathematics of HOWNEAR can become difficult
932  // and sometimes almost impossible! If it is easier for the user to
933  // compute some lower bound to the nearest distance, this could be used...
934  EGS_Float hownear_exterior(const EGS_Vector &p, EGS_Mesh &mesh) const {
935  // If the point is outside the octree bounding box, return the distance
936  // to the bounding box.
937  if (!root_.bbox_.contains(p)) {
938  return distance(root_.bbox_.closest_point(p), p);
939  }
940  // Otherwise, descend the octree
941  return root_.hownear_exterior(p, mesh);
942  }
943 };
945 
947  EGS_BaseGeometry(EGS_BaseGeometry::getUniqueName()) {
948  spec.checkValid();
949  initializeElements(std::move(spec.elements), std::move(spec.nodes),
950  std::move(spec.media));
951  initializeNeighbours();
952  initializeOctrees();
953  initializeNormals();
954 }
955 
956 void EGS_Mesh::initializeElements(
957  std::vector<EGS_MeshSpec::Tetrahedron> elements,
958  std::vector<EGS_MeshSpec::Node> nodes,
959  std::vector<EGS_MeshSpec::Medium> materials) {
960  EGS_BaseGeometry::nreg = elements.size();
961 
962  elt_tags_.reserve(elements.size());
963  elt_node_indices_.reserve(elements.size());
964  nodes_.reserve(nodes.size());
965 
966  std::unordered_map<int, int> node_map;
967  node_map.reserve(nodes.size());
968  for (int i = 0; i < static_cast<int>(nodes.size()); i++) {
969  const auto &n = nodes[i];
970  node_map.insert({n.tag, i});
971  nodes_.push_back(EGS_Vector(n.x, n.y, n.z));
972  }
973  if (node_map.size() != nodes.size()) {
974  throw std::runtime_error("duplicate nodes in node list");
975  }
976  // Find the matching node indices for every tetrahedron
977  auto find_node = [&](int node_tag) -> int {
978  auto node_it = node_map.find(node_tag);
979  if (node_it == node_map.end()) {
980  throw std::runtime_error("No mesh node with tag: " + std::to_string(node_tag));
981  }
982  return node_it->second;
983  };
984  for (int i = 0; i < static_cast<int>(elements.size()); i++) {
985  const auto &e = elements[i];
986  elt_tags_.push_back(e.tag);
987  elt_node_indices_.push_back({
988  find_node(e.a), find_node(e.b), find_node(e.c), find_node(e.d)
989  });
990  }
991 
992  initializeMedia(std::move(elements), std::move(materials));
993 }
994 
995 void EGS_Mesh::initializeMedia(std::vector<EGS_MeshSpec::Tetrahedron> elements,
996  std::vector<EGS_MeshSpec::Medium> materials) {
997  std::unordered_map<int, int> medium_offsets;
998  for (const auto &m : materials) {
999  // If the medium was already registered, returns its offset. For new
1000  // media, addMedium adds them to the list and returns the new offset.
1001  const int media_offset = EGS_BaseGeometry::addMedium(m.medium_name);
1002  bool inserted = medium_offsets.insert({m.tag, media_offset}).second;
1003  if (!inserted) {
1004  throw std::runtime_error("duplicate medium tag: "
1005  + std::to_string(m.tag));
1006  }
1007  }
1008 
1009  medium_indices_.reserve(elements.size());
1010  for (const auto &e: elements) {
1011  medium_indices_.push_back(medium_offsets.at(e.medium_tag));
1012  }
1013 }
1014 
1015 void EGS_Mesh::initializeNeighbours() {
1016  std::vector<mesh_neighbours::Tetrahedron> neighbour_elts;
1017  neighbour_elts.reserve(num_elements());
1018  for (const auto &e: elt_node_indices_) {
1019  neighbour_elts.emplace_back(mesh_neighbours::Tetrahedron(e[0], e[1], e[2], e[3]));
1020  }
1021 
1022  egs_mesh::internal::PercentCounter progress(get_logger(),
1023  "EGS_Mesh: finding element neighbours");
1024 
1025  neighbours_ = mesh_neighbours::tetrahedron_neighbours(
1026  neighbour_elts, progress);
1027 
1028  progress.finish("EGS_Mesh: found element neighbours");
1029 
1030  boundary_faces_.reserve(num_elements() * 4);
1031  for (const auto &ns: neighbours_) {
1032  for (const auto &n: ns) {
1033  boundary_faces_.push_back(n == mesh_neighbours::NONE);
1034  }
1035  }
1036 }
1037 
1038 void EGS_Mesh::initializeNormals() {
1039  face_normals_.reserve(num_elements());
1040  for (int i = 0; i < static_cast<int>(num_elements()); i++) {
1041  auto get_normal = [](const EGS_Vector& a, const EGS_Vector& b,
1042  const EGS_Vector& c, const EGS_Vector& d) -> EGS_Vector {
1043  EGS_Vector normal = cross(b - a, c - a);
1044  normal.normalize();
1045  if (dot(normal, d-a) < 0) {
1046  normal *= -1.0;
1047  }
1048  return normal;
1049  };
1050  const auto &n = element_nodes(i);
1051  face_normals_.push_back({
1052  get_normal(n.B, n.C, n.D, n.A),
1053  get_normal(n.A, n.C, n.D, n.B),
1054  get_normal(n.A, n.B, n.D, n.C),
1055  get_normal(n.A, n.B, n.C, n.D)
1056  });
1057  }
1058 }
1059 
1060 void EGS_Mesh::initializeOctrees() {
1061  std::vector<int> elts;
1062  std::vector<int> boundary_elts;
1063  elts.reserve(num_elements());
1064  for (int i = 0; i < num_elements(); i++) {
1065  elts.push_back(i);
1066  if (is_boundary(i)) {
1067  boundary_elts.push_back(i);
1068  }
1069  }
1070  // Max element sizes from Furuta et al section 2.1.1
1071  std::size_t n_vol = 200;
1072  egs_mesh::internal::PercentCounter vol_progress(get_logger(),
1073  "EGS_Mesh: building volume octree");
1074  volume_tree_ = std::unique_ptr<EGS_Mesh_Octree>(
1075  new EGS_Mesh_Octree(elts, n_vol, *this, vol_progress)
1076  );
1077  vol_progress.finish("EGS_Mesh: built volume octree");
1078 
1079  std::size_t n_surf = 100;
1080  egs_mesh::internal::PercentCounter surf_progress(get_logger(),
1081  "EGS_Mesh: building surface octree");
1082  surface_tree_ = std::unique_ptr<EGS_Mesh_Octree>(
1083  new EGS_Mesh_Octree(boundary_elts, n_surf, *this, surf_progress)
1084  );
1085  surf_progress.finish("EGS_Mesh: built surface octree");
1086 }
1087 
1088 bool EGS_Mesh::isInside(const EGS_Vector &x) {
1089  return isWhere(x) != -1;
1090 }
1091 
1092 int EGS_Mesh::inside(const EGS_Vector &x) {
1093  return isWhere(x);
1094 }
1095 
1096 int EGS_Mesh::medium(int ireg) const {
1097  return medium_indices_.at(ireg);
1098 }
1099 
1100 bool EGS_Mesh::insideElement(int i, const EGS_Vector &x) { /* const */
1101  const auto &n = element_nodes(i);
1102  if (point_outside_of_plane(x, n.A, n.B, n.C, n.D)) {
1103  return false;
1104  }
1105  if (point_outside_of_plane(x, n.A, n.C, n.D, n.B)) {
1106  return false;
1107  }
1108  if (point_outside_of_plane(x, n.A, n.B, n.D, n.C)) {
1109  return false;
1110  }
1111  if (point_outside_of_plane(x, n.B, n.C, n.D, n.A)) {
1112  return false;
1113  }
1114  return true;
1115 }
1116 
1117 int EGS_Mesh::isWhere(const EGS_Vector &x) {
1118  return volume_tree_->isWhere(x, *this);
1119 }
1120 
1121 EGS_Float EGS_Mesh::hownear(int ireg, const EGS_Vector &x) {
1122  // inside
1123  if (ireg >= 0) {
1124  return min_interior_face_dist(ireg, x);
1125  }
1126  // outside
1127  return min_exterior_face_dist(x);
1128 }
1129 
1130 // Assumes the input normal is normalized. Returns the absolute value of the
1131 // distance.
1132 EGS_Float distance_to_plane(const EGS_Vector &x,
1133  const EGS_Vector &unit_plane_normal, const EGS_Vector &plane_point) {
1134  return std::abs(dot(unit_plane_normal, x - plane_point));
1135 }
1136 
1137 EGS_Float EGS_Mesh::min_interior_face_dist(int ireg, const EGS_Vector &x) {
1138  const auto &n = element_nodes(ireg);
1139 
1140  // First face is BCD, second is ACD, third is ABD, fourth is ABC
1141  EGS_Float min_dist = distance_to_plane(x, face_normals_[ireg][0], n.B);
1142  min_dist = std::min(min_dist,
1143  distance_to_plane(x, face_normals_[ireg][1], n.A));
1144  min_dist = std::min(min_dist,
1145  distance_to_plane(x, face_normals_[ireg][2], n.A));
1146  min_dist = std::min(min_dist,
1147  distance_to_plane(x, face_normals_[ireg][3], n.A));
1148 
1149  return min_dist;
1150 }
1151 
1152 EGS_Float EGS_Mesh::min_exterior_face_dist(const EGS_Vector &x) {
1153  return surface_tree_->hownear_exterior(x, *this);
1154 }
1155 
1156 int EGS_Mesh::howfar(int ireg, const EGS_Vector &x, const EGS_Vector &u,
1157  EGS_Float &t, int *newmed /* =0 */, EGS_Vector *normal /* =0 */) {
1158  if (ireg < 0) {
1159  return howfar_exterior(x, u, t, newmed, normal);
1160  }
1161  // Find the minimum distance to an element boundary. If this distance is
1162  // smaller than the intended step, adjust the step length and return the
1163  // neighbouring element. If the step length is larger than the distance to a
1164  // boundary, don't adjust it upwards! This will break particle transport.
1165  EGS_Float distance_to_boundary = veryFar;
1166  auto new_reg = howfar_interior(
1167  ireg, x, u, distance_to_boundary, newmed, normal);
1168 
1169  if (distance_to_boundary < t) {
1170  t = distance_to_boundary;
1171  return new_reg;
1172  }
1173  // Otherwise, return the current region and don't change the value of t.
1174  return ireg;
1175 }
1176 
1177 // howfar_interior is the most complicated EGS_BaseGeometry method. Apart from
1178 // the intersection logic, there are exceptional cases that must be carefully
1179 // handled. In particular, the region number and the position `x` may not agree.
1180 // For example, the region may be 1, but the position x may be slightly outside
1181 // of region 1 because of numerical undershoot. The region number takes priority
1182 // because it is where EGS thinks the particle should be based on the simulation
1183 // so far, and we assume steps like boundary crossing calculations have already
1184 // taken place. So we have to do our best to calculate intersections as if the
1185 // position really is inside the given tetrahedron.
1186 int EGS_Mesh::howfar_interior(int ireg, const EGS_Vector &x, const EGS_Vector &u,
1187  EGS_Float &t, int *newmed, EGS_Vector *normal) {
1188  // General idea is to use ray-plane intersection because it's watertight and
1189  // uses less flops than ray-triangle intersection. To my understanding, this
1190  // approach is also used by Geant, PHITS and MCNP.
1191  //
1192  // Because planes are infinite, intersections can be found far away from
1193  // the actual tetrahedron bounds if the particle is travelling away from the
1194  // element. So we limit valid intersections with planes by element face
1195  // distances and normal angles.
1196  //
1197  // OK, if d < eps, and theta > -angle_eps
1198  //
1199  // |<-d->| | /
1200  // | | i /
1201  // | n * | /
1202  // * -> | --> | | /
1203  // | v |/
1204  // | /
1205  // | /
1206  // x
1207  //
1208  // There is a chance that floating-point rounding may cause computed
1209  // quantities to be inconsistent. The most important thing is the simulation
1210  // should try to continue by always at least taking a small step. Returning
1211  // 0.0 opens the door to hanging the transport routine entirely and for
1212  // enough histories, it will almost certainly hang. Even returning a small
1213  // step does not ensure forward progress for all possible input meshes,
1214  // since if the step is too small, it may be wiped out by rounding error
1215  // after being added to a large number. But if the step is too large, small
1216  // tetrahedrons may be skipped entirely.
1217 
1218  // TODO add test for transport after intersection point lands right on a
1219  // corner node.
1220 
1221  const auto &n = element_nodes(ireg);
1222  // Pick an arbitrary face point to do plane math with. Face 0 is BCD, Face 1
1223  // is ACD, etc...
1224  std::array<EGS_Vector, 4> face_points {n.B, n.A, n.A, n.A};
1225  std::array<PointLocation, 4> intersect_tests {};
1226  for (int i = 0; i < 4; ++i) {
1227  intersect_tests[i] = find_point_location(
1228  x, u, face_points[i], face_normals_[ireg][i]);
1229  }
1230  // If the particle is not strictly inside the element, try transporting
1231  // along a thick plane.
1232  if (intersect_tests[0].signed_distance < 0.0 ||
1233  intersect_tests[1].signed_distance < 0.0 ||
1234  intersect_tests[2].signed_distance < 0.0 ||
1235  intersect_tests[3].signed_distance < 0.0) {
1236  return howfar_interior_thick_plane(intersect_tests, ireg, x, u, t,
1237  newmed, normal);
1238  }
1239 
1240  // Otherwise, if points are inside the element, calculate the minimum
1241  // distance to a plane.
1242  auto ix = find_interior_intersection(intersect_tests);
1243  if (ix.dist < 0.0 || ix.face_index == -1) {
1244  egsWarning("\nEGS_Mesh warning: bad interior intersection t = %.17g, face_index = %d in region %d: "
1245  "x=(%.17g,%.17g,%.17g) u=(%.17g,%.17g,%.17g)\n", ix.dist, ix.face_index,
1246  ireg, x.x, x.y, x.z, u.x, u.y, u.z);
1248  return ireg;
1249  }
1250  // Very small distances might get swallowed up by rounding error, so enforce
1251  // a minimum step size.
1252  if (ix.dist < EGS_Mesh::min_step_size) {
1253  ix.dist = EGS_Mesh::min_step_size;
1254  }
1255  t = ix.dist;
1256  int new_reg = neighbours_[ireg].at(ix.face_index);
1257  update_medium(new_reg, newmed);
1258  update_normal(face_normals_[ireg].at(ix.face_index), u, normal);
1259  return new_reg;
1260 }
1261 
1262 // Returns the position of the point relative to the face.
1263 EGS_Mesh::PointLocation EGS_Mesh::find_point_location(const EGS_Vector &x,
1264  const EGS_Vector &u, const EGS_Vector &plane_point,
1265  const EGS_Vector &plane_normal) {
1266  // TODO handle degenerate triangles, by not finding any intersections.
1267 
1268  // Face normals point inwards, to the tetrahedron centroid. So if
1269  // dot(u, n) < 0, the particle is travelling towards the plane and will
1270  // intersect it at some point. The intersection point might be outside the
1271  // face's triangular bounds though.
1272  EGS_Float direction_dot_normal = dot(u, plane_normal);
1273 
1274  // Find the signed distance to the plane, to see if it lies in the thick
1275  // plane and so might be a candidate for transport even if it's not strictly
1276  // inside the tetrahedron. This is the distance along the plane normal, not
1277  // the distance along the velocity vector.
1278  //
1279  // (-) (+)
1280  // |
1281  // * |-> n
1282  // |
1283  // |-----+
1284  // d
1285  //
1286  EGS_Float signed_distance = dot(plane_normal, x - plane_point);
1287  return PointLocation(direction_dot_normal, signed_distance);
1288 }
1289 
1290 // Assuming the point is inside the element, find the plane intersection point.
1291 //
1292 // Caller is responsible for checking that each intersections signed_distance is
1293 // >= 0.
1294 EGS_Mesh::Intersection EGS_Mesh::find_interior_intersection(
1295  const std::array<PointLocation, 4> &ixs) {
1296  EGS_Float t_min = veryFar;
1297  int min_face_index = -1;
1298  for (int i = 0; i < 4; ++i) {
1299  // If the particle is travelling away from the face, it will not
1300  // intersect it.
1301  if (ixs[i].direction_dot_normal >= 0.0) {
1302  continue;
1303  }
1304  EGS_Float t_i = -ixs[i].signed_distance / ixs[i].direction_dot_normal;
1305  if (t_i < t_min) {
1306  t_min = t_i;
1307  min_face_index = i;
1308  }
1309  }
1310  // Is there a risk of returning min_face_index = -1? Could t_min also be negative?
1311  return Intersection(t_min, min_face_index);
1312 }
1313 
1314 // Try and transport the particle using thick plane intersections.
1315 int EGS_Mesh::howfar_interior_thick_plane(const std::array<PointLocation, 4> &
1316  intersect_tests, int ireg, const EGS_Vector &x, const EGS_Vector &u,
1317  EGS_Float &t, int *newmed, EGS_Vector *normal) {
1318  // The particle isn't inside the element, but it might be a candidate for
1319  // transport along a thick plane, as long as it is parallel or travelling
1320  // towards the element.
1321  //
1322  // OK not OK not OK
1323  //
1324  // * ^
1325  // - * -> - | - |
1326  // d | | d | v d | *
1327  // -----v--- ------ ------
1328  //
1329 
1330  // Find the largest negative distance to a face plane. If it is bigger
1331  // than the thick plane tolerance, the particle isn't close enough to be
1332  // considered part of the element.
1333  EGS_Float max_neg_dist = veryFar;
1334  int face_index = -1;
1335  for (int i = 0; i < 4; ++i) {
1336  if (intersect_tests[i].signed_distance < max_neg_dist) {
1337  max_neg_dist = intersect_tests[i].signed_distance;
1338  face_index = i;
1339  }
1340  }
1341  if (face_index == -1) {
1342  egsWarning("\nEGS_Mesh warning: howfar_interior_thick_plane face_index %d in region %d: "
1343  "x=(%.17g,%.17g,%.17g) u=(%.17g,%.17g,%.17g)\n",
1344  face_index, ireg, x.x, x.y, x.z, u.x, u.y, u.z);
1346  return ireg;
1347  }
1348 
1349  // If the perpendicular distance to the plane is too big to be a thick
1350  // plane, or if the particle is travelling away from the plane, push the
1351  // particle by a small step and return the region the particle is in.
1352 
1353  // Some small epsilon, TODO maybe look into gamma bounds for this?
1354  // Or EGS_BaseGeometry::BoundaryTolerance?
1355  constexpr EGS_Float thick_plane_bounds = EGS_Mesh::min_step_size;
1356  if (max_neg_dist < -thick_plane_bounds ||
1357  intersect_tests.at(face_index).direction_dot_normal < -thick_plane_bounds) {
1358  return howfar_interior_recover_lost_particle(ireg, x, u, t, newmed);
1359  }
1360 
1361  // If the particle is inside the thick plane and travelling towards a face
1362  // plane, find the intersection point. This might be outside the strict
1363  // bounds of the tetrahedron, but necessary for "wall-riding" particles to
1364  // be transported without tanking simulation efficiency.
1365  //
1366  // \ /
1367  // out \ * -> x
1368  // \--------/
1369  // in \ /
1370  //
1371  EGS_Float t_min = veryFar;
1372  int min_face_index = -1;
1373  for (int i = 0; i < 4; ++i) {
1374  // If the particle is travelling away from the face, it will not
1375  // intersect it.
1376  if (intersect_tests[i].direction_dot_normal >= 0.0) {
1377  continue;
1378  }
1379 
1380  EGS_Float t_i = -intersect_tests[i].signed_distance
1381  / intersect_tests[i].direction_dot_normal;
1382 
1383  if (t_i < t_min) {
1384  t_min = t_i;
1385  min_face_index = i;
1386  }
1387  }
1388 
1389  // Rarely, rounding error near an edge or corner can cause t_min to be
1390  // negative or a very small positive number. For this case, try to recover
1391  // as if the particle is lost.
1392  if (t_min < EGS_Mesh::min_step_size) {
1393  return howfar_interior_recover_lost_particle(ireg, x, u, t, newmed);
1394  }
1395  if (min_face_index == -1) {
1396  egsWarning("\nEGS_Mesh warning: face_index %d in region %d: "
1397  "x=(%.17g,%.17g,%.17g) u=(%.17g,%.17g,%.17g)\n",
1398  min_face_index, ireg, x.x, x.y, x.z, u.x, u.y, u.z);
1400  return ireg;
1401  }
1402  t = t_min;
1403  int new_reg = neighbours_[ireg].at(min_face_index);
1404  update_medium(new_reg, newmed);
1405  update_normal(face_normals_[ireg].at(min_face_index), u, normal);
1406  return new_reg;
1407 }
1408 
1409 /* No valid intersections were found for this particle. Push it along the
1410  momentum vector by a small step and try to recover.
1411 
1412  /\
1413  <- * / \
1414  /____\
1415 
1416  ^^^^ i.e., which region is this particle actually in?
1417 
1418  There are many cases where this could happen, so this routine can't make
1419  too many assumptions. Some possibiltites:
1420 
1421  * The particle is right on an edge and a negative distance was calculated
1422  * The particle is inside the thick plane but travelling away from ireg
1423  * The particle is outside the thick plane but travelling away or towards ireg
1424 */
1425 int EGS_Mesh::howfar_interior_recover_lost_particle(int ireg,
1426  const EGS_Vector &x, const EGS_Vector &u, EGS_Float &t, int *newmed) {
1427  t = EGS_Mesh::min_step_size;
1428  // This may hang if min_step_size is too small for the mesh.
1429  EGS_Vector new_pos = x + u * t;
1430  // Fast path: particle is in a neighbouring element.
1431  for (int i = 0; i < 4; ++i) {
1432  const auto neighbour = neighbours_[ireg][i];
1433  if (neighbour == -1) {
1434  continue;
1435  }
1436  if (insideElement(neighbour, new_pos)) {
1437  update_medium(neighbour, newmed);
1438  return neighbour;
1439  }
1440  }
1441  // Slow path: particle isn't in a neighbouring element, initiate a full
1442  // octree search. This can also happen if the particle is leaving the
1443  // mesh.
1444  auto actual_elt = isWhere(new_pos);
1445  update_medium(actual_elt, newmed);
1446  // We can't determine which normal to display (which is only for
1447  // egs_view in any case), so we don't update the normal for this
1448  // exceptional case.
1449  return actual_elt;
1450 }
1451 
1452 // exclude from doxygen
1454 EGS_Mesh::Intersection EGS_Mesh::closest_boundary_face(int ireg, const EGS_Vector &x,
1455  const EGS_Vector &u) {
1456  assert(is_boundary(ireg));
1457  EGS_Float min_dist = std::numeric_limits<EGS_Float>::max();
1458 
1459  auto dist = min_dist;
1460  auto closest_face = -1;
1461 
1462  auto check_face_intersection = [&](int face, const EGS_Vector& A, const EGS_Vector& B,
1463  const EGS_Vector& C, const EGS_Vector& D) {
1464  if (boundary_faces_[4*ireg + face] &&
1465  // check if the point is on the outside looking in (rather than just
1466  // clipping the edge of a boundary face)
1467  point_outside_of_plane(x, A, B, C, D) &&
1468  dot(face_normals_[ireg][face], u) > 0.0 && // point might be in a thick plane
1469  exterior_triangle_ray_intersection(x, u, A, B, C, dist) &&
1470  dist < min_dist) {
1471  min_dist = dist;
1472  closest_face = face;
1473  }
1474  };
1475 
1476  const auto &n = element_nodes(ireg);
1477  // face 0 (BCD), face 1 (ACD) etc.
1478  check_face_intersection(0, n.B, n.C, n.D, n.A);
1479  check_face_intersection(1, n.A, n.C, n.D, n.B);
1480  check_face_intersection(2, n.A, n.B, n.D, n.C);
1481  check_face_intersection(3, n.A, n.B, n.C, n.D);
1482 
1483  return EGS_Mesh::Intersection(min_dist, closest_face);
1484 }
1486 
1487 int EGS_Mesh::howfar_exterior(const EGS_Vector &x, const EGS_Vector &u,
1488  EGS_Float &t, int *newmed, EGS_Vector *normal) {
1489  EGS_Float min_dist = 1e30;
1490  auto min_reg = surface_tree_->howfar_exterior(x, u, t, min_dist, *this);
1491 
1492  // no intersection
1493  if (min_dist > t || min_reg == -1) {
1494  return -1;
1495  }
1496 
1497  // intersection found, update out parameters
1498  t = min_dist;
1499  if (newmed) {
1500  *newmed = medium(min_reg);
1501  }
1502  if (normal) {
1503  auto intersection = closest_boundary_face(min_reg, x, u);
1504  EGS_Vector tmp_normal = face_normals_[min_reg]
1505  .at(intersection.face_index);
1506  // egs++ convention is normal pointing opposite view ray
1507  if (dot(tmp_normal, u) > 0) {
1508  tmp_normal = -1.0 * tmp_normal;
1509  }
1510  *normal = tmp_normal;
1511  }
1512  return min_reg;
1513 }
1514 
1515 const std::string EGS_Mesh::type = "EGS_Mesh";
1516 
1517 void EGS_Mesh::printInfo() const {
1519  std::ostringstream oss;
1520  printElement(0, oss);
1521  egsInformation(oss.str().c_str());
1522 }
1523 
1524 // Parse a mesh file from the input file to an EGS_MeshSpec.
1525 //
1526 // Supported file types are:
1527 // * Gmsh msh4.1 (.msh)
1528 // * TetGen elt and node file pairs (.ele, .node)
1529 //
1530 // Throws a std::runtime_error if parsing fails.
1531 static EGS_MeshSpec parse_mesh_file(const std::string &mesh_file) {
1532  // Needs to be at least four characters long (.ele, .msh)
1533  auto ends_with = [](const std::string& str, const std::string& suffix)
1534  -> bool {
1535  if (suffix.size() > str.size()) {
1536  return false;
1537  }
1538  return str.compare(str.size() - suffix.size(), str.size(), suffix) == 0;
1539  };
1540 
1541  if (ends_with(mesh_file, ".msh")) {
1542  std::ifstream file_stream(mesh_file);
1543  if (!file_stream) {
1544  throw std::runtime_error(std::string("mesh file: `") + mesh_file
1545  + "` does not exist or is not readable");
1546  }
1547  try {
1548  return msh_parser::parse_msh_file(file_stream, egsInformation);
1549  }
1550  catch (const std::runtime_error &e) {
1551  throw std::runtime_error(std::string("Gmsh msh file parsing failed")
1552  + "\nerror: " + e.what() + "\n");
1553  }
1554  }
1555 
1556  if (ends_with(mesh_file, ".ele")) {
1557  return tetgen_parser::parse_tetgen_files(mesh_file,
1558  tetgen_parser::TetGenFile::Ele, egsInformation);
1559  }
1560 
1561  if (ends_with(mesh_file, ".node")) {
1562  return tetgen_parser::parse_tetgen_files(mesh_file,
1563  tetgen_parser::TetGenFile::Node, egsInformation);
1564  }
1565 
1566  throw std::runtime_error(std::string("unknown extension for mesh file `")
1567  + mesh_file + "`, supported extensions are msh, ele, node");
1568 }
1569 
1570 extern "C" {
1571  static void setInputs() {
1572  inputSet = true;
1573 
1574  setBaseGeometryInputs(false);
1575 
1576  geomBlockInput->getSingleInput("library")->setValues({"egs_mesh"});
1577 
1578  // Format: name, isRequired, description, vector string of allowed values
1579  geomBlockInput->addSingleInput("file", true, "The full filepath to the .msh, .node or .ele file, including the extension.");
1580  geomBlockInput->addSingleInput("scale", false, "Apply a multiplicative scaling factor to positions. E.g. if your model is in mm, scale to cm using scale=0.1.");
1581  }
1582 
1583  EGS_MESH_EXPORT string getExample() {
1584  string example;
1585  example = {
1586  R"(
1587  # Use Gmsh to convert CAD formats to .msh
1588  # Define materials by naming "physical volumes" with the medium name
1589  #:start geometry:
1590  name = my_mesh
1591  library = egs_mesh
1592  # Using the msh4.1 format:
1593  file = model.msh
1594  # or, using the TetGen format:
1595  # file = model.node # or model.ele
1596  scale = 0.1 # scale from mm to cm
1597  :stop geometry:
1598 )"};
1599  return example;
1600  }
1601 
1602  EGS_MESH_EXPORT shared_ptr<EGS_BlockInput> getInputs() {
1603  if(!inputSet) {
1604  setInputs();
1605  }
1606  return geomBlockInput;
1607  }
1608 
1609  EGS_MESH_EXPORT EGS_BaseGeometry *createGeometry(EGS_Input *input) {
1610  if (!input) {
1611  egsWarning("createGeometry(EGS_Mesh): null input\n");
1612  return nullptr;
1613  }
1614  std::string mesh_file;
1615  int err = input->getInput("file", mesh_file);
1616  if (err) {
1617  egsWarning("createGeometry(EGS_Mesh): no mesh file key `file` in input\n");
1618  return nullptr;
1619  }
1620 
1621  EGS_MeshSpec mesh_spec;
1622  try {
1623  mesh_spec = parse_mesh_file(mesh_file);
1624  }
1625  catch (const std::runtime_error &e) {
1626  std::string error_msg = std::string("createGeometry(EGS_Mesh): ") +
1627  e.what() + "\n";
1628  egsWarning("\n%s", error_msg.c_str());
1629  return nullptr;
1630  }
1631 
1632  EGS_Float scale = 0.0;
1633  err = input->getInput("scale", scale);
1634  if (!err) {
1635  if (scale > 0.0) {
1636  mesh_spec.scale(scale);
1637  }
1638  else {
1639  egsFatal("createGeometry(EGS_Mesh): invalid scale value (%g), "
1640  "expected a positive number\n", scale);
1641  }
1642  }
1643 
1644  EGS_Mesh *mesh = nullptr;
1645  try {
1646  mesh = new EGS_Mesh(std::move(mesh_spec));
1647  }
1648  catch (const std::runtime_error &e) {
1649  std::string error_msg = std::string("createGeometry(EGS_Mesh): ") +
1650  "bad input to EGS_Mesh\nerror: " + e.what() + "\n";
1651  egsWarning("\n%s", error_msg.c_str());
1652  return nullptr;
1653  }
1654 
1655  mesh->setBoundaryTolerance(input);
1656  mesh->setName(input);
1657  mesh->setLabels(input);
1658  return mesh;
1659  }
1660 }
Base geometry class. Every geometry class must be derived from EGS_BaseGeometry.
int nreg
Number of local regions in this geometry.
void setName(EGS_Input *inp)
Set the name of the geometry from the input inp.
static int error_flag
Set to non-zero status if a geometry problem is encountered.
int setLabels(EGS_Input *input)
Set the labels from an input block.
virtual void printInfo() const
Print information about this geometry.
static int addMedium(const string &medname)
Add a medium or get the index of an existing medium.
void setBoundaryTolerance(EGS_Input *inp)
Set the value of the boundary tolerance from the input inp.
A class for storing information in a tree-like structure of key-value pairs. This class is used throu...
Definition: egs_input.h:182
int getInput(const string &key, vector< string > &values) const
Assign values to an array of strings from an input identified by key.
Definition: egs_input.cpp:341
A container for raw unstructured tetrahedral mesh data.
Definition: egs_mesh.h:95
void checkValid() const
Definition: egs_mesh.cpp:83
std::vector< EGS_MeshSpec::Tetrahedron > elements
Unique mesh elements.
Definition: egs_mesh.h:156
void scale(EGS_Float factor)
Multiply all node coordinates by a constant factor.
Definition: egs_mesh.h:145
std::vector< EGS_MeshSpec::Node > nodes
Unique nodes.
Definition: egs_mesh.h:158
std::vector< EGS_MeshSpec::Medium > media
Unique medium information.
Definition: egs_mesh.h:160
A tetrahedral mesh geometry.
Definition: egs_mesh.h:243
EGS_Mesh(EGS_MeshSpec spec)
Definition: egs_mesh.cpp:946
bool insideElement(int i, const EGS_Vector &x)
Check if a point x is inside element i.
Definition: egs_mesh.cpp:1100
void printElement(int i, std::ostream &elt_info=std::cout) const
Print information about element i to the stream elt_info.
Definition: egs_mesh.h:302
int num_elements() const
Returns the number of mesh elements.
Definition: egs_mesh.h:261
Nodes element_nodes(int element) const
Given an element offset, return the element's node coordinates.
Definition: egs_mesh.h:365
bool is_boundary(int reg) const
Definition: egs_mesh.h:296
A class representing 3D vectors.
Definition: egs_vector.h:57
EGS_Float y
y-component
Definition: egs_vector.h:62
EGS_Float z
z-component
Definition: egs_vector.h:63
EGS_Float x
x-component
Definition: egs_vector.h:61
void(* EGS_InfoFunction)(const char *,...)
Defines a function printf-like prototype for functions to be used to report info, warnings,...
EGS_GLIB_EXPORT EGS_BaseGeometry * createGeometry(EGS_Input *input)
Definition: egs_glib.cpp:84
EGS_Input class header file.
Tetrahedral mesh geometry: header.
EGS_Vector methods for the manipulation of 3D vectors in cartesian co-ordinates.
EGS_InfoFunction EGS_EXPORT egsInformation
Always use this function for reporting the progress of a simulation and any other type of information...
EGS_InfoFunction EGS_EXPORT egsFatal
Always use this function for reporting fatal errors.
EGS_InfoFunction EGS_EXPORT egsWarning
Always use this function for reporting warnings.
const EGS_Float veryFar
A very large float.