I've run into an extremely weird issue, I don't know the cause but I can reliably reproduce it.
In the case where I create a qtree with certain bounds and I add multiple box that is partially out of bounds. The first 10 seem to add just fine, but the 11th hangs and memory usage continues to grow.
I've verified that this does not happen with all out of bounds boxes. It seems like there are only particular combinations of the entire box is out of bounds, or if both the top-left x and y are partially in bounds. I ended up writing a script to test a bunch of cases:
import ubelt as ub
# Test multiple cases
def basis_product(basis):
"""
Args:
basis (Dict[str, List[T]]): list of values for each axes
Yields:
Dict[str, T] - points in the grid
"""
import itertools as it
keys = list(basis.keys())
for vals in it.product(*basis.values()):
kw = ub.dzip(keys, vals)
yield kw
height, width = 600, 600
# offsets = [-100, -50, 0, 50, 100]
offsets = [-100, -10, 0, 10, 100]
# offsets = [-100, 0, 100]
x_edges = [0, width]
y_edges = [0, height]
# x_edges = [width]
# y_edges = [height]
basis = {
'tl_x': [e + p for p in offsets for e in x_edges],
'tl_y': [e + p for p in offsets for e in y_edges],
'br_x': [e + p for p in offsets for e in x_edges],
'br_y': [e + p for p in offsets for e in y_edges],
}
# Collect and label valid cases
# M = in bounds (middle)
# T = out of bounds on the top
# L = out of bounds on the left
# B = out of bounds on the bottom
# R = out of bounds on the right
cases = []
for item in basis_product(basis):
bbox = (item['tl_x'], item['tl_y'], item['br_x'], item['br_y'])
x1, y1, x2, y2 = bbox
if x1 < x2 and y1 < y2:
parts = []
if x1 < 0:
parts.append('x1=L')
elif x1 < width:
parts.append('x1=M')
else:
parts.append('x1=R')
if x2 <= 0:
parts.append('x2=L')
elif x2 <= width:
parts.append('x2=M')
else:
parts.append('x2=R')
if y1 < 0:
parts.append('y1=T')
elif y1 < width:
parts.append('y1=M')
else:
parts.append('y1=B')
if y2 <= 0:
parts.append('y2=T')
elif y2 <= width:
parts.append('y2=M')
else:
parts.append('y2=B')
assert len(parts) == 4
label = ','.join(parts)
cases.append((label, bbox))
cases = sorted(cases)
print('total cases: {}'.format(len(cases)))
failed_cases = []
passed_cases = []
# We will execute the MWE in a separate python process via the "-c"
# argument so we can programatically kill cases that hang
test_case_lines = [
'import pyqtree',
'bbox, width, height = {!r}, {!r}, {!r}',
'qtree = pyqtree.Index((0, 0, width, height))',
'[qtree.insert(idx, bbox) for idx in range(1, 11)]',
'qtree.insert(11, bbox)',
]
import subprocess
for label, bbox in cases:
pycmd = ';'.join(test_case_lines).format(bbox, width, height)
command = 'python -c "{}"'.format(pycmd)
info = ub.cmd(command, detatch=True)
proc = info['proc']
try:
if proc.wait(timeout=0.1) != 0:
raise AssertionError
except (subprocess.TimeoutExpired, AssertionError):
# Kill cases that hang
proc.terminate()
text = 'Failed case: {}, bbox = {!r}'.format(label, bbox)
color = 'red'
failed_cases.append((label, bbox, text))
else:
out, err = proc.communicate()
text = 'Passed case: {}, bbox = {!r}'.format(label, bbox)
color = 'green'
passed_cases.append((label, bbox, text))
print(ub.color_text(text, color))
print('len(failed_cases) = {}'.format(len(failed_cases)))
print('len(passed_cases) = {}'.format(len(passed_cases)))
passed_labels = set([t[0] for t in passed_cases])
failed_labels = set([t[0] for t in failed_cases])
print('passed_labels = {}'.format(ub.repr2(sorted(passed_labels))))
print('failed_labels = {}'.format(ub.repr2(sorted(failed_labels))))
print('overlap = {}'.format(set(passed_labels) & set(failed_labels)))
The idea is I create boxes that are either in or out of bounds in various ways. I label these as:
In the script I run a separate python process that executes the MWE and checks to see if it times out. If it does then I assume the process hung.
Running this script (which takes a bit when there a lot of cases). In total 1557 cases pass and 468 cases fail. The passing and failing cases follow a pattern. Passing and failing labels disjoint are:
passed_labels = [
'x1=L,x2=L,y1=B,y2=B',
'x1=L,x2=L,y1=T,y2=T',
'x1=L,x2=M,y1=M,y2=B',
'x1=L,x2=M,y1=M,y2=M',
'x1=L,x2=M,y1=T,y2=B',
'x1=L,x2=M,y1=T,y2=M',
'x1=L,x2=R,y1=M,y2=B',
'x1=L,x2=R,y1=M,y2=M',
'x1=L,x2=R,y1=T,y2=B',
'x1=L,x2=R,y1=T,y2=M',
'x1=M,x2=M,y1=M,y2=B',
'x1=M,x2=M,y1=M,y2=M',
'x1=M,x2=M,y1=T,y2=B',
'x1=M,x2=M,y1=T,y2=M',
'x1=M,x2=R,y1=M,y2=B',
'x1=M,x2=R,y1=M,y2=M',
'x1=M,x2=R,y1=T,y2=B',
'x1=M,x2=R,y1=T,y2=M',
'x1=R,x2=R,y1=B,y2=B',
'x1=R,x2=R,y1=T,y2=T',
]
failed_labels = [
'x1=L,x2=L,y1=M,y2=B',
'x1=L,x2=L,y1=M,y2=M',
'x1=L,x2=L,y1=T,y2=B',
'x1=L,x2=L,y1=T,y2=M',
'x1=L,x2=M,y1=B,y2=B',
'x1=L,x2=M,y1=T,y2=T',
'x1=L,x2=R,y1=B,y2=B',
'x1=L,x2=R,y1=T,y2=T',
'x1=M,x2=M,y1=B,y2=B',
'x1=M,x2=M,y1=T,y2=T',
'x1=M,x2=R,y1=B,y2=B',
'x1=M,x2=R,y1=T,y2=T',
'x1=R,x2=R,y1=M,y2=B',
'x1=R,x2=R,y1=M,y2=M',
'x1=R,x2=R,y1=T,y2=B',
'x1=R,x2=R,y1=T,y2=M',
]
Each label is 4 chars following the above coding sequence for x1,x2,y1,y2. You can see that all failing cases involve exactly one of x or y being entirely out of bounds on the head or tail side of the axis, while the other axis is always partially touching the middle.
I haven't dug into the code at all to see where the error could be coming from. Hopefully these details help.