Skip to content

Surface_mesh_shortest_path: a source point located exactly on a vertex is silently rooted at the wrong vertex #9646

Description

@jf---

Issue Details

Surface_mesh_shortest_path::locate() performs an inexact construction under
Exact_predicates_inexact_constructions_kernel, so for a query point that lies exactly on a mesh
vertex
it returns barycentric coordinates carrying round-off — typically one component at
-5.6e-17 instead of 0. Surface_mesh_shortest_paths_3::Classify_barycentric_coordinates then
tests those coordinates exactly, so the location is not classified as
BARYCENTRIC_COORDINATES_ON_VERTEX, and expand_root() roots the wavefront somewhere other than
the intended vertex.

The failure is silent — no exception, no assertion, a plausible-looking field — and the affected
vertex reports a distance to its own source point of exactly one edge length instead of zero.

Adding the same vertex via add_source_point(vertex_descriptor) is correct, because
face_location(v) produces exactly (0, 0, 1). So the two documented ways of seeding the same
geometric point disagree, and the composition used by the shortest_path_with_locate example is
the one that is wrong.

It is intermittent with respect to mesh size on identical topology, since it turns on the last bit
of the projection. In the program below, a negative component in the middle slot mis-roots;
n = 8 has a negative in the first slot and survives.

Source Code

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Surface_mesh.h>
#include <CGAL/Surface_mesh_shortest_path.h>
#include <CGAL/AABB_tree.h>
#include <CGAL/AABB_traits_3.h>
#include <CGAL/AABB_face_graph_triangle_primitive.h>
#include <iostream>
#include <iomanip>
#include <vector>

typedef CGAL::Exact_predicates_inexact_constructions_kernel   Kernel;
typedef CGAL::Surface_mesh<Kernel::Point_3>                   Mesh;
typedef CGAL::Surface_mesh_shortest_path_traits<Kernel, Mesh> Traits;
typedef CGAL::Surface_mesh_shortest_path<Traits>              ShortestPath;
typedef CGAL::AABB_face_graph_triangle_primitive<Mesh>        Primitive;
typedef CGAL::AABB_traits_3<Kernel, Primitive>                AABBTraits;
typedef CGAL::AABB_tree<AABBTraits>                           AABBTree;
typedef boost::graph_traits<Mesh>::vertex_descriptor          vertex_descriptor;

int main()
{
  for (int n : {4, 5, 6, 7, 8, 9})
  {
    // planar unit square, n x n vertices, two triangles per cell
    Mesh mesh;
    std::vector<Mesh::Vertex_index> vi;
    for (int i = 0; i < n; ++i)
      for (int j = 0; j < n; ++j)
        vi.push_back(mesh.add_vertex(Kernel::Point_3(double(i)/(n-1), double(j)/(n-1), 0.0)));
    for (int i = 0; i < n - 1; ++i)
      for (int j = 0; j < n - 1; ++j)
      {
        int a = i*n+j, b = i*n+j+1, c = (i+1)*n+j, d = (i+1)*n+j+1;
        mesh.add_face(vi[a], vi[b], vi[d]);
        mesh.add_face(vi[a], vi[d], vi[c]);
      }

    const vertex_descriptor vd(static_cast<std::size_t>(n - 1));   // corner (0, 1, 0)

    ShortestPath by_vertex(mesh);
    by_vertex.add_source_point(vd);
    const double d_vertex = by_vertex.shortest_distance_to_source_points(vd).first;

    ShortestPath by_point(mesh);
    AABBTree tree;
    by_point.build_aabb_tree(tree);
    const auto loc_v = by_vertex.face_location(vd);
    const auto loc_p = by_point.locate(mesh.point(vd), tree);   // the SAME point
    by_point.add_source_point(loc_p);
    const double d_point = by_point.shortest_distance_to_source_points(vd).first;

    std::cout << "n=" << n
              << "  d(vertex source)=" << std::setw(8) << d_vertex
              << "  d(point source)="  << std::setw(8) << d_point
              << (d_point > 1e-12 ? "   <-- WRONG" : "")
              << "\n    face_location bary = " << std::setprecision(17)
              << loc_v.second[0] << ", " << loc_v.second[1] << ", " << loc_v.second[2]
              << "\n    locate()      bary = "
              << loc_p.second[0] << ", " << loc_p.second[1] << ", " << loc_p.second[2]
              << std::setprecision(6) << "\n";
  }
  return 0;
}

Built header-only:

c++ -std=c++17 -O3 -DNDEBUG -DCGAL_HEADER_ONLY -DCGAL_DISABLE_GMP -DCGAL_USE_BOOST_MP \
    -I <cgal>/include -I <boost> -I <eigen> smsp_locate_bug.cpp -o smsp_locate_bug

Actual output

n=4  d(vertex source)=       0  d(point source)=0.333333   <-- WRONG
    face_location bary = 0, 0, 1
    locate()      bary = 0, -5.5511151231257821e-17, 1
n=5  d(vertex source)=       0  d(point source)=       0
    face_location bary = 0, 0, 1
    locate()      bary = 0, 0, 1
n=6  d(vertex source)=       0  d(point source)=     0.2   <-- WRONG
    face_location bary = 0, 0, 1
    locate()      bary = 0, -6.5919492087118657e-17, 1
n=7  d(vertex source)=       0  d(point source)=0.166667   <-- WRONG
    face_location bary = 0, 0, 1
    locate()      bary = 0, -5.5511151231257864e-17, 1
n=8  d(vertex source)=       0  d(point source)=1.38778e-17
    face_location bary = 0, 0, 1
    locate()      bary = -1.1102230246251565e-16, 6.09863722023096e-17, 1
n=9  d(vertex source)=       0  d(point source)=       0
    face_location bary = 0, 0, 1
    locate()      bary = 0, 0, 1

Expected: d(point source) == 0 in every case, since the source point is the queried vertex. The
wrong value is always exactly one edge length, 1/(n-1).

Suggested direction

The composition of an inexact construction with an exact classification is the root of it, so the
repair seems to belong between the two rather than in either alone — for instance normalising a
Face_location onto the simplex (clamping sub-ulp components to zero and renormalising) inside
add_source_point(const Face_location&), or having locate() return coordinates already snapped.
Barycentric coordinates are dimensionless and bounded in [0, 1], so such a threshold is
scale-free rather than a tolerance on a length.

As a downstream workaround we snap components below 8 * DBL_EPSILON to zero and renormalise before
calling add_source_point, which makes all six cases above return zero and leaves face-interior
sources untouched.

Environment

  • CGAL 6.1.1 (release tarball, header-only)
  • Boost 1.82.0, Eigen 3.4.0
  • Apple clang, macOS arm64, -O3 -DNDEBUG

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions