Быстрый способ получить ближайшую точку из каждого квадранта

Я хочу быстро (скажем, ‹1 мс) выполнить поиск ближайшего соседа по квадранту.

Ввод каждого поиска:

  • фиксированный набор точек в 2D-пространстве (черные точки на изображении). Это остается постоянным при поиске, поэтому его можно сохранить в эффективной структуре данных. Количество баллов от 1 до 1000.
  • точка, расположение которой различается для каждого поиска (красная точка на изображении). Абстрактно это делит 2D-пространство на 4 квадранта (разделенных красными линиями на изображении), очень похоже на начало координат в декартовых координатах.

Результат каждого поиска:

  • черная точка из каждого красного квадранта, ближайшая (обведена синим на изображении) к красной точке.

Два поиска ближайших соседей на квадрант

На выходе обычно должно быть 4 точки, по одной из каждого квадранта. Но есть несколько возможных крайних случаев:

  • квадрант, чтобы не было черных точек; не набрать очков за этот квадрант
  • квадрант имеет несколько связей; собрать все такие точки для этого квадранта
  • одна и та же точка лежит на границах (красные линии) квадрантов: не собирайте эту точку более одного раза

Вещи, которые я знаю, не будут работать:

  • ближайшая точка только по одной оси, скажем, x, может быть очень далеко (обведена зеленым на изображении)
  • вторая ближайшая точка в квадранте (обведена фиолетовым на изображении) может быть ближе, чем точки в других квадрантах. Простой поиск 4 ближайших соседей красной точки найдет эту точку, которую я не хочу.

РЕДАКТИРОВАТЬ: версия принятого кода ответа и тайминги

def trial1(quadneighbori,
           printout = False, seedN = None, Npts = 1000, Niter = 1000):

    if seedN != None: np.random.seed(seedN) # random seed

    # Generate Npts (x,y) coordinates where x and y are standard normal
    dataset = np.random.randn(Npts,2)

    for n in range(Niter):
        # Generate random pixel (x,y) coordinates where x and y are standard normal
        red = np.random.randn(1,2)
        dst, i = quadneighbori(dataset, red)
        if printout: print(dst, i)

def quadneighbor1(dataset, red):
    dst = np.zeros(4)
    closest = np.zeros((4,2))

    # Work out a Boolean mask for the 4 quadrants
    right_exclu = dataset[:,0] > red[0,0]
    top_exclu =   dataset[:,1] > red[0,1]
    Q1 = np.logical_and( top_exclu, right_exclu)
    Q2 = np.logical_and(~top_exclu, right_exclu)
    Q3 = np.logical_and(~top_exclu,~right_exclu)
    Q4 = np.logical_and( top_exclu,~right_exclu)
    Qs = [Q1, Q2, Q3, Q4]

    for Qi in range(4):
        # Boolean mask to select points in this quadrant
        thisQuad = dataset[Qs[Qi]]
        if len(thisQuad)==0: continue # if no points, move on to next quadrant
        # Calculate distance of each point in dataset to red point
        distances = cdist(thisQuad, red, 'sqeuclidean')
        # Choose nearest
        idx = np.argmin(distances)
        dst[Qi] = distances[idx]
        closest[Qi] = thisQuad[idx]

    return dst, closest

# numba turns 1.53s trial1 to 4.12ms trial1
@nb.jit(nopython=True, fastmath=True)
def quadneighbor2(dataset, red):
   redX, redY = red[0,0], red[0,1]
   # Distance to and index of nearest point in each quadrant
   dst = np.zeros(4) + 1.0e308           # Start large! Update with something smaller later
   idx = np.zeros(4, dtype=np.uint32)
   for i in nb.prange(dataset.shape[0]):
      # Get this point's x,y
      x, y = dataset[i,0], dataset[i,1]
      # Get quadrant of this point (minus 1)
      if x>redX:
         if y>redY:
             Qi = 0
         else:
             Qi = 1
      else:
         if y>redY:
             Qi = 3
         else:
             Qi = 2

      # Get distance (squared) of this point - square root is slow and unnecessary
      d = (x-redX)*(x-redX) + (y-redY)*(y-redY)
      # Update if nearest
      if d<dst[Qi]:
         dst[Qi] = d
         idx[Qi] = i

   return dst, idx
%timeit trial1(quadneighbor1)
111 ms ± 3.21 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

%timeit trial1(quadneighbor2)
4.12 ms ± 79.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

PS: cdist несовместимо с numba, но изменение строки для удаления cdist и добавления @nb.jit к quadneighbor1 ускоряет trial1(quadneighbor1) со 111 мс до 30,1 мс.


person BatWannaBe    schedule 20.04.2021    source источник
comment
Можете ли вы сгруппировать свои точки в соответствии с тем, к каким квадрантам они принадлежат?   -  person auburg    schedule 20.04.2021
comment
@auburg - я думаю, я могу сделать это грубой силой, просто назначив каждую точку данных 4 квадрантам, определенным произвольной точкой (или я мог бы просто сохранить ближайшую точку данных для каждого квадранта). Но 4 квадранта меняются в зависимости от произвольной точки, так что это придется вычислять снова и снова. Поэтому я ищу менее расточительные альтернативы грубой силе.   -  person BatWannaBe    schedule 20.04.2021
comment
Исходное положение квадранта (красная точка) также фиксировано или каждый запрос может иметь другую красную точку?   -  person Berthur    schedule 20.04.2021
comment
@Berthur Красная точка переменная, черные точки фиксированы   -  person BatWannaBe    schedule 20.04.2021
comment
Пожалуйста, сколько всего (черных) точек? И какую скорость вы ищете?   -  person Mark Setchell    schedule 21.04.2021
comment
@MarkSetchell Количество и расположение точек данных (черных точек) неизвестны, но постоянны.   -  person BatWannaBe    schedule 21.04.2021
comment
Так что где-то между 7 и 7 000 000 000 является репрезентативным? Или шире этого?   -  person Mark Setchell    schedule 21.04.2021
comment
Я не ожидал, что размер набора данных будет иметь значение, но для моих целей да, он будет в пределах ‹1000. Что касается скорости, я хочу выполнять этот поиск сотни раз в секунду.   -  person BatWannaBe    schedule 21.04.2021
comment
Можно ли выполнить этап предварительной обработки, на котором (как вы упомянули в своем ответе на мой первоначальный комментарий) вы выполняете расчет для каждой точки данных для каждого заданного квадранта и сохраняете эту информацию в виде списка для каждой точки данных. Таким образом, он вычисляется один раз для каждой точки данных, и его не нужно делать повторно (поскольку точки данных никогда не меняются).   -  person auburg    schedule 21.04.2021
comment
Рассчитать, что именно заранее? Красная точка меняется при каждом поиске, поэтому расстояния черных точек данных до этой красной точки и квадрантов, разделяющих черные точки данных, также изменяются при каждом поиске.   -  person BatWannaBe    schedule 21.04.2021


Ответы (1)


Обновленный ответ

Я изменил исходный ответ, чтобы он работал под numba. Теперь он обрабатывает набор данных из 1000 точек за 3 микросекунды, что значительно быстрее по сравнению с исходной 1 миллисекундой или около того.

#!/usr/bin/env python3

import random, sys
import numpy as np
import numba as nb

@nb.jit(nopython=True, fastmath=True)
def QuadNearestNeighbour(dataset,redX,redY):
   # Distance to and index of nearest point in each quadrant
   dst = np.zeros(4) + 1.0e308           # Start large! Update with something smaller later
   idx = np.zeros(4, dtype=np.uint32)
   for i in nb.prange(dataset.shape[0]):
      # Get this point's x,y
      x, y = dataset[i,0], dataset[i,1]
      # Get quadrant of this point (minus 1)
      if x>redX:
         if y>redY:
             Q = 0
         else:
             Q = 1
      else:
         if y>redY:
             Q = 3
         else:
             Q = 2

      # Get distance (squared) of this point - square root is slow and unnecessary
      d = (x-redX)*(x-redX) + (y-redY)*(y-redY)
      # Update if nearest
      if d<dst[Q]:
         dst[Q] = d
         idx[Q] = i

   return  dst, idx

# Number of points
Npts = 1000

# Generate the Npts random X,Y points
dataset = np.random.rand(Npts,2)

# Generate random red pixel (X,Y)
redX, redY = random.random(), random.random()

res = QuadNearestNeighbour(dataset,redX,redY)
print(res)

Исходный ответ

Это выполняется менее чем за 1 мс для 1000 точек, рассчитанных на более чем 1000 различных местоположений для красной точки.

Это создаст один набор данных с 1000 точек, а затем создаст 1000 красных центров и сделает 4 квадранта для каждого из центров. На моем MacBook он выполняется менее чем за секунду, поэтому 1 мс на красный центр.

Я уверен, что его можно было бы оптимизировать, убрав лишние операторы print, не разрабатывая вещи, которые использовались только для иллюстрации, возможно, используя numba, но этого достаточно для одного дня.

Я не добавил много проверок ошибок или пограничных случаев, это не качественный код для производства.

#!/usr/bin/env python3

import random
import numpy as np
from scipy.spatial.distance import cdist

# Let's get some repeatable randomness
np.random.seed(42)

# Number of points, number of iterations
Npts, Niter = 1000, 1000

# Generate the Npts random X,Y points
dataset = np.random.rand(Npts,2)

# Run lots of iterations for better timing
for n in range(Niter):

   # Generate random red pixel (X,Y)
   red = np.random.rand(1,2)

   # Work out a Boolean mask for the 4 quadrants
   above = dataset[:,0]>red[0,0]
   right = dataset[:,1]>red[0,1]
   Q1 = np.logical_and(above,right)       # top-right
   Q2 = np.logical_and(above,~right)      # top-left
   Q3 = np.logical_and(~above,~right)     # bottom-left
   Q4 = np.logical_and(~above,right)      # bottom-right

   Q = 1
   for quadrant in [Q1,Q2,Q3,Q4]:
       print(f'Quadrant {Q}: ')
       # Boolean mask to select points in this quadrant
       thisQuad = dataset[quadrant]
       l = len(thisQuad)
       if l==0:
           print('No points in quadrant')
           continue
       # print(f'nPoints in quadrant: {l}')
       # print(f'Points: {dataset[quadrant]}')
       # Calculate distance of each point in dataset to red point
       distances = cdist(thisQuad, red, 'sqeuclidean')
       # Choose nearest
       idx = np.argmin(distances)
       print(f'Index: {idx}, point: {thisQuad[idx]}, distance: {distances[idx]}')
       Q += 1
person Mark Setchell    schedule 21.04.2021
comment
Я ожидал какой-то умной древовидной структуры, которую они будут использовать в видеоиграх, но эй, если работает грубая сила, она работает. Я проверю это, когда доберусь до своего ноутбука. - person BatWannaBe; 21.04.2021
comment
Похоже, что ваша версия numba сделала 1 красную точку, в то время как ваша оригинальная версия сделала 1000. Я внес некоторые изменения, чтобы сделать сравнение более точным, и опубликовал тайминги. Я очень впечатлен производительностью Numba с невекторизованным кодом; Я должен изучить это. - person BatWannaBe; 25.04.2021