artem-ogre/CDT

A case that failed in triangulation

pan3rock opened this issue · 20 comments

I encountered some failed cases that said "Duplicate vertex detected: # is a duplicate of #". After checking it, I found it's because of the triangulation at the last iteration in my codes. There is a simple example below. It's kind of like #166. I don't know the reason, is it a bug or due to invalid points?

The vertices are

    0      0.000010000000      0.000010000000
    1      1.000000000000      0.000000000000
    2      0.999990000000      0.099990000000
    3      0.000000000000      0.100000000000
    4      0.500005000000      0.000005000000
    5      0.500000000000      0.050000000000
    6      0.499995000000      0.099995000000

The generated triangles are

    5    0    4
    0    5    3
    5    2    6
    2    3    6
    3    5    6
    2    5    1
    4    1    5

The triangle (2, 3, 6) is the red line, a flat triangle.
image

2 0.999990000000 0.099990000000
3 0.000000000000 0.100000000000
6 0.499995000000 0.099995000000

It's clear these points aren't collinear from their y values! you can round your input points to match with your need.

Yes, it is the duplicate. The points are not exactly on the same line. Exactly means that if you solve orient2d predicate: the determinant is exactly zero. It is easy to see that 0.09999 != 0.1 or 0.099995.

@artem-ogre @Islam0mar The line is slanted.. After checking the area or slope, the three points are collinear.

x1, y1 = 0.999990000000, 0.099990000000 # 2
x2, y2 = 0.000000000000, 0.100000000000 # 3
x3, y3 = 0.499995000000, 0.099995000000 # 6

def collinear(x1, y1, x2, y2, x3, y3):
    a = x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)
 
    if (a == 0):
        print("Yes")
    else:
        print("No")
collinear(x1, y1, x2, y2, x3, y3)

print((y1 - y2)/(x1 - x2) == (y2 - y3)/(x2 - x3))

The output is Yes.

This code is likely returning “Yes” because of the floating point rounding errors. This is exactly the problem that is solved by the robust geometric predicates (RGPs). CDT is using RGPs. There are a lot of very good RGP explanations that can be found online.

Thanks. I tried the RGPs provided by predicates. The value of orient2d of the three points is also 0. I'll check it again tomorrow and find if there is another mistake.

What about this one used in CDT?

template <typename T> T orient2d(T const ax, T const ay, T const bx, T const by, T const cx, T const cy);

I used the orient2d in CDT, the results are below:

    5    0    4     2.49973e-02
    0    5    3     4.99945e-02
    5    2    6     2.49973e-02
    2    3    6     6.93847e-18
    3    5    6     2.49973e-02
    2    5    1     4.99945e-02
    4    1    5     2.49973e-02

The value for (2, 3, 6) is 6.93847e-18. It may be a precision problem.

I print the orient2d in my codes when it's absolute value < 1.0e-5, it shows:

   29  114  132     0.00000e+00
  112   57   98     0.00000e+00
   51   92    0     0.00000e+00
  211  183  223     0.00000e+00
  145  127   52    -1.73472e-18
  195  236  222     0.00000e+00
  212  209  193     0.00000e+00
  119  155  100    -2.60209e-18
   95   79   77     0.00000e+00
iter_b:    13
   29  114  132     0.00000e+00
  183  223  211     0.00000e+00
   51   92    0     0.00000e+00
  212  209  193     0.00000e+00
  112   57   98     0.00000e+00
  245  244  263     3.46945e-18
  127   52  145    -1.73472e-18
  195  236  222     0.00000e+00
  119  155  100    -2.60209e-18
   95   79   77     0.00000e+00
iter_b:    14
  127   52  145    -1.73472e-18
  112   57   98     0.00000e+00
   51   92    0     0.00000e+00
  183  223  211     0.00000e+00
  212  209  193     0.00000e+00
  245  244  263     3.46945e-18
  195  236  222     0.00000e+00
  132   29  114    -1.73472e-18
  119  155  100    -2.60209e-18
   77   95   79     0.00000e+00

you will find value of some triangles are at the level of 1.0e-18 and some are zeros. Under my understanding, these triangles should be ignored when generating, but now they exist.

I also computed orient2d(p1, p2, p3), orient2d(p1, p3, p2), orient2d(p3, p2, p1) for those weird triangles
e.g.

   15    8    3     0.00000e+00
   15    8    3    -1.73472e-18
   15    8    3    -8.67362e-19

e.g.

   93  175  176     2.16840e-19
   93  175  176    -2.16840e-19
   93  175  176    -2.16840e-19

It says that those with values of zero will be at levels of <1.0e-18 if sequences are changed.

Are all of these results for adaptive::orient2d from predicates used in CDT?

Yes.

auto v1 = predicates::adaptive::orient2d(x1, y1, x2, y2, x3, y3);

In the last examples change of sign is due to changing the order of points (which changes winding). It however should not change from zero to negative in my understanding. What are the coordinates of points (15, 8, 3)?

Was your other claim that orient2d should return zero for (2,3,6) compared to another RGP implementation.

Another claim is that there are triangles in final triangulation for which adaptive::orient2d is zero?

I would appreciate the minimal reproducers. In that case I could take a closer look when I’m back at the keyboard.

A demo is provided. You can observe the change from zero to other values. Another RGP implementation is also included. In the first case, they are the same, but the second is different.

the double precision is 1e-16, is that 1.0e-18 should be regarded as zero? Like my job, the update of new vertices is from some splitted edges (x_new = (x_i + x_j) / 2.0), the location has an error of 1.0e-16, is it possible that's the reason to produce such problem?

git clone https://github.com/pan3rock/CDT-issue-172.git
cd CDT-issue-172
git submodule init
git submodule update
mkdir build
cd build
cmake ..
make -j4
../bin/demo1

my output:

(6, 4, 7):      2.16840e-19
(6, 7, 4):     -2.16840e-19
(7, 4, 6):     -2.16840e-19
another predicates:      2.16840e-19
another predicates:     -2.16840e-19
another predicates:     -2.16840e-19
(1, 2, 3):     -8.67362e-19
(1, 3, 2):      4.33681e-19
(3, 2, 1):      0.00000e+00
another predicates:     -4.33681e-19
another predicates:      4.33681e-19
another predicates:      4.33681e-19

Only exactly zero should be considered as zero. E.g., smallest representable positive double 2.225E-307 should be considered positive.

What happens if you change fmt::print to display the actual value of double?

you mean to print the double value with full precision?

fmt::print("({:d}, {:d}, {:d}): {}\n", v[0], v[1], v[2], v1);

output:

(6, 4, 7): 2.168404344971009e-19
(6, 7, 4): -2.168404344971009e-19
(7, 4, 6): -2.168404344971009e-19
another predicates: 2.168404344971009e-19
another predicates: -2.168404344971009e-19
another predicates: -2.168404344971009e-19
(1, 2, 3): -8.673617379884035e-19
(1, 3, 2): 4.336808689942018e-19
(3, 2, 1): 0
another predicates: -4.336808689942018e-19
another predicates: 4.336808689942018e-19
another predicates: 4.336808689942018e-19
(1, 2, 3): -8.673617379884035e-19
(1, 3, 2): 4.336808689942018e-19
(3, 2, 1): 0
another predicates: -4.336808689942018e-19
another predicates: 4.336808689942018e-19
another predicates: 4.336808689942018e-19

Doesn’t look right. I will check (1,2,3)-case next week.

Can't reproduce on gcc and clang:
https://godbolt.org/z/s5MqjrTvf

It turns out that predicate breaks when -Ofast flag is used, works fine with -O3
https://godbolt.org/z/Kx9xrcETs

from Ofast docs:

Disregard strict standards compliance. -Ofast enables all -O3 optimizations. It also enables optimizations that are not valid for all standard-compliant programs. It turns on -ffast-math, -fallow-store-data-races and the Fortran-specific -fstack-arrays, unless -fmax-stack-var-size is specified, and -fno-protect-parens. It turns off -fsemantic-interposition.

ffast-math is not supported by robust predicates.

For the (2, 3, 6) case both predicates agree that points are collinear with O3.
My guess is that if Ofast flag is removed, the triangulation will not contain flat triangles.

Great, you're right. It's Ofast. Thanks a lot.