Deltares / numba_celltree

Celltree data structure for searching for points, lines, boxes, and cells (convex polygons) in a two dimensional unstructured mesh.
https://deltares.github.io/numba_celltree/
MIT License
11 stars 3 forks source link

locate_points fails for some topologies? #2

Closed Huite closed 1 year ago

Huite commented 1 year ago

The voronoi tesselation of this grid:

import numpy as np
import xugrid as xu
from numba_celltree import CellTree2d

xy = np.asarray([
    [-0.101, 0.872], [-0.080, 0.883], [-0.069, 0.888], [-0.054, 0.890],
    [-0.045, 0.897], [-0.057, 0.895], [-0.073, 0.900], [-0.087, 0.898],
    [-0.090, 0.904], [-0.069, 0.907], [-0.069, 0.921], [-0.080, 0.919],
    [-0.073, 0.928], [-0.052, 0.930], [-0.048, 0.942], [-0.062, 0.949],
    [-0.054, 0.958], [-0.069, 0.954], [-0.087, 0.952], [-0.087, 0.959],
    [-0.080, 0.966], [-0.085, 0.973], [-0.087, 0.965], [-0.097, 0.965],
    [-0.097, 0.975], [-0.092, 0.984], [-0.101, 0.980], [-0.108, 0.980],
    [-0.104, 0.987], [-0.102, 0.993], [-0.115, 1.001], [-0.099, 0.996],
    [-0.101, 1.007], [-0.090, 1.010], [-0.087, 1.021], [-0.069, 1.021],
    [-0.052, 1.022], [-0.052, 1.017], [-0.069, 1.010], [-0.064, 1.005],
    [-0.048, 1.005], [-0.031, 1.005], [-0.031, 0.996], [-0.040, 0.987],
    [-0.045, 0.980], [-0.052, 0.975], [-0.040, 0.973], [-0.026, 0.968],
    [-0.020, 0.954], [-0.006, 0.947], [ 0.003, 0.935], [ 0.006, 0.926],
    [ 0.005, 0.921], [ 0.022, 0.923], [ 0.033, 0.912], [ 0.029, 0.905],
    [ 0.017, 0.900], [ 0.012, 0.895], [ 0.027, 0.893], [ 0.019, 0.886],
    [ 0.001, 0.883], [-0.012, 0.884], [-0.029, 0.883], [-0.038, 0.879],
    [-0.057, 0.881], [-0.062, 0.876], [-0.078, 0.876], [-0.087, 0.872],
    [-0.030, 0.907], [-0.007, 0.905], [-0.057, 0.916], [-0.025, 0.933],
    [-0.077, 0.990], [-0.059, 0.993]])
x = np.degrees(xy[:, 0])
y = np.degrees(xy[:, 1])
x0 = -5
y0 = 52
z = np.exp(-0.01 * ((x - x0) ** 2 + (y - y0) ** 2))

triangles = np.asarray([
    [67, 66,  1], [65,  2, 66], [ 1, 66,  2], [64,  2, 65], [63,  3, 64],
    [60, 59, 57], [ 2, 64,  3], [ 3, 63,  4], [ 0, 67,  1], [62,  4, 63],
    [57, 59, 56], [59, 58, 56], [61, 60, 69], [57, 69, 60], [ 4, 62, 68],
    [ 6,  5,  9], [61, 68, 62], [69, 68, 61], [ 9,  5, 70], [ 6,  8,  7],
    [ 4, 70,  5], [ 8,  6,  9], [56, 69, 57], [69, 56, 52], [70, 10,  9],
    [54, 53, 55], [56, 55, 53], [68, 70,  4], [52, 56, 53], [11, 10, 12],
    [69, 71, 68], [68, 13, 70], [10, 70, 13], [51, 50, 52], [13, 68, 71],
    [52, 71, 69], [12, 10, 13], [71, 52, 50], [71, 14, 13], [50, 49, 71],
    [49, 48, 71], [14, 16, 15], [14, 71, 48], [17, 19, 18], [17, 20, 19],
    [48, 16, 14], [48, 47, 16], [47, 46, 16], [16, 46, 45], [23, 22, 24],
    [21, 24, 22], [17, 16, 45], [20, 17, 45], [21, 25, 24], [27, 26, 28],
    [20, 72, 21], [25, 21, 72], [45, 72, 20], [25, 28, 26], [44, 73, 45],
    [72, 45, 73], [28, 25, 29], [29, 25, 31], [43, 73, 44], [73, 43, 40],
    [72, 73, 39], [72, 31, 25], [42, 40, 43], [31, 30, 29], [39, 73, 40],
    [42, 41, 40], [72, 33, 31], [32, 31, 33], [39, 38, 72], [33, 72, 38],
    [33, 38, 34], [37, 35, 38], [34, 38, 35], [35, 37, 36]])

Produced with:

grid = xu.Ugrid2d(*xy.T, -1, triangles)
vertices, faces, node_to_face_index = xu.voronoi.voronoi_topology(
    grid.node_face_connectivity,
    grid.node_coordinates,
    grid.centroids,
    edge_face_connectivity=grid.edge_face_connectivity,
    edge_node_connectivity=grid.edge_node_connectivity,
    fill_value=grid.fill_value,
    add_exterior=True,
    add_vertices=False,
)
voronoi_grid = xu.Ugrid2d(
    vertices[:, 0],
    vertices[:, 1],
    -1,
    faces,
)

Or statically:

def voronoi_grid():
    vertices = np.array(
        [
            [-0.08166667, 0.877],
            [-0.06966667, 0.88],
            [-0.07566667, 0.88233333],
            [-0.06266667, 0.88166667],
            [-0.04966667, 0.88333333],
            [0.01066667, 0.888],
            [-0.06, 0.88633333],
            [-0.04566667, 0.88866667],
            [-0.08933333, 0.87566667],
            [-0.03733333, 0.88633333],
            [0.016, 0.89366667],
            [0.021, 0.893],
            [-0.006, 0.89066667],
            [0.002, 0.89433333],
            [-0.03466667, 0.89566667],
            [-0.06633333, 0.90066667],
            [-0.02366667, 0.89133333],
            [-0.01633333, 0.89866667],
            [-0.061, 0.906],
            [-0.08333333, 0.90066667],
            [-0.053, 0.90266667],
            [-0.07733333, 0.90366667],
            [0.00733333, 0.9],
            [0.005, 0.90866667],
            [-0.065, 0.91466667],
            [0.028, 0.91333333],
            [0.02266667, 0.90933333],
            [-0.044, 0.90666667],
            [0.01466667, 0.91466667],
            [-0.074, 0.92266667],
            [-0.02066667, 0.915],
            [-0.04633333, 0.91766667],
            [-0.05933333, 0.92233333],
            [0.00466667, 0.92733333],
            [-0.03566667, 0.92333333],
            [-0.009, 0.91966667],
            [-0.06466667, 0.92633333],
            [-0.00566667, 0.92966667],
            [-0.04166667, 0.935],
            [-0.00933333, 0.93833333],
            [-0.017, 0.94466667],
            [-0.05466667, 0.94966667],
            [-0.031, 0.943],
            [-0.081, 0.955],
            [-0.07866667, 0.95966667],
            [-0.04066667, 0.95133333],
            [-0.03333333, 0.96],
            [-0.04, 0.96633333],
            [-0.04866667, 0.96866667],
            [-0.09366667, 0.96833333],
            [-0.08966667, 0.971],
            [-0.05833333, 0.96233333],
            [-0.067, 0.965],
            [-0.09133333, 0.97733333],
            [-0.10433333, 0.98233333],
            [-0.08066667, 0.97633333],
            [-0.08466667, 0.98233333],
            [-0.06966667, 0.977],
            [-0.099, 0.98366667],
            [-0.052, 0.98266667],
            [-0.06266667, 0.986],
            [-0.09933333, 0.988],
            [-0.09766667, 0.991],
            [-0.048, 0.98666667],
            [-0.049, 0.995],
            [-0.06666667, 0.996],
            [-0.08933333, 0.99],
            [-0.03966667, 0.996],
            [-0.10533333, 0.99666667],
            [-0.057, 1.001],
            [-0.03666667, 1.002],
            [-0.08866667, 0.99866667],
            [-0.09666667, 1.00433333],
            [-0.07, 1.00166667],
            [-0.07866667, 1.00333333],
            [-0.082, 1.01366667],
            [-0.06333333, 1.016],
            [-0.075, 1.01733333],
            [-0.05766667, 1.02],
            [-0.09033808, 0.87758482],
            [-0.08933333, 0.872],
            [-0.07665982, 0.88451826],
            [-0.06037555, 0.88914993],
            [-0.04945385, 0.8935359],
            [-0.05186486, 0.89585586],
            [-0.06711625, 0.89816133],
            [-0.08303333, 0.89856667],
            [-0.08733333, 0.89866667],
            [-0.07763333, 0.90576667],
            [-0.069, 0.91466667],
            [-0.07354667, 0.92017333],
            [-0.07596154, 0.92419231],
            [-0.06489888, 0.92877154],
            [-0.04946667, 0.9376],
            [-0.0564, 0.9462],
            [-0.05843218, 0.95301379],
            [-0.0569668, 0.95720885],
            [-0.0807439, 0.95269512],
            [-0.087, 0.955],
            [-0.0825, 0.9635],
            [-0.08511261, 0.97315766],
            [-0.0857451, 0.97001961],
            [-0.09366667, 0.965],
            [-0.097, 0.96833333],
            [-0.09467296, 0.97918868],
            [-0.09796907, 0.98134708],
            [-0.10433333, 0.98],
            [-0.10609231, 0.98333846],
            [-0.10323333, 0.9893],
            [-0.10605436, 0.99549499],
            [-0.10495967, 0.9978624],
            [-0.100392, 1.003656],
            [-0.09764359, 1.00791538],
            [-0.08851538, 1.01544359],
            [-0.075, 1.021],
            [-0.05776437, 1.02166092],
            [-0.052, 1.02],
            [-0.06204241, 1.01286489],
            [-0.06533333, 1.00633333],
            [-0.057, 1.005],
            [-0.03666667, 1.005],
            [-0.031, 1.002],
            [-0.03533333, 0.99166667],
            [-0.04286036, 0.9829955],
            [-0.04837387, 0.97759009],
            [-0.04772973, 0.97428829],
            [-0.03788839, 0.97224585],
            [-0.02424138, 0.96389655],
            [-0.01386667, 0.95093333],
            [-0.00304, 0.94305333],
            [0.00546667, 0.9276],
            [0.00620513, 0.92702564],
            [0.01379977, 0.92203527],
            [0.02983333, 0.91516667],
            [0.03234359, 0.91085128],
            [0.02514201, 0.9033925],
            [0.02297315, 0.89581879],
            [0.02360177, 0.89002655],
            [0.01121622, 0.8847027],
            [-0.0065451, 0.88358039],
            [-0.02319655, 0.88334138],
            [-0.03472165, 0.88045704],
            [-0.04998995, 0.8802621],
            [-0.0595, 0.8785],
            [-0.06966667, 0.876],
            [-0.08069072, 0.87480412],
        ]
    )

    faces = np.array(
        [
            [13, 5, 10, 22, -1, -1, -1, -1, -1],
            [27, 14, 16, 17, 30, 34, 31, -1, -1],
            [17, 12, 13, 22, 23, 35, 30, -1, -1],
            [24, 18, 20, 27, 31, 32, -1, -1, -1],
            [34, 30, 35, 37, 39, 40, 42, 38, -1],
            [66, 56, 55, 57, 60, 65, 73, 74, 71],
            [60, 59, 63, 64, 69, 65, -1, -1, -1],
            [80, 8, 79, -1, -1, -1, -1, -1, -1],
            [79, 8, 0, 2, 81, -1, -1, -1, -1],
            [2, 1, 3, 6, 82, 81, -1, -1, -1],
            [6, 4, 7, 83, 82, -1, -1, -1, -1],
            [83, 7, 9, 14, 27, 20, 84, -1, -1],
            [15, 85, 84, 20, 18, -1, -1, -1, -1],
            [86, 85, 15, 21, 19, -1, -1, -1, -1],
            [87, 86, 19, -1, -1, -1, -1, -1, -1],
            [87, 19, 21, 88, -1, -1, -1, -1, -1],
            [88, 21, 15, 18, 24, 89, -1, -1, -1],
            [89, 24, 32, 36, 29, 90, -1, -1, -1],
            [90, 29, 91, -1, -1, -1, -1, -1, -1],
            [91, 29, 36, 92, -1, -1, -1, -1, -1],
            [36, 32, 31, 34, 38, 93, 92, -1, -1],
            [93, 38, 42, 45, 41, 94, -1, -1, -1],
            [94, 41, 95, -1, -1, -1, -1, -1, -1],
            [96, 95, 41, 45, 46, 47, 48, 51, -1],
            [43, 97, 96, 51, 52, 44, -1, -1, -1],
            [97, 43, 98, -1, -1, -1, -1, -1, -1],
            [98, 43, 44, 99, -1, -1, -1, -1, -1],
            [99, 44, 52, 57, 55, 100, -1, -1, -1],
            [50, 101, 100, 55, 56, 53, -1, -1, -1],
            [49, 102, 101, 50, -1, -1, -1, -1, -1],
            [102, 49, 103, -1, -1, -1, -1, -1, -1],
            [103, 49, 50, 53, 104, -1, -1, -1, -1],
            [58, 105, 104, 53, 56, 66, 62, 61, -1],
            [106, 105, 58, 54, -1, -1, -1, -1, -1],
            [106, 54, 107, -1, -1, -1, -1, -1, -1],
            [107, 54, 58, 61, 108, -1, -1, -1, -1],
            [108, 61, 62, 68, 109, -1, -1, -1, -1],
            [109, 68, 110, -1, -1, -1, -1, -1, -1],
            [68, 62, 66, 71, 72, 111, 110, -1, -1],
            [111, 72, 112, -1, -1, -1, -1, -1, -1],
            [72, 71, 74, 75, 113, 112, -1, -1, -1],
            [113, 75, 77, 114, -1, -1, -1, -1, -1],
            [77, 76, 78, 115, 114, -1, -1, -1, -1],
            [78, 116, 115, -1, -1, -1, -1, -1, -1],
            [76, 117, 116, 78, -1, -1, -1, -1, -1],
            [74, 73, 118, 117, 76, 77, 75, -1, -1],
            [73, 65, 69, 119, 118, -1, -1, -1, -1],
            [64, 67, 70, 120, 119, 69, -1, -1, -1],
            [70, 121, 120, -1, -1, -1, -1, -1, -1],
            [67, 122, 121, 70, -1, -1, -1, -1, -1],
            [63, 123, 122, 67, 64, -1, -1, -1, -1],
            [124, 123, 63, 59, -1, -1, -1, -1, -1],
            [52, 51, 48, 125, 124, 59, 60, 57, -1],
            [48, 47, 126, 125, -1, -1, -1, -1, -1],
            [46, 127, 126, 47, -1, -1, -1, -1, -1],
            [45, 42, 40, 128, 127, 46, -1, -1, -1],
            [39, 129, 128, 40, -1, -1, -1, -1, -1],
            [37, 33, 130, 129, 39, -1, -1, -1, -1],
            [131, 130, 33, -1, -1, -1, -1, -1, -1],
            [35, 23, 28, 132, 131, 33, 37, -1, -1],
            [28, 26, 25, 133, 132, -1, -1, -1, -1],
            [134, 133, 25, -1, -1, -1, -1, -1, -1],
            [135, 134, 25, 26, -1, -1, -1, -1, -1],
            [22, 10, 11, 136, 135, 26, 28, 23, -1],
            [137, 136, 11, -1, -1, -1, -1, -1, -1],
            [5, 138, 137, 11, 10, -1, -1, -1, -1],
            [139, 138, 5, 13, 12, -1, -1, -1, -1],
            [140, 139, 12, 17, 16, -1, -1, -1, -1],
            [9, 141, 140, 16, 14, -1, -1, -1, -1],
            [4, 142, 141, 9, 7, -1, -1, -1, -1],
            [3, 143, 142, 4, 6, -1, -1, -1, -1],
            [144, 143, 3, 1, -1, -1, -1, -1, -1],
            [0, 145, 144, 1, 2, -1, -1, -1, -1],
            [80, 145, 0, 8, -1, -1, -1, -1, -1],
        ]
    )
    return vertices, faces

This produces an incorrect value of -1 for:

vertices, faces = voronoi_grid()
tree = CellTree2d(vertices, faces, -1)
tree.locate_points([[-0.02, 0.90]])

While it is clearly located in the grid.

Triangulating the grid seems to resolve the issue, but it raises the question why it fails. From walking through the tree interactively with the debugger, it takes a wrong left/right decision and ends up in a wrong leaf node. The point_in_polygon algorithms does give the correct result, but it's never reached.

Huite commented 1 year ago

I have a feeling the issue is found in the construction.

Maybe something with special case here: https://github.com/Deltares/numba_celltree/blob/main/numba_celltree/creation.py#L309

Or the merging thereafter.

Huite commented 1 year ago

The problem is indeed a small error in the construction.

See the visual example from the paper (here with 5 buckets):

image

Faulty code:

        # plane is the separation line to split on:
        # 0 [bucket0] 1 [bucket1] 2 [bucket2] 3 [bucket3]
        plane = split_plane(buckets, root, range_Lmax, range_Rmin, bucket_length)

        right_index = buckets[plane].index
        right_size = root.ptr + root.size - right_index
        left_index = root.ptr
        left_size = root.size - right_size
        nodes[root_index]["Lmax"] = buckets[plane - 1].Lmax
        nodes[root_index]["Rmin"] = buckets[plane].Rmin

The error lies in the last two lines. It assumes that the left and right boundaries of the bounding boxes contained in the buckets (Lmax, Rmin) are monotonic with the buckets. This is not guaranteed: the bounding boxes are placed in the buckets based on centroid of the bounding box. When the grid contains both small and large cells (and thus bounding boxes), it's possible that the leftmost bucket contains an Lmax that is to the left of the second bucket.

That is exactly what happens when n_buckets=4, in this case for node 71:

Bucket(Max=-0.0307499975, Min=-0.04633333, Rmin=-0.04633333, Lmax=-0.01633333, index=53, size=1)
Bucket(Max=-0.015166665, Min=-0.0307499975, Rmin=-0.03733333, Lmax=-0.02319655, index=54, size=1)
Bucket(Max=0.00041666750000000224, Min=-0.015166665, Rmin=-0.02366667, Lmax=0.00733333, index=55, size=2)
Bucket(Max=0.016, Min=0.00041666750000000224, Rmin=-0.0065451, Lmax=0.016, index=57, size=2)

The second Lmax (-0.023) lies left of the first Lmax (-0.016). As the splitting plane is 2, the implementation above picks the value of -0.023 while -0.016 should be used instead.

Visually:

image

The bounding box of 1 is right of the bounding box of 68. But in terms of centroids, 1 is left of 68.

This is easily resolved by taking the maximum value of Lmax left of the splitting plane, and the minimum value of Rmin right of the splitting plane:

def split_plane(
    buckets: List[Bucket],
    root: np.void,
    range_Lmax: float,
    range_Rmin: float,
    bucket_length: float,
):
    plane_min_cost = FLOAT_MAX
    plane = INT_MAX
    bbs_in_left = 0
    bbs_in_right = 0

    # if we split here, lmax is from bucket 0, and rmin is from bucket 1 after
    # computing those, we can compute the cost to split here, and if this is the
    # minimum, we split here.
    for i in range(1, len(buckets)):
        current_bucket = buckets[i - 1]
        next_bucket = buckets[i]
        bbs_in_left += current_bucket.size
        bbs_in_right = root.size - bbs_in_left
        left_volume = (current_bucket.Lmax - range_Rmin) / bucket_length
        right_volume = (range_Lmax - next_bucket.Rmin) / bucket_length
        plane_cost = left_volume * bbs_in_left + right_volume * bbs_in_right
        if plane_cost < plane_min_cost:
            plane_min_cost = plane_cost
            plane = i

    Lmax = FLOAT_MIN
    Rmin = FLOAT_MAX
    for i in range(plane):
        bLmax = buckets[i].Lmax
        if bLmax > Lmax:
            Lmax = bLmax
    for i in range(plane, len(buckets)):
        bRmin = buckets[i].Rmin
        if bRmin < Rmin:
            Rmin = bRmin

    return plane, Lmax, Rmin
Huite commented 1 year ago

See also VTK implementation, which does the same:

https://github.com/Kitware/VTK/blob/d706250a1422ae1e7ece0fa09a510186769a5fec/Common/DataModel/vtkCellTreeLocator.cxx#L567-L568