Program Listing for File triangulation.hpp

Return to documentation for file (src/triangulation.hpp)

/*
// Copyright © 2019,2020 Greg Tucker <greg.tucker@stfc.ac.uk>
//
// This file is part of brille.
//
// brille is free software: you can redistribute it and/or modify it under the
// terms of the GNU Affero General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// brille is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
// or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with brille. If not, see <https://www.gnu.org/licenses/>.
*/

#ifndef BRILLE_TRIANGULATION_HPP_
#define BRILLE_TRIANGULATION_HPP_
#include <vector>
#include <array>
#include <cassert>
#include <algorithm>
#include "array_latvec.hpp" // defines bArray
#include "tetgen.h"
#include "debug.hpp"
#include "balltrellis.hpp"
#include "approx.hpp"
namespace brille {

template<class T, size_t N> static size_t find_first(const std::array<T,N>& x, const T val){
  auto at = std::find(x.begin(), x.end(), val);
  if (at == x.end()) throw std::logic_error("Value not found?!");
  return std::distance(x.begin(), at);
}

// // In case we need to store the input polyhedron and mesh-refining parameters:
// class TetgenInput{
//   bArray<double> _vertices;
//   std::vector<std::vector<int>> _vertices_per_facet;
//   double _max_cell_size;
//   double _min_dihedral_angle;
//   double _max_dihedral_angle;
//   double _radius_edge_ratio;
//   int _max_mesh_points;
// public:
//   TetgenInput(
//     const bArray<double>& v, const std::vector<std::vector<int>>& f,
//     const double mcs=-1.0, const double nda=-1.0, const double xda=-1.0,
//     const double rer=-1.0, const int mmp=-1):
//       _vertices(v), _vertices_per_facet(f), _max_cell_size(mcs),
//       _min_dihedral_angle(nda), _max_dihedral_angle(xda),
//       _radius_edge_ratio(rer), _max_mesh_points(mmp) {}
//   const bArray<double>& vertices() const { return _vertices; }
//   const std::vector<std::vector<int>>& vertices_per_facet() const {return _vertices_per_facet; }
//   double max_cell_size() const { return _max_cell_size; }
//   double max_dihedral_angle() const { return _max_dihedral_angle; }
//   double min_dihedral_angle() const { return _min_dihedral_angle; }
//   double radius_edge_ratio() const { return _radius_edge_ratio; }
//   int max_mesh_points() const { return _max_mesh_points; }
// };

// template<class T, template<class> class L, typename=typename std::enable_if<std::is_base_of<bArray<T>,L<T>>::value>::type>
class TetTri{
  // L<T> vertex_positions; // (nVertices, 3), bArray<T>, LQVec<T>, or LDVec<T>
  size_t nVertices;
  size_t nTetrahedra;
  bArray<double> vertex_positions; // (nVertices, 3)
  bArray<size_t> vertices_per_tetrahedron; // (nTetrahedra, 4)
  std::vector<std::vector<size_t>> tetrahedra_per_vertex; // (nVertices,)(1+,)
  std::vector<std::vector<size_t>> neighbours_per_tetrahedron; // (nTetrahedra,)(1+,)
  //tetgenio tgsource; // we need to store the output of tetgen so that we can refine the mesh
  // BallNode tetrahedraTree;
  // std::vector<BallLeaf> leaves;
  Trellis tetrahedraTrellis;
  std::vector<TrellisLeaf> leaves;
public:
  size_t number_of_vertices(void) const {return nVertices;}
  size_t number_of_tetrahedra(void) const {return nTetrahedra;}
  const bArray<double>& get_vertex_positions(void) const {return vertex_positions;}
  const bArray<size_t>& get_vertices_per_tetrahedron(void) const {return vertices_per_tetrahedron;}

  TetTri(void): nVertices(0), nTetrahedra(0), vertex_positions({0u,3u}), vertices_per_tetrahedron({0u,4u}){}
  TetTri(const tetgenio& tgio, const double fraction): vertex_positions({0u,3u}), vertices_per_tetrahedron({0u,4u}){ //, tgsource(tgio){
    nVertices = static_cast<size_t>(tgio.numberofpoints);
    nTetrahedra = static_cast<size_t>(tgio.numberoftetrahedra);
    // copy-over all vertex positions:
    vertex_positions.resize(nVertices);
    for (size_t i=0; i<nVertices; ++i)
    for (size_t j=0; j<3u; ++j)
    vertex_positions.val(i,j) = tgio.pointlist[3*i+j];
    // copy-over all tetrahedron vertex indices
    vertices_per_tetrahedron.resize(nTetrahedra);
    for (size_t i=0; i<nTetrahedra; ++i)
    for (size_t j=0; j<4u; ++j)
    vertices_per_tetrahedron.val(i,j) = static_cast<size_t>(tgio.tetrahedronlist[i*tgio.numberofcorners+j]);
    // for (size_t i=0; i<nTetrahedra; ++i)
    // Construct the tetrahedra per vertex vector of vectors
    tetrahedra_per_vertex.resize(nVertices);
    for (size_t i=0; i<nVertices; ++i)
    for (size_t j=0; j<nTetrahedra; ++j)
    for (size_t k=0; k<4u; ++k)
    if (vertices_per_tetrahedron.val(j,k)==i)
      tetrahedra_per_vertex[i].push_back(j);
    // Construct the neighbours per tetrahedron vector of vectors
    neighbours_per_tetrahedron.resize(nTetrahedra);
    for (size_t i=0; i<nTetrahedra; ++i)
    for (size_t j=0; j<4u; ++j)
    if (tgio.neighborlist[i*4u+j] >= 0)
      neighbours_per_tetrahedron[i].push_back(static_cast<size_t>(tgio.neighborlist[i*4u+j]));
    // ensure that all tetrahedra have positive (orient3d) volume
    this->correct_tetrahedra_vertex_ordering();
    // construct the tree for faster locating:
    this->make_balltree(fraction);
    // Create a string full of object information:
  }
  std::string to_string(void) const {
    std::string str;
    str  = std::to_string(nVertices) + " vertices";
    str += " in " + std::to_string(nTetrahedra) + " tetrahedra";
    str += " with a Trellis[" + tetrahedraTrellis.to_string() + "]";
    return str;
  }
  // emulate the locate functionality of CGAL -- for which we need an enumeration
  enum Locate_Type{ VERTEX, EDGE, FACET, CELL, OUTSIDE_CONVEX_HULL };
  size_t old_locate(const bArray<double>& x, Locate_Type& type, size_t& v0, size_t& v1) const{
    std::vector<size_t> v;
    std::vector<double> w;
    size_t idx = this->locate(x, v, w);
    switch (v.size()){
      case 0:
        type = OUTSIDE_CONVEX_HULL;
        return 0;
      case 1:
        type = VERTEX;
        v0 = v[0];
        break;
      case 2:
        type = EDGE;
        v0 = v[0];
        v1 = v[1];
        break;
      case 3:
        type = FACET;
        for (size_t i=0; i<4u; ++i){
          v0 = vertices_per_tetrahedron.val(idx, i);
          if (std::find(v.begin(), v.end(), v0) == v.end()) break;
        }
        break;
      case 4:
        type = CELL;
        break;
      default:
        throw std::logic_error("Intentionally unreachable switch statement..");
    }
    return idx;
  }
  // Make a new locator which slots into the interpolation routine more easily
  // size_t locate(const bArray<double>& x, std::vector<size_t>& v, std::vector<double>& w) const {
  //   if (x.numel() != 3u || x.size() != 1u)
  //     throw std::runtime_error("locate requires a single 3-element vector.");
  //   std::array<double,4> ws;
  //   v.clear();
  //   w.clear(); // make sure w is back to zero-elements
  //
  //   size_t tet_idx;
  //   for (tet_idx=0; tet_idx < nTetrahedra; ++tet_idx) if (this->might_contain(tet_idx, x)){
  //     this->weights(tet_idx, x, ws);
  //     // if all weights are greater or equal to ~zero, we can use this tetrahedron
  //     if (std::all_of(ws.begin(), ws.end(), [](double z){return z>0. || brille::approx::scalar(z,0.);})){
  //       for (size_t i=0; i<4u; ++i) if (!brille::approx::scalar(ws[i], 0.)){
  //         v.push_back(vertices_per_tetrahedron.getvalue(tet_idx, i));
  //         w.push_back(ws[i]);
  //       }
  //       break;
  //     }
  //   }
  //   return tet_idx;
  // }
  size_t locate(const bArray<double>& x, std::vector<size_t>& v, std::vector<double>& w) const {
    if (x.ndim()!=2u || x.size(0)!=1u || x.size(1)!=3u)
      throw std::runtime_error("locate requires a single 3-element vector.");
    std::array<double,4> ws;
    v.clear();
    w.clear(); // make sure w is back to zero-elements

    // verbose_update("Find which tetrahedra might contain the point ",x.to_string(0));
    // std::vector<size_t> tets_to_check = tetrahedraTree.all_containing_leaf_indexes(x);
    // verbose_update("The tree would have us search ",tets_to_check);
    // for (size_t tet_idx: tets_to_check) if (this->unsafe_might_contain(tet_idx, x)){

    for (size_t node: tetrahedraTrellis.nodes_to_search(x)) // returns a distance-sorted list of nodes which might have a containing leaf
    for (auto leaf: tetrahedraTrellis.node_leaves(node))
    if (this->unsafe_might_contain(leaf.index(), x)){
      this->weights(leaf.index(), x, ws);
      // if all weights are greater or equal to ~zero, we can use this tetrahedron
      if (std::all_of(ws.begin(), ws.end(), [](double z){return z>0. || brille::approx::scalar(z,0.);})){
        for (size_t i=0; i<4u; ++i) if (!brille::approx::scalar(ws[i], 0.)){
          v.push_back(vertices_per_tetrahedron[leaf.index(), i]);
          w.push_back(ws[i]);
        }
        return leaf.index();
      }
    }
    throw std::runtime_error("The containing tetrahedra was not found?!");
    return nTetrahedra;
  }
  // and a special version which doesn't return the weights
  size_t locate(const bArray<double>& x, std::vector<size_t>& v) const{
    std::vector<double> w;
    return this->locate(x, v, w);
  }

  // /* If the following function is more useful than the preceeding, it could be
  //    advantageous to replicate the above code in this function's for loop.
  //    Either way, this should probably be parallelised with OpenMP.
  // */
  // std::vector<std::vector<size_t>> locate_all_for_interpolation(const bArray<double>& x) const {
  //   if (x.numel()!=3u){
  //     std::string msg = "locate_all requires 3-element vector(s)";
  //     throw std::runtime_error(msg);
  //   }
  //   std::vector<std::vector<size_t>> all_idx(x.size());
  //   for (size_t i=0; i<x.size(); ++i)
  //     all_idx[i] = this->locate_for_interpolation(x.extract(i));
  //   return all_idx;
  // }

  /* Given a vertex in the mesh, return a vector of all of the other vertices to
     which it is connected.
  */
  std::vector<size_t> neighbours(const bArray<double>& x) const {
    std::vector<size_t> v;
    this->locate(x, v);
    if (v.size() != 1u){
      std::string msg = "The provided point is not a mesh vertex.";
      throw std::runtime_error(msg);
    }
    return this->neighbours(v[0]);
  }
  std::vector<size_t> neighbours(const size_t vert) const {
    if (vert >= this->nVertices){
      std::string msg = "The provided vertex index is out of bounds";
      throw std::out_of_range(msg);
    }
    std::vector<size_t> n;
    size_t v;
    for (size_t t: this->tetrahedra_per_vertex[vert])
    // for (size_t v: this->vertices_per_tetrahedron[t]) // would work if vertices_per_tetrahedron was a vector<array<size_t,4>>
    for (size_t j=0; j<4u; ++j){
      v = this->vertices_per_tetrahedron.val(t, j);
      if ( v!= vert && std::find(n.begin(), n.end(), v) == n.end() ) n.push_back(v);
    }
    return n;
  }
  double volume(const size_t tet) const {
    double v;
    const ind_t* i = vertices_per_tetrahedron.ptr(tet);
    v = orient3d(
      vertex_positions.ptr(i[0]), vertex_positions.ptr(i[1]),
      vertex_positions.ptr(i[2]), vertex_positions.ptr(i[3]))/6.0;return v;
  }
  bool might_contain(const size_t tet, const bArray<double>& x) const {
    if (x.ndim()!=2u || x.size(0)!=1u || x.size(1)!=3u)
      throw std::runtime_error("x must be a single 3-vector");
    if (tet >= nTetrahedra) return false;
    return this->unsafe_might_contain(tet, x);
  }
protected:
  bool unsafe_might_contain(const size_t tet, const bArray<double>& x) const {
    return leaves[tet].fuzzy_contains(x);
  }
  void weights(const size_t tet, const bArray<double>& x, std::array<double,4>& w) const {
    double vol6 = 6.0*this->volume(tet);
    const ind_t* i = vertices_per_tetrahedron.ptr(tet);
    const auto& p{vertex_positions};
    w[0] = orient3d(x.ptr(0),    p.ptr(i[1]), p.ptr(i[2]), p.ptr(i[3]) )/vol6;
    w[1] = orient3d(p.ptr(i[0]), x.ptr(0),    p.ptr(i[2]), p.ptr(i[3]) )/vol6;
    w[2] = orient3d(p.ptr(i[0]), p.ptr(i[1]), x.ptr(0),    p.ptr(i[3]) )/vol6;
    w[3] = orient3d(p.ptr(i[0]), p.ptr(i[1]), p.ptr(i[2]), x.ptr(0)    )/vol6;
  }
  void correct_tetrahedra_vertex_ordering(void){
    for (size_t i=0; i<nTetrahedra; ++i)
    if (std::signbit(this->volume(i))) // the volume of tetrahedra i is negative
    vertices_per_tetrahedron.swap(i, 0,1); // swap two vertices to switch sign
  }
  void make_balltree(const double fraction){
    // Construct a vector of BallLeaf objects for each tetrahedra:
    // std::vector<BallLeaf> leaves;
    leaves.clear();
    std::array<double,3> centre;
    double radius;
    verbose_update("Pull together the circumsphere information for all tetrahedra");
    tetgenmesh tgm; // to get access to circumsphere
    const auto& p{vertex_positions};
    for (size_t i=0; i<nTetrahedra; ++i){
      const ind_t* v = vertices_pertetrahedron.ptr(i);
      // use tetgen's circumsphere to find the centre and radius for each tetrahedra
      tgm.circumsphere(p.ptr(v[0]), p.ptr(v[1]), p.ptr(v[2]), p.ptr(v[3]), centre.data(), &radius);
      // leaves.push_back(BallLeaf(centre, radius, i));
      leaves.push_back(TrellisLeaf(centre, radius, i));
    }
    // // construct the full tree structure at once using an algorithm similar to
    // // Omohundro's Kd algorithm in 'Five Balltree Construction Algorithms'
    // if (nTetrahedra > 0){
    //   // we will construct a binary tree. If we have N leaves to place at the
    //   // end of the branches, they can occupy as many as N branches.
    //   // A tree with N final branches has log₂(N) levels, so we will never need
    //   // to exceed this.
    //   // If we reduce the number of branchings we increase the number of leaves
    //   // per terminal branch. There must be some trade-off between tree-granularity
    //   // and tree-size for how quickly a containing tetrahedra can be located.
    //   size_t maximum_branchings = static_cast<size_t>(std::log2(nTetrahedra))-2;
    //   maximum_branchings = 3;
    //   verbose_update("Construct a tree for tetrahedra location with up to ",maximum_branchings," branchings.");
    //   tetrahedraTree = construct_balltree(leaves, maximum_branchings);
    //   verbose_update("Tree for locating tetrahedra now exists");
    //   // info_update("Tetrahedra tree:\n",tetrahedraTree.to_string());
    // }
    tetrahedraTrellis = construct_trellis(leaves, fraction);
  }
};

template <class T>
TetTri
triangulate(
  const bArray<T>& verts,
  const std::vector<std::vector<int>>& vpf,
  const double max_cell_size=-1.0,
  const double min_dihedral=-1.0,
  const double max_dihedral=-1.0,
  const double radius_edge_ratio=-1.0,
  const int max_mesh_points=-1,
  const double fraction=1.0
) {
  assert(verts.ndim()==2 && verts.size(1)==3);// otherwise we can't make a 3-D triangulation
  // create the tetgenbehavior object which contains all options/switches for tetrahedralize
  verbose_update("Creating `tetgenbehavior` object");
  tetgenbehavior tgb;
  tgb.plc = 1; // we will always tetrahedralize a piecewise linear complex
  tgb.quality = 1; // we will (almost) always improve the tetrahedral mesh
  tgb.neighout = 1; // we *need* the neighbour information to be stored into tgo.
  if (max_cell_size > 0){
    // volume constraint with a specified size
    tgb.fixedvolume = 1;
    tgb.maxvolume = max_cell_size;
  } else{
    //volume constraint without a specified size?
    tgb.varvolume = 1;
  }
  if (max_mesh_points>0) tgb.steinerleft = max_mesh_points;
  if (radius_edge_ratio>0) tgb.minratio = radius_edge_ratio;
  if (min_dihedral>0) tgb.mindihedral = min_dihedral;
  if (max_dihedral>0) tgb.optmaxdihedral = max_dihedral;
  #ifndef VERBOSE_MESHING
  tgb.quiet = 1;
  #endif
  #ifdef VERBOSE_MESHING
  tgb.verbose = 10000;
  #endif

  // make the input and output tetgenio objects and fill the input with our polyhedron
  verbose_update("Creating input and output `tetgenio` objects");
  tetgenio tgi, tgo;
  // we have to handle initializing points/facets, but tetgenio has a destructor
  // which handles deleting all non-NULL fields.
  verbose_update("Initialize and fill the input object's pointlist parameter");
  tgi.numberofpoints = static_cast<int>(verts.size(0));
  tgi.pointlist = new double[3*tgi.numberofpoints];
  tgi.pointmarkerlist = new int[tgi.numberofpoints];
  //tgi.point2tetlist = new int[tgi.numberofpoints];
  int idx=0;
  for (size_t i=0; i<verts.size(0); ++i){
    tgi.pointmarkerlist[i] = static_cast<int>(i);
    for (size_t j=0; j<verts.size(1); ++j)
      tgi.pointlist[idx++] = verts.val(i,j);
  }
  verbose_update("Initialize and fill the input object's facetlist parameter");
  tgi.numberoffacets = static_cast<int>(vpf.size());
  tgi.facetlist = new tetgenio::facet[tgi.numberoffacets];
  tgi.facetmarkerlist = new int[tgi.numberoffacets];
  for (size_t i=0; i<vpf.size(); ++i){
    tgi.facetmarkerlist[i] = static_cast<int>(i);
    tgi.facetlist[i].numberofpolygons = 1;
    tgi.facetlist[i].polygonlist = new tetgenio::polygon[1];
    tgi.facetlist[i].polygonlist[0].numberofvertices = static_cast<int>(vpf[i].size());
    tgi.facetlist[i].polygonlist[0].vertexlist = new int[tgi.facetlist[i].polygonlist[0].numberofvertices];
    for (size_t j=0; j<vpf[i].size(); ++j)
      tgi.facetlist[i].polygonlist[0].vertexlist[j] = vpf[i][j];
  }
  // The input is now filled with the piecewise linear complex information.
  // so we can call tetrahedralize:
  verbose_update("Calling tetgen::tetrahedralize");
  try {
      tetrahedralize(&tgb, &tgi, &tgo);
  } catch (const std::logic_error& e) {
    std::string msg = "tetgen threw a logic error with message\n" + std::string(e.what());
    throw std::runtime_error(msg);
  } catch (const std::runtime_error& e) {
    std::string msg = "tetgen threw a runtime_error with message\n" + std::string(e.what());
    throw std::runtime_error(msg);
  } catch (...) {
    std::string msg = "tetgen threw an undetermined error";
    throw std::runtime_error(msg);
  }
  verbose_update("Constructing TetTri object");
  return TetTri(tgo, fraction);
}

} // end namespace brille
#endif // _TRIANGULATION_H_