LCOV - code coverage report
Current view: top level - utils - geodata_impl.cpp (source / functions) Coverage Total Hit
Test: Palace Coverage Report Lines: 50.5 % 317 160
Test Date: 2025-10-23 22:45:05 Functions: 59.4 % 32 19
Legend: Lines: hit not hit

            Line data    Source code
       1              : // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
       2              : // SPDX-License-Identifier: Apache-2.0
       3              : 
       4              : #include "geodata.hpp"
       5              : 
       6              : #include "geodata_impl.hpp"
       7              : 
       8              : #include <array>
       9              : #include <limits>
      10              : #include <random>
      11              : #include <unordered_set>
      12              : #include <Eigen/Dense>
      13              : #include "utils/communication.hpp"
      14              : #include "utils/omp.hpp"
      15              : 
      16              : namespace palace::mesh
      17              : {
      18              : 
      19              : using Vector3dMap = Eigen::Map<Eigen::Vector3d>;
      20              : using CVector3dMap = Eigen::Map<const Eigen::Vector3d>;
      21              : 
      22              : // Compute a lexicographic comparison of Eigen Vector3d.
      23         7176 : bool EigenLE(const Eigen::Vector3d &x, const Eigen::Vector3d &y)
      24              : {
      25         7176 :   return std::lexicographical_compare(x.begin(), x.end(), y.begin(), y.end());
      26              : }
      27              : 
      28              : // Helper for collecting a point cloud from a mesh, used in calculating bounding boxes and
      29              : // bounding balls. Returns the dominant rank, for which the vertices argument will be
      30              : // filled, while all other ranks will have an empty vector. Vertices are de-duplicated to a
      31              : // certain floating point precision.
      32           60 : int CollectPointCloudOnRoot(const mfem::ParMesh &mesh, const mfem::Array<int> &marker,
      33              :                             bool bdr, std::vector<Eigen::Vector3d> &vertices)
      34              : {
      35           60 :   if (!mesh.GetNodes())
      36              :   {
      37              :     // Linear mesh, work with element vertices directly.
      38            0 :     PalacePragmaOmp(parallel)
      39              :     {
      40              :       std::unordered_set<int> vertex_indices;
      41              :       if (bdr)
      42              :       {
      43              :         PalacePragmaOmp(for schedule(static))
      44              :         for (int i = 0; i < mesh.GetNBE(); i++)
      45              :         {
      46              :           if (!marker[mesh.GetBdrAttribute(i) - 1])
      47              :           {
      48              :             continue;
      49              :           }
      50              :           const int *verts = mesh.GetBdrElement(i)->GetVertices();
      51              :           vertex_indices.insert(verts, verts + mesh.GetBdrElement(i)->GetNVertices());
      52              :         }
      53              :       }
      54              :       else
      55              :       {
      56              :         PalacePragmaOmp(for schedule(static))
      57              :         for (int i = 0; i < mesh.GetNE(); i++)
      58              :         {
      59              :           if (!marker[mesh.GetAttribute(i) - 1])
      60              :           {
      61              :             continue;
      62              :           }
      63              :           const int *verts = mesh.GetElement(i)->GetVertices();
      64              :           vertex_indices.insert(verts, verts + mesh.GetElement(i)->GetNVertices());
      65              :         }
      66              :       }
      67              :       PalacePragmaOmp(critical(PointCloud))
      68              :       {
      69              :         for (auto i : vertex_indices)
      70              :         {
      71              :           const auto &vx = mesh.GetVertex(i);
      72              :           vertices.emplace_back(vx[0], vx[1], vx[2]);
      73              :         }
      74              :       }
      75              :     }
      76              :   }
      77              :   else
      78              :   {
      79              :     // Curved mesh, need to process point matrices.
      80           60 :     const int ref = mesh.GetNodes()->FESpace()->GetMaxElementOrder();
      81          432 :     auto AddPoints = [&](mfem::GeometryRefiner &refiner, mfem::ElementTransformation &T,
      82              :                          mfem::DenseMatrix &pointmat,
      83              :                          std::vector<Eigen::Vector3d> &loc_vertices)
      84              :     {
      85          432 :       mfem::RefinedGeometry *RefG = refiner.Refine(T.GetGeometryType(), ref);
      86          432 :       T.Transform(RefG->RefPts, pointmat);
      87         1728 :       for (int j = 0; j < pointmat.Width(); j++)
      88              :       {
      89         1296 :         loc_vertices.emplace_back(pointmat(0, j), pointmat(1, j), pointmat(2, j));
      90              :       }
      91          492 :     };
      92           60 :     PalacePragmaOmp(parallel)
      93              :     {
      94              :       mfem::GeometryRefiner refiner;
      95              :       mfem::IsoparametricTransformation T;
      96              :       mfem::DenseMatrix pointmat;  // 3 x N
      97              :       std::vector<Eigen::Vector3d> loc_vertices;
      98              :       if (bdr)
      99              :       {
     100              :         PalacePragmaOmp(for schedule(static))
     101              :         for (int i = 0; i < mesh.GetNBE(); i++)
     102              :         {
     103              :           if (!marker[mesh.GetBdrAttribute(i) - 1])
     104              :           {
     105              :             continue;
     106              :           }
     107              :           mesh.GetBdrElementTransformation(i, &T);
     108              :           AddPoints(refiner, T, pointmat, loc_vertices);
     109              :         }
     110              :       }
     111              :       else
     112              :       {
     113              :         PalacePragmaOmp(for schedule(static))
     114              :         for (int i = 0; i < mesh.GetNE(); i++)
     115              :         {
     116              :           if (!marker[mesh.GetAttribute(i) - 1])
     117              :           {
     118              :             continue;
     119              :           }
     120              :           mesh.GetElementTransformation(i, &T);
     121              :           AddPoints(refiner, T, pointmat, loc_vertices);
     122              :         }
     123              :       }
     124              :       PalacePragmaOmp(critical(PointCloud))
     125              :       {
     126              :         for (const auto &v : loc_vertices)
     127              :         {
     128              :           vertices.push_back(v);
     129              :         }
     130              :       }
     131              :     }
     132              :   }
     133              : 
     134              :   // dominant_rank will perform the calculation.
     135           60 :   MPI_Comm comm = mesh.GetComm();
     136           60 :   const auto num_vertices = int(vertices.size());
     137          180 :   const int dominant_rank = [&]()
     138              :   {
     139           60 :     int vert = num_vertices, rank = Mpi::Rank(comm);
     140           60 :     Mpi::GlobalMaxLoc(1, &vert, &rank, comm);
     141           60 :     return rank;
     142           60 :   }();
     143           60 :   std::vector<int> recv_counts(Mpi::Size(comm)), displacements;
     144              :   std::vector<Eigen::Vector3d> collected_vertices;
     145           60 :   MPI_Gather(&num_vertices, 1, MPI_INT, recv_counts.data(), 1, MPI_INT, dominant_rank,
     146              :              comm);
     147           60 :   if (dominant_rank == Mpi::Rank(comm))
     148              :   {
     149              :     // First displacement is zero, then after is the partial sum of recv_counts.
     150           56 :     displacements.resize(Mpi::Size(comm));
     151           56 :     displacements[0] = 0;
     152              :     std::partial_sum(recv_counts.begin(), recv_counts.end() - 1, displacements.begin() + 1);
     153              : 
     154              :     // Add on slots at the end of vertices for the incoming data.
     155           56 :     collected_vertices.resize(std::accumulate(recv_counts.begin(), recv_counts.end(), 0));
     156              : 
     157              :     // MPI transfer will be done with MPI_DOUBLE, so duplicate all these values.
     158          116 :     for (auto &x : displacements)
     159              :     {
     160           60 :       x *= 3;
     161              :     }
     162          116 :     for (auto &x : recv_counts)
     163              :     {
     164           60 :       x *= 3;
     165              :     }
     166              :   }
     167              : 
     168              :   // Gather the data to the dominant rank.
     169              :   static_assert(sizeof(Eigen::Vector3d) == 3 * sizeof(double));
     170           60 :   MPI_Gatherv(vertices.data(), 3 * num_vertices, MPI_DOUBLE, collected_vertices.data(),
     171              :               recv_counts.data(), displacements.data(), MPI_DOUBLE, dominant_rank, comm);
     172              : 
     173              :   // Deduplicate vertices. Given floating point precision, need a tolerance.
     174           60 :   if (dominant_rank == Mpi::Rank(comm))
     175              :   {
     176              :     auto vertex_equality = [](const auto &x, const auto &y)
     177              :     {
     178              :       constexpr double tolerance = 10.0 * std::numeric_limits<double>::epsilon();
     179         1240 :       return std::abs(x[0] - y[0]) < tolerance && std::abs(x[1] - y[1]) < tolerance &&
     180          944 :              std::abs(x[2] - y[2]) < tolerance;
     181              :     };
     182              :     vertices = std::move(collected_vertices);
     183           56 :     std::sort(vertices.begin(), vertices.end(), EigenLE);
     184           56 :     vertices.erase(std::unique(vertices.begin(), vertices.end(), vertex_equality),
     185              :                    vertices.end());
     186              :   }
     187              :   else
     188              :   {
     189              :     vertices.clear();
     190              :   }
     191              : 
     192           60 :   return dominant_rank;
     193              : }
     194              : 
     195              : // Compute the distance from a point orthogonal to the list of normal axes, relative to
     196              : // the given origin.
     197         1365 : auto PerpendicularDistance(const std::initializer_list<Eigen::Vector3d> &normals,
     198              :                            const Eigen::Vector3d &origin, const Eigen::Vector3d &v)
     199              : {
     200              :   Eigen::Vector3d v0 = v - origin;
     201         3463 :   for (const auto &n : normals)
     202              :   {
     203              :     v0 -= n.dot(v0) * n;
     204              :   }
     205         1365 :   return v0.norm();
     206              : }
     207              : 
     208              : // Calculates a bounding box from a point cloud, result is broadcast across all processes.
     209           31 : BoundingBox BoundingBoxFromPointCloud(MPI_Comm comm,
     210              :                                       const std::vector<Eigen::Vector3d> &vertices,
     211              :                                       int dominant_rank)
     212              : {
     213              :   BoundingBox box;
     214           31 :   if (dominant_rank == Mpi::Rank(comm))
     215              :   {
     216              :     // Pick a candidate 000 vertex using lexicographic sort. This can be vulnerable to
     217              :     // floating point precision if the box is axis aligned, but not floating point exact.
     218              :     // Pick candidate 111 as the furthest from this candidate, then reassign 000 as the
     219              :     // furthest from 111. Such a pair has to form the diagonal for a point cloud defining a
     220              :     // box. Verify that p_111 is also the maximum distance from p_000 -> a diagonal is
     221              :     // found.
     222           29 :     MFEM_VERIFY(vertices.size() >= 4,
     223              :                 "A bounding box requires a minimum of four vertices for this algorithm!");
     224              :     auto p_000 = std::min_element(vertices.begin(), vertices.end(), EigenLE);
     225              :     auto p_111 =
     226           29 :         std::max_element(vertices.begin(), vertices.end(),
     227          316 :                          [p_000](const Eigen::Vector3d &x, const Eigen::Vector3d &y)
     228          316 :                          { return (x - *p_000).norm() < (y - *p_000).norm(); });
     229           29 :     p_000 = std::max_element(vertices.begin(), vertices.end(),
     230          316 :                              [p_111](const Eigen::Vector3d &x, const Eigen::Vector3d &y)
     231          316 :                              { return (x - *p_111).norm() < (y - *p_111).norm(); });
     232              :     MFEM_ASSERT(std::max_element(vertices.begin(), vertices.end(),
     233              :                                  [p_000](const Eigen::Vector3d &x, const Eigen::Vector3d &y)
     234              :                                  { return (x - *p_000).norm() < (y - *p_000).norm(); }) ==
     235              :                     p_111,
     236              :                 "p_000 and p_111 must be mutually opposing points!");
     237              : 
     238              :     // Define a diagonal of the ASSUMED cuboid bounding box.
     239              :     const auto &v_000 = *p_000;
     240              :     const auto &v_111 = *p_111;
     241           29 :     MFEM_VERIFY(&v_000 != &v_111, "Minimum and maximum extents cannot be identical!");
     242              :     const Eigen::Vector3d origin = v_000;
     243           29 :     const Eigen::Vector3d n_1 = (v_111 - v_000).normalized();
     244              : 
     245              :     // Find the vertex furthest from the diagonal axis. We cannot know yet if this defines
     246              :     // (001) or (011).
     247           29 :     const auto &t_0 = *std::max_element(vertices.begin(), vertices.end(),
     248          316 :                                         [&](const auto &x, const auto &y)
     249              :                                         {
     250          316 :                                           return PerpendicularDistance({n_1}, origin, x) <
     251          316 :                                                  PerpendicularDistance({n_1}, origin, y);
     252              :                                         });
     253           29 :     MFEM_VERIFY(&t_0 != &v_000 && &t_0 != &v_111, "Vertices are degenerate!");
     254              : 
     255              :     // Use the discovered vertex to define a second direction and thus a plane. n_1 and n_2
     256              :     // now define a planar coordinate system intersecting the main diagonal, and two
     257              :     // opposite edges of the cuboid.
     258              :     const Eigen::Vector3d n_2 =
     259           29 :         ((t_0 - origin) - ((t_0 - origin).dot(n_1) * n_1)).normalized();
     260              : 
     261              :     // Collect the furthest point from the plane to determine if the box is planar. Look for
     262              :     // a component that maximizes distance from the planar system: complete the axes with a
     263              :     // cross, then use a dot product to pick the greatest deviation.
     264           29 :     constexpr double rel_tol = 1.0e-6;
     265           29 :     auto max_distance = PerpendicularDistance(
     266              :         {n_1, n_2}, origin,
     267           29 :         *std::max_element(vertices.begin(), vertices.end(),
     268          316 :                           [&](const auto &x, const auto &y)
     269              :                           {
     270          316 :                             return PerpendicularDistance({n_1, n_2}, origin, x) <
     271          316 :                                    PerpendicularDistance({n_1, n_2}, origin, y);
     272           29 :                           }));
     273           29 :     box.planar = (max_distance < rel_tol * (v_111 - v_000).norm());
     274              : 
     275              :     // For the non-planar case, collect points furthest from the plane and choose the one
     276              :     // closest to the origin as the next vertex which might be (001) or (011).
     277           29 :     const auto &t_1 = [&]()
     278              :     {
     279           29 :       if (box.planar)
     280              :       {
     281           21 :         return t_0;
     282              :       }
     283              :       std::vector<Eigen::Vector3d> vertices_out_of_plane;
     284            8 :       std::copy_if(vertices.begin(), vertices.end(),
     285              :                    std::back_inserter(vertices_out_of_plane),
     286           72 :                    [&](const auto &v)
     287              :                    {
     288           72 :                      return std::abs(PerpendicularDistance({n_1, n_2}, origin, v) -
     289           72 :                                      max_distance) < rel_tol * max_distance;
     290              :                    });
     291            8 :       return *std::min_element(vertices_out_of_plane.begin(), vertices_out_of_plane.end(),
     292            0 :                                [&](const Eigen::Vector3d &x, const Eigen::Vector3d &y)
     293            0 :                                { return (x - origin).norm() < (y - origin).norm(); });
     294           29 :     }();
     295              : 
     296              :     // Given candidates t_0 and t_1, the closer to origin defines v_001.
     297              :     const bool t_0_gt_t_1 = (t_0 - origin).norm() > (t_1 - origin).norm();
     298           29 :     const auto &v_001 = t_0_gt_t_1 ? t_1 : t_0;
     299           29 :     const auto &v_011 = box.planar ? v_111 : (t_0_gt_t_1 ? t_0 : t_1);
     300              : 
     301              :     // Compute the center as halfway along the main diagonal.
     302           29 :     Vector3dMap(box.center.data()) = 0.5 * (v_000 + v_111);
     303              : 
     304              :     if constexpr (false)
     305              :     {
     306              :       fmt::print("box.center {}!\n", box.center);
     307              :       fmt::print("v_000 {}!\n", v_000);
     308              :       fmt::print("v_001 {}!\n", v_001);
     309              :       fmt::print("v_011 {}!\n", v_011);
     310              :       fmt::print("v_111 {}!\n", v_111);
     311              :     }
     312              : 
     313              :     // Compute the box axes. Using the 4 extremal points, we find the first two axes as the
     314              :     // edges which are closest to perpendicular. For a perfect rectangular prism point
     315              :     // cloud, we could instead compute the axes and length in each direction using the
     316              :     // found edges of the cuboid, but this does not work for non-rectangular prism
     317              :     // cross-sections or pyramid shapes.
     318              :     {
     319           29 :       const auto [e_0, e_1] = [&v_000, &v_001, &v_011, &v_111]()
     320              :       {
     321           29 :         std::array<const Eigen::Vector3d *, 4> verts = {&v_000, &v_001, &v_011, &v_111};
     322              :         Eigen::Vector3d e_0 = Eigen::Vector3d::Zero(), e_1 = Eigen::Vector3d::Zero();
     323              :         double dot_min = mfem::infinity();
     324           33 :         for (int i_0 = 0; i_0 < 4; i_0++)
     325              :         {
     326           38 :           for (int j_0 = i_0 + 1; j_0 < 4; j_0++)
     327              :           {
     328           86 :             for (int i_1 = 0; i_1 < 4; i_1++)
     329              :             {
     330          200 :               for (int j_1 = i_1 + 1; j_1 < 4; j_1++)
     331              :               {
     332          148 :                 if ((i_1 == i_0 && j_1 == j_0) || verts[i_0] == verts[j_0] ||
     333          109 :                     verts[i_1] == verts[j_1])
     334              :                 {
     335           44 :                   continue;
     336              :                 }
     337          104 :                 const auto e_ij_0 = (*verts[j_0] - *verts[i_0]).normalized();
     338          104 :                 const auto e_ij_1 = (*verts[j_1] - *verts[i_1]).normalized();
     339              :                 const auto dot = std::abs(e_ij_0.dot(e_ij_1));
     340          104 :                 if (dot < dot_min)
     341              :                 {
     342              :                   if constexpr (false)
     343              :                   {
     344              :                     fmt::print("i_0 {} i_1 {} j_0 {} j_1 {}\n", i_0, i_1, j_0, j_1);
     345              :                     fmt::print("e_ij_0 {}, e_ij_1 {}!\n", e_ij_0, e_ij_1);
     346              :                   }
     347              :                   dot_min = dot;
     348              :                   e_0 = e_ij_0;
     349              :                   e_1 = e_ij_1;
     350           66 :                   if (dot_min < rel_tol)
     351              :                   {
     352           28 :                     return std::make_pair(e_0, e_1);
     353              :                   }
     354              :                 }
     355              :               }
     356              :             }
     357              :           }
     358              :         }
     359              :         return std::make_pair(e_0, e_1);
     360           29 :       }();
     361              : 
     362              :       if constexpr (false)
     363              :       {
     364              :         fmt::print("e_0 {}, e_1 {}!\n", e_0, e_1);
     365              :       }
     366              : 
     367              :       Vector3dMap(box.axes[0].data()) = e_0;
     368              :       Vector3dMap(box.axes[1].data()) = e_1;
     369              :       Vector3dMap(box.axes[2].data()) =
     370           29 :           box.planar ? Eigen::Vector3d::Zero() : e_0.cross(e_1);
     371              :     }
     372              : 
     373              :     // Scale axes by length of the box in each direction.
     374           29 :     std::array<double, 3> l = {0.0};
     375          145 :     for (const auto &v : {v_000, v_001, v_011, v_111})
     376              :     {
     377              :       const auto v_0 = v - Vector3dMap(box.center.data());
     378          116 :       l[0] = std::max(l[0], std::abs(v_0.dot(Vector3dMap(box.axes[0].data()))));
     379          116 :       l[1] = std::max(l[1], std::abs(v_0.dot(Vector3dMap(box.axes[1].data()))));
     380          116 :       l[2] = std::max(l[2], std::abs(v_0.dot(Vector3dMap(box.axes[2].data()))));
     381              :     }
     382              :     Vector3dMap(box.axes[0].data()) *= l[0];
     383              :     Vector3dMap(box.axes[1].data()) *= l[1];
     384              :     Vector3dMap(box.axes[2].data()) *= l[2];
     385              : 
     386              :     // Make sure the longest dimension comes first.
     387              :     std::sort(box.axes.begin(), box.axes.end(), [](const auto &x, const auto &y)
     388              :               { return CVector3dMap(x.data()).norm() > CVector3dMap(y.data()).norm(); });
     389              :   }
     390              : 
     391              :   // Broadcast result to all processors.
     392              :   Mpi::Broadcast(3, box.center.data(), dominant_rank, comm);
     393              :   Mpi::Broadcast(3 * 3, box.axes.data()->data(), dominant_rank, comm);
     394              :   Mpi::Broadcast(1, &box.planar, dominant_rank, comm);
     395              : 
     396           31 :   return box;
     397              : }
     398              : 
     399              : // Use 4 points to define a sphere in 3D. If the points are coplanar, 3 of them are used to
     400              : // define a circle which is interpreted as the equator of the sphere. We assume the points
     401              : // are unique and not collinear.
     402            0 : BoundingBall SphereFromPoints(const std::vector<std::size_t> &indices,
     403              :                               const std::vector<Eigen::Vector3d> &vertices)
     404              : {
     405              :   // Given 0 or 1 points, just return a radius of 0.
     406            0 :   MFEM_VERIFY(
     407              :       indices.size() <= 4,
     408              :       "Determining a sphere in 3D requires 4 points (and a circle requires 3 points)!");
     409              :   BoundingBall ball;
     410            0 :   ball.planar = (indices.size() < 4);
     411            0 :   if (indices.size() < 2)
     412              :   {
     413              :     ball.origin = Eigen::Vector3d::Zero();
     414            0 :     ball.radius = 0.0;
     415            0 :     return ball;
     416              :   }
     417              : 
     418              :   // For two points, construct a circle with the segment as its diameter. This could also
     419              :   // handle the collinear case for more than 2 points.
     420            0 :   if (indices.size() == 2)
     421              :   {
     422            0 :     ball.origin = 0.5 * (vertices[indices[0]] + vertices[indices[1]]);
     423            0 :     ball.radius = (vertices[indices[0]] - ball.origin).norm();
     424            0 :     return ball;
     425              :   }
     426              : 
     427              :   // Check for coplanarity.
     428              :   constexpr double rel_tol = 1.0e-6;
     429            0 :   const Eigen::Vector3d AB = vertices[indices[1]] - vertices[indices[0]];
     430            0 :   const Eigen::Vector3d AC = vertices[indices[2]] - vertices[indices[0]];
     431              :   const Eigen::Vector3d ABAC = AB.cross(AC);
     432              :   Eigen::Vector3d AD = Eigen::Vector3d::Zero();
     433            0 :   if (!ball.planar)
     434              :   {
     435            0 :     AD = vertices[indices[3]] - vertices[indices[0]];
     436            0 :     ball.planar = (std::abs(AD.dot(ABAC)) < rel_tol * AD.norm() * ABAC.norm());
     437              :   }
     438              : 
     439              :   // Construct a circle passing through 3 points.
     440              :   // See: https://en.wikipedia.org/wiki/Circumcircle#Higher_dimensions.
     441            0 :   if (ball.planar)
     442              :   {
     443            0 :     ball.origin = (0.5 / ABAC.squaredNorm()) *
     444            0 :                   ((AB.squaredNorm() * AC) - (AC.squaredNorm() * AB)).cross(ABAC);
     445            0 :     ball.radius = ball.origin.norm();
     446            0 :     ball.origin += vertices[indices[0]];
     447              : #if defined(MFEM_DEBUG)
     448              :     const auto r1 = (vertices[indices[1]] - ball.origin).norm();
     449              :     const auto r2 = (vertices[indices[2]] - ball.origin).norm();
     450              :     MFEM_VERIFY((1.0 - rel_tol) * ball.radius < r1 && r1 < (1.0 + rel_tol) * ball.radius &&
     451              :                     (1.0 - rel_tol) * ball.radius < r2 &&
     452              :                     r2 < (1.0 + rel_tol) * ball.radius,
     453              :                 "Invalid circle calculated from 3 points!");
     454              : #endif
     455              :     return ball;
     456              :   }
     457              : 
     458              :   // Construct a sphere passing through 4 points.
     459              :   // See: https://steve.hollasch.net/cgindex/geometry/sphere4pts.html.
     460              :   Eigen::Matrix3d C;
     461              :   Eigen::Vector3d d;
     462            0 :   const auto s = vertices[indices[0]].squaredNorm();
     463              :   C.row(0) = AB.transpose();
     464              :   C.row(1) = AC.transpose();
     465              :   C.row(2) = AD.transpose();
     466            0 :   d(0) = 0.5 * (vertices[indices[1]].squaredNorm() - s);
     467            0 :   d(1) = 0.5 * (vertices[indices[2]].squaredNorm() - s);
     468            0 :   d(2) = 0.5 * (vertices[indices[3]].squaredNorm() - s);
     469            0 :   ball.origin = C.inverse() * d;  // 3x3 matrix inverse might be faster than general LU
     470              :                                   // if Eigen uses the explicit closed-form solution
     471            0 :   ball.radius = (vertices[indices[0]] - ball.origin).norm();
     472              : #if defined(MFEM_DEBUG)
     473              :   const auto r1 = (vertices[indices[1]] - ball.origin).norm();
     474              :   const auto r2 = (vertices[indices[2]] - ball.origin).norm();
     475              :   const auto r3 = (vertices[indices[3]] - ball.origin).norm();
     476              :   MFEM_VERIFY((1.0 - rel_tol) * ball.radius < r1 && r1 < (1.0 + rel_tol) * ball.radius &&
     477              :                   (1.0 - rel_tol) * ball.radius < r2 &&
     478              :                   r2 < (1.0 + rel_tol) * ball.radius &&
     479              :                   (1.0 - rel_tol) * ball.radius < r3 && r3 < (1.0 + rel_tol) * ball.radius,
     480              :               "Invalid sphere calculated from 3 points!");
     481              : #endif
     482            0 :   return ball;
     483              : }
     484              : 
     485            0 : BoundingBall Welzl(std::vector<std::size_t> P, std::vector<std::size_t> R,
     486              :                    const std::vector<Eigen::Vector3d> &vertices)
     487              : {
     488              :   // Base case.
     489            0 :   if (R.size() == 4 || P.empty())
     490              :   {
     491            0 :     return SphereFromPoints(R, vertices);
     492              :   }
     493              : 
     494              :   // Choose a p ∈ P randomly, and recurse for (P \ {p}, R). The set P has already been
     495              :   // randomized on input.
     496            0 :   const std::size_t p = P.back();
     497              :   P.pop_back();
     498            0 :   BoundingBall D = Welzl(P, R, vertices);
     499              : 
     500              :   // If p is outside the sphere, recurse for (P \ {p}, R U {p}).
     501              :   constexpr double rel_tol = 1.0e-6;
     502            0 :   if ((vertices[p] - D.origin).norm() >= (1.0 + rel_tol) * D.radius)
     503              :   {
     504              :     R.push_back(p);
     505            0 :     D = Welzl(P, R, vertices);
     506              :   }
     507              : 
     508              :   return D;
     509              : }
     510              : 
     511              : // Calculates a bounding ball from a point cloud using Welzl's algorithm, result is
     512              : // broadcast across all processes. We don't operate on the convex hull, since the number of
     513              : // points should be small enough that operating on the full set should be OK. If only three
     514              : // points are provided, the bounding circle is computed (likewise for if the points are
     515              : // coplanar).
     516            0 : BoundingBox BoundingBallFromPointCloud(MPI_Comm comm,
     517              :                                        const std::vector<Eigen::Vector3d> &vertices,
     518              :                                        int dominant_rank)
     519              : {
     520              :   BoundingBox ball;
     521            0 :   if (dominant_rank == Mpi::Rank(comm))
     522              :   {
     523            0 :     MFEM_VERIFY(vertices.size() >= 3,
     524              :                 "A bounding ball requires a minimum of three vertices for this algorithm!");
     525            0 :     std::vector<std::size_t> indices(vertices.size());
     526              :     std::iota(indices.begin(), indices.end(), 0);
     527              : 
     528              :     // Acceleration from https://informatica.vu.lt/journal/INFORMATICA/article/1251. Allow
     529              :     // for duplicate points and just add the 4 points to the end of the indicies list to be
     530              :     // considered first. The two points are not necessarily the maximizer of the distance
     531              :     // between all pairs, but they should be a good estimate.
     532              :     {
     533            0 :       auto p_1 = std::min_element(vertices.begin(), vertices.end(), EigenLE);
     534            0 :       auto p_2 = std::max_element(vertices.begin(), vertices.end(),
     535            0 :                                   [p_1](const Eigen::Vector3d &x, const Eigen::Vector3d &y)
     536            0 :                                   { return (x - *p_1).norm() < (y - *p_1).norm(); });
     537            0 :       p_1 = std::max_element(vertices.begin(), vertices.end(),
     538            0 :                              [p_2](const Eigen::Vector3d &x, const Eigen::Vector3d &y)
     539            0 :                              { return (x - *p_2).norm() < (y - *p_2).norm(); });
     540              : 
     541              :       // Find the next point as the vertex furthest from the initial axis.
     542            0 :       const Eigen::Vector3d n_1 = (*p_2 - *p_1).normalized();
     543            0 :       auto p_3 = std::max_element(vertices.begin(), vertices.end(),
     544            0 :                                   [&](const auto &x, const auto &y)
     545              :                                   {
     546            0 :                                     return PerpendicularDistance({n_1}, *p_1, x) <
     547            0 :                                            PerpendicularDistance({n_1}, *p_1, y);
     548              :                                   });
     549            0 :       auto p_4 = std::max_element(vertices.begin(), vertices.end(),
     550            0 :                                   [p_3](const Eigen::Vector3d &x, const Eigen::Vector3d &y)
     551            0 :                                   { return (x - *p_3).norm() < (y - *p_3).norm(); });
     552            0 :       MFEM_VERIFY(p_3 != p_1 && p_3 != p_2 && p_4 != p_1 && p_4 != p_2,
     553              :                   "Vertices are degenerate!");
     554              : 
     555              :       // Start search with these points, which should be roughly extremal. With the search
     556              :       // for p_3 done in an orthogonal direction, p_1, p_2, p_3, and p_4 should all be
     557              :       // unique.
     558            0 :       std::swap(indices[indices.size() - 1], indices[p_1 - vertices.begin()]);
     559            0 :       std::swap(indices[indices.size() - 2], indices[p_2 - vertices.begin()]);
     560            0 :       std::swap(indices[indices.size() - 3], indices[p_3 - vertices.begin()]);
     561            0 :       std::swap(indices[indices.size() - 4], indices[p_4 - vertices.begin()]);
     562              :     }
     563              : 
     564              :     // Randomly permute the point set.
     565              :     {
     566            0 :       std::random_device rd;
     567            0 :       std::mt19937 g(rd());
     568            0 :       std::shuffle(indices.begin(), indices.end() - 4, g);
     569              :     }
     570              : 
     571              :     // Compute the bounding ball.
     572            0 :     BoundingBall min_ball = Welzl(indices, {}, vertices);
     573              :     Vector3dMap(ball.center.data()) = min_ball.origin;
     574            0 :     Vector3dMap(ball.axes[0].data()) = Eigen::Vector3d(min_ball.radius, 0.0, 0.0);
     575            0 :     Vector3dMap(ball.axes[1].data()) = Eigen::Vector3d(0.0, min_ball.radius, 0.0);
     576              :     Vector3dMap(ball.axes[2].data()) = Eigen::Vector3d(0.0, 0.0, min_ball.radius);
     577            0 :     ball.planar = min_ball.planar;
     578              :   }
     579              : 
     580              :   // Broadcast result to all processors.
     581              :   Mpi::Broadcast(3, ball.center.data(), dominant_rank, comm);
     582              :   Mpi::Broadcast(3 * 3, ball.axes.data()->data(), dominant_rank, comm);
     583              :   Mpi::Broadcast(1, &ball.planar, dominant_rank, comm);
     584              : 
     585            0 :   return ball;
     586              : }
     587              : 
     588              : // Compute a normal vector from an element transformation, optionally ensure aligned
     589              : // (| normal ⋅ align | > 0)
     590          240 : void Normal(mfem::ElementTransformation &T, mfem::Vector &normal,
     591              :             const mfem::Vector *const align)
     592              : {
     593              :   const mfem::IntegrationPoint &ip = mfem::Geometries.GetCenter(T.GetGeometryType());
     594              :   T.SetIntPoint(&ip);
     595          240 :   mfem::CalcOrtho(T.Jacobian(), normal);
     596          240 :   normal /= normal.Norml2();
     597          240 :   if (align && (normal * (*align) < 0))
     598              :   {
     599           12 :     normal *= -1;
     600              :   }
     601          240 : }
     602              : 
     603              : // Compute the centroid of a container of vertices.
     604              : template <typename T>
     605           26 : mfem::Vector ComputeCentroid(const std::unique_ptr<mfem::Mesh> &mesh, const T &vertidxs)
     606              : {
     607              :   mfem::Vector centroid(mesh->SpaceDimension());
     608           26 :   centroid = 0.0;
     609              :   int c = 0;
     610          156 :   for (const int v : vertidxs)
     611              :   {
     612          130 :     centroid += mfem::Vector(mesh->GetVertex(v), 3);
     613          130 :     c++;
     614              :   }
     615           52 :   return centroid /= c;
     616              : }
     617              : 
     618              : // Compute the normal vector for a set of elements. If "inside" is true, normal will
     619              : // point inside the mesh, otherwise it will point outside the mesh.
     620              : template <typename T>
     621            2 : mfem::Vector ComputeNormal(const std::unique_ptr<mfem::Mesh> &mesh, const T &elem_set,
     622              :                            bool inside, bool check_planar = true)
     623              : {
     624              :   const int sdim = mesh->SpaceDimension();
     625            2 :   mfem::IsoparametricTransformation trans;
     626              :   mfem::Vector normal(sdim), last_normal(sdim), align(sdim);
     627            2 :   normal = 0.0;
     628            2 :   last_normal = 0.0;
     629              :   mfem::Array<int> vert_bdr;
     630              : 
     631              :   // Ensure that the computed normal points "inside" or "outside".
     632           26 :   auto Alignment = [&](int el, auto &align)
     633              :   {
     634              :     int eladj, info;
     635           24 :     mesh->GetBdrElementAdjacentElement(el, eladj, info);
     636           24 :     mesh->GetElementCenter(eladj, align);
     637           24 :     mesh->GetBdrElementVertices(el, vert_bdr);
     638           24 :     align -= ComputeCentroid(mesh, vert_bdr);
     639           24 :     if (!inside)  // align points inwards
     640              :     {
     641           12 :       align *= -1;
     642              :     }
     643              :   };
     644              : 
     645           26 :   for (auto elem : elem_set)
     646              :   {
     647           24 :     Alignment(elem, align);
     648           24 :     mesh->GetBdrElementTransformation(elem, &trans);
     649           24 :     mesh::Normal(trans, normal, &align);
     650           24 :     if (!check_planar)
     651              :     {
     652              :       break;  // If not checking planar, use the first.
     653              :     }
     654           24 :     MFEM_VERIFY((last_normal * last_normal == 0.0) || ((last_normal * normal - 1) < 1e-8),
     655              :                 "Periodic boundary mapping is only supported for planar boundaries!");
     656           24 :     last_normal = normal;
     657              :   }
     658            2 :   return normal;
     659            2 : }
     660              : 
     661              : struct Frame
     662              : {
     663            0 :   Frame(const mfem::Vector &o) : origin(o)
     664              :   {
     665            0 :     for (auto &x : basis)
     666              :     {
     667            0 :       x.SetSize(3);
     668            0 :       x = 0.0;
     669              :     }
     670            0 :   }
     671              :   mfem::Vector origin;
     672              :   std::array<mfem::Vector, 3> basis;
     673              : };
     674              : 
     675            0 : Frame Find3DFrame(std::unique_ptr<mfem::Mesh> &mesh,
     676              :                   const std::unordered_set<int> &vertidxs, const mfem::Vector &centroid,
     677              :                   const mfem::Vector &normal, double mesh_dim)
     678              : {
     679            0 :   Frame frame(centroid);
     680            0 :   frame.basis[0] = normal;
     681              : 
     682              :   // For each point, compute its distance to the centroid.
     683              :   std::map<int, std::vector<int>, std::greater<int>> dist2points;
     684            0 :   for (const int v : vertidxs)
     685              :   {
     686              :     auto dist = centroid.DistanceTo(mesh->GetVertex(v));
     687              :     // Convert dist to integer to avoid floating point differences.
     688            0 :     dist2points[std::round(dist / mesh_dim * 1e8)].push_back(v);
     689              :   }
     690              : 
     691            0 :   for (const auto &[dist, verts] : dist2points)
     692              :   {
     693            0 :     if (verts.size() > 1 || dist == 0)
     694              :     {
     695              :       continue;
     696              :     }
     697            0 :     frame.basis[1] = mesh->GetVertex(verts.front());
     698            0 :     frame.basis[1] -= centroid;
     699            0 :     frame.basis[1] /= frame.basis[1].Norml2();
     700              :     break;
     701              :   }
     702              : 
     703              :   // Define final point by computing the cross product.
     704            0 :   frame.basis[0].cross3D(frame.basis[1], frame.basis[2]);
     705              : 
     706            0 :   return frame;
     707            0 : }
     708              : 
     709              : // Calculate the rotation matrix between two vectors.
     710            0 : void ComputeRotation(const mfem::Vector &normal1, const mfem::Vector &normal2,
     711              :                      mfem::DenseMatrix &transformation)
     712              : {
     713            0 :   mfem::DenseMatrix R(3), vx(3), vx2(3);
     714              : 
     715              :   mfem::Vector v(normal1.Size());
     716            0 :   normal1.cross3D(normal2, v);
     717            0 :   double c = normal1 * normal2;
     718              : 
     719            0 :   vx(0, 1) = -v[2];
     720            0 :   vx(0, 2) = v[1];
     721            0 :   vx(1, 0) = v[2];
     722            0 :   vx(1, 2) = -v[0];
     723            0 :   vx(2, 0) = -v[1];
     724            0 :   vx(2, 1) = v[0];
     725              : 
     726            0 :   R(0, 0) = R(1, 1) = R(2, 2) = 1.0;
     727            0 :   R += vx;
     728            0 :   Mult(vx, vx, vx2);
     729            0 :   if (std::abs(1.0 + c) > 1e-8)
     730              :   {
     731            0 :     vx2.Set(1.0 / (1.0 + c), vx2);
     732              :   }
     733            0 :   R += vx2;
     734              : 
     735            0 :   for (int i = 0; i < 3; i++)
     736              :   {
     737            0 :     for (int j = 0; j < 3; j++)
     738              :     {
     739            0 :       transformation(i, j) = R(i, j);
     740              :     }
     741              :   }
     742            0 : }
     743              : 
     744            0 : mfem::DenseMatrix ComputeAffineTransformationMatrix(const Frame &donor,
     745              :                                                     const Frame &receiver)
     746              : {
     747              : 
     748            0 :   mfem::DenseMatrix A(4, 4);
     749            0 :   A = 0.0;
     750            0 :   if (donor.basis[1].Norml2() > 0.0 && receiver.basis[1].Norml2() > 0.0)
     751              :   {
     752              :     // Stably compute the rotation matrix from unit vectors.
     753              :     Eigen::Matrix3d source, target;
     754              :     Eigen::Vector3d source_centroid, target_centroid;
     755            0 :     for (int i = 0; i < 3; i++)
     756              :     {
     757            0 :       for (int j = 0; j < 3; j++)
     758              :       {
     759            0 :         source(i, j) = donor.basis[j](i);
     760            0 :         target(i, j) = receiver.basis[j](i);
     761              :       }
     762            0 :       source_centroid(i) = donor.origin(i);
     763            0 :       target_centroid(i) = receiver.origin(i);
     764              :     }
     765              :     Eigen::Matrix3d R = source * target.transpose();
     766              :     Eigen::JacobiSVD<Eigen::Matrix3d> svd(R, Eigen::ComputeFullU | Eigen::ComputeFullV);
     767            0 :     R = svd.matrixV() * svd.matrixU().transpose();
     768              : 
     769              :     // Account for possible reflection in R (det(R) = -1).
     770              :     Eigen::DiagonalMatrix<double, 3> Diag(1.0, 1.0, R.determinant());
     771            0 :     R = svd.matrixV() * Diag * svd.matrixU().transpose();
     772              : 
     773              :     // Compute translation and form transformation matrix.
     774            0 :     const Eigen::Vector3d translation = target_centroid - R * source_centroid;
     775            0 :     for (int i = 0; i < 3; i++)
     776              :     {
     777            0 :       for (int j = 0; j < 3; j++)
     778              :       {
     779            0 :         A(i, j) = R(i, j);
     780              :       }
     781            0 :       A(i, 3) = translation(i);
     782              :     }
     783            0 :     A(3, 3) = 1.0;
     784              :   }
     785              :   else
     786              :   {
     787              :     // If the donor or receiver basis is ambiguous, we assume no rotation around the
     788              :     // normals, and that the rotation comes only from realigning normal vectors.
     789            0 :     ComputeRotation(donor.basis[0], receiver.basis[0], A);
     790            0 :     for (int i = 0; i < 3; i++)
     791              :     {
     792            0 :       A(i, 3) = receiver.origin(i) - donor.origin(i);
     793              :     }
     794            0 :     A(3, 3) = 1.0;
     795              :   }
     796              : 
     797            0 :   return A;
     798            0 : }
     799              : 
     800              : // Create the vertex mapping between sets of donor and receiver pts related
     801              : // by an affine transformation matrix.
     802            0 : std::vector<int> CreatePeriodicVertexMapping(std::unique_ptr<mfem::Mesh> &mesh,
     803              :                                              const std::unordered_set<int> &donor_v,
     804              :                                              const std::unordered_set<int> &receiver_v,
     805              :                                              const mfem::DenseMatrix &transform,
     806              :                                              double tol = 1e-6)
     807              : {
     808              :   // Similar to MFEM's CreatePeriodicVertexMapping, maps from replica to primary vertex.
     809              :   std::unordered_map<int, int> replica2primary;
     810              : 
     811              :   // KD-tree containing all the receiver points.
     812              :   mfem::KDTree3D kdtree;
     813            0 :   for (const int v : receiver_v)
     814              :   {
     815            0 :     kdtree.AddPoint(mesh->GetVertex(v), v);
     816              :   }
     817              :   kdtree.Sort();
     818              : 
     819              :   // Loop over donor points and find the corresponding receiver point.
     820              :   mfem::Vector from(4), to(4);
     821            0 :   for (int vi : donor_v)
     822              :   {
     823              :     // TODO: mfem patch to allow SetVector direct from pointer.
     824            0 :     std::copy(mesh->GetVertex(vi), mesh->GetVertex(vi) + 3, from.begin());
     825            0 :     from[3] = 1.0;             // reset
     826            0 :     transform.Mult(from, to);  // receiver = transform * donor
     827              : 
     828            0 :     const int vj = kdtree.FindClosestPoint(to.GetData());
     829            0 :     std::copy(mesh->GetVertex(vj), mesh->GetVertex(vj) + 3, from.begin());
     830            0 :     from -= to;  // Check that the loaded vertex is identical to the transformed
     831            0 :     MFEM_VERIFY(from.Norml2() < tol,
     832              :                 "Could not match points on periodic boundaries, transformed donor point "
     833              :                 "does not correspond to a receiver point!");
     834            0 :     MFEM_VERIFY(replica2primary.find(vj) == replica2primary.end(),
     835              :                 "Could not match points on periodic boundaries, multiple donor points map "
     836              :                 "to the same receiver point!")
     837            0 :     replica2primary[vj] = vi;
     838              :   }
     839              : 
     840            0 :   std::vector<int> v2v(mesh->GetNV());
     841              :   std::iota(v2v.begin(), v2v.end(), 0);
     842            0 :   for (const auto &[r, p] : replica2primary)
     843              :   {
     844            0 :     v2v[r] = p;
     845              :   }
     846            0 :   return v2v;
     847              : }
     848              : 
     849              : // Determine the vertex mapping between donor and receiver boundary attributes.
     850              : // Uses the translation vector or affine transformation matrix specified in the
     851              : // configuration file. If not provided, attempts to automatically detect the
     852              : // affine transformation between donor and receiver boundary vertices.
     853              : std::vector<int>
     854            1 : DeterminePeriodicVertexMapping(std::unique_ptr<mfem::Mesh> &mesh,
     855              :                                const struct palace::config::PeriodicData &data,
     856              :                                const double tol)
     857              : {
     858              :   // Get mesh dimensions, will be used to define a reasonable tolerance in mesh units.
     859              :   mfem::Vector bbmin, bbmax;
     860            1 :   mesh->GetBoundingBox(bbmin, bbmax);
     861            1 :   bbmax -= bbmin;
     862            1 :   const double mesh_dim = bbmax.Norml2();
     863            1 :   const double mesh_tol = tol * mesh_dim;
     864              : 
     865              :   // Identify donor and receiver vertices and elements.
     866              :   const auto &da = data.donor_attributes, &ra = data.receiver_attributes;
     867              :   std::unordered_set<int> bdr_v_donor, bdr_v_receiver;
     868              :   std::unordered_set<int> bdr_e_donor, bdr_e_receiver;
     869           49 :   for (int be = 0; be < mesh->GetNBE(); be++)
     870              :   {
     871           48 :     int attr = mesh->GetBdrAttribute(be);
     872           48 :     auto donor = std::find(da.begin(), da.end(), attr) != da.end();
     873           48 :     auto receiver = std::find(ra.begin(), ra.end(), attr) != ra.end();
     874           48 :     if (donor || receiver)
     875              :     {
     876              :       int el, info;
     877           24 :       mesh->GetBdrElementAdjacentElement(be, el, info);
     878              :       mfem::Array<int> vertidxs;
     879              :       mesh->GetBdrElementVertices(be, vertidxs);
     880           24 :       (donor ? bdr_e_donor : bdr_e_receiver).insert(be);
     881           24 :       (donor ? bdr_v_donor : bdr_v_receiver).insert(vertidxs.begin(), vertidxs.end());
     882              :     }
     883              :   }
     884              : 
     885            1 :   MFEM_VERIFY(bdr_v_donor.size() == bdr_v_receiver.size(),
     886              :               "Different number of "
     887              :               "vertices on donor and receiver boundaries. Cannot create periodic mesh.");
     888              : 
     889              :   // Check if mesh has enough elements in periodic direction. MFEM's periodicity
     890              :   // fails for meshes with <=2 elements in the period direction.
     891              :   // Compare the number of mesh elements to the number of periodic boundary
     892              :   // elements.
     893            1 :   const int num_periodic_bc_elems = bdr_e_donor.size() + bdr_e_receiver.size();
     894              :   mfem::Array<mfem::Geometry::Type> geoms;
     895            1 :   mesh->GetGeometries(3, geoms);
     896            1 :   if (geoms.Size() == 1 && geoms[0] == mfem::Geometry::TETRAHEDRON)
     897              :   {
     898              :     // Pure tet mesh.
     899            0 :     MFEM_VERIFY(mesh->GetNE() > 3 * num_periodic_bc_elems,
     900              :                 "Not enough mesh elements in periodic direction!");
     901              :   }
     902              :   else
     903              :   {
     904              :     // No tets.
     905            1 :     MFEM_VERIFY(mesh->GetNE() > num_periodic_bc_elems,
     906              :                 "Not enough mesh elements in periodic direction!");
     907              :   }
     908              : 
     909              :   // Determine the affine transformation between donor and receiver points.
     910              :   // Use the translation vector or affine transformation matrix if provided
     911              :   // in the config file, otherwise automatically detect the transformation.
     912            1 :   mfem::DenseMatrix transformation(4);
     913            1 :   if (std::any_of(data.affine_transform.begin(), data.affine_transform.end(),
     914              :                   [](auto y) { return std::abs(y) > 0.0; }))
     915              :   {
     916              :     // Use user-provided affine transformation matrix.
     917            0 :     for (int i = 0; i < 4; i++)
     918              :     {
     919            0 :       for (int j = 0; j < 4; j++)
     920              :       {
     921            0 :         transformation(i, j) = data.affine_transform[i * 4 + j];  // row major conversion
     922              :       }
     923              :     }
     924              :   }
     925              :   else
     926              :   {
     927              :     // Automatically detect transformation.
     928              :     // Compute the centroid for each boundary.
     929            1 :     auto donor_centroid = ComputeCentroid(mesh, bdr_v_donor);
     930            1 :     auto receiver_centroid = ComputeCentroid(mesh, bdr_v_receiver);
     931              : 
     932              :     // Compute the normal vector for each boundary.
     933            1 :     auto donor_normal = ComputeNormal(mesh, bdr_e_donor, true);
     934            1 :     auto receiver_normal = ComputeNormal(mesh, bdr_e_receiver, false);
     935              : 
     936              :     // Return empty mapping if centroids and normal vectors are the same (up to a sign).
     937            1 :     mfem::Vector diff = donor_centroid;
     938            1 :     diff -= receiver_centroid;
     939            1 :     double dot = donor_normal * receiver_normal;
     940            1 :     if (diff.Norml2() < mesh_tol && std::abs(std::abs(dot) - 1.0) < mesh_tol)
     941              :     {
     942            1 :       return {};
     943              :     }
     944              : 
     945              :     // Compute a frame (origin, normal, and two in plane points) for each boundary.
     946              :     auto donor_frame =
     947            0 :         Find3DFrame(mesh, bdr_v_donor, donor_centroid, donor_normal, mesh_dim);
     948              :     auto receiver_frame =
     949            0 :         Find3DFrame(mesh, bdr_v_receiver, receiver_centroid, receiver_normal, mesh_dim);
     950              : 
     951              :     // Compute the affine transformation matrix.
     952            0 :     transformation = ComputeAffineTransformationMatrix(donor_frame, receiver_frame);
     953            0 :   }
     954              :   return CreatePeriodicVertexMapping(mesh, bdr_v_donor, bdr_v_receiver, transformation,
     955            0 :                                      mesh_tol);
     956            1 : }
     957              : 
     958              : }  // namespace palace::mesh
        

Generated by: LCOV version 2.0-1