spatial-graph 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spatial_graph/__init__.py +15 -0
- spatial_graph/dtypes.py +130 -0
- spatial_graph/graph/__init__.py +3 -0
- spatial_graph/graph/graph.py +226 -0
- spatial_graph/graph/src/LICENSE.txt +7 -0
- spatial_graph/graph/src/graph_lite.h +1357 -0
- spatial_graph/graph/wrapper_template.pyx +654 -0
- spatial_graph/rtree/__init__.py +5 -0
- spatial_graph/rtree/line_rtree.py +156 -0
- spatial_graph/rtree/point_rtree.py +6 -0
- spatial_graph/rtree/rtree.py +125 -0
- spatial_graph/rtree/src/ARCHITECTURE.md +162 -0
- spatial_graph/rtree/src/LICENSE +20 -0
- spatial_graph/rtree/src/config.h +13 -0
- spatial_graph/rtree/src/rtree.c +1021 -0
- spatial_graph/rtree/src/rtree.h +97 -0
- spatial_graph/rtree/wrapper_template.pyx +356 -0
- spatial_graph/spatial_graph.py +95 -0
- spatial_graph-0.0.1.dist-info/METADATA +130 -0
- spatial_graph-0.0.1.dist-info/RECORD +22 -0
- spatial_graph-0.0.1.dist-info/WHEEL +4 -0
- spatial_graph-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Copyright 2023 Joshua J Baker. All rights reserved.
|
|
2
|
+
// Use of this source code is governed by an MIT-style
|
|
3
|
+
// license that can be found in the LICENSE file.
|
|
4
|
+
|
|
5
|
+
#ifndef RTREE_H
|
|
6
|
+
#define RTREE_H
|
|
7
|
+
|
|
8
|
+
#include <stdlib.h>
|
|
9
|
+
#include "config.h"
|
|
10
|
+
|
|
11
|
+
// rtree_new returns a new rtree
|
|
12
|
+
//
|
|
13
|
+
// Returns NULL if the system is out of memory.
|
|
14
|
+
struct rtree *rtree_new(void);
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
// rtree_new returns a new rtree using a custom allocator
|
|
18
|
+
//
|
|
19
|
+
// Returns NULL if the system is out of memory.
|
|
20
|
+
struct rtree *rtree_new_with_allocator(void *(*malloc)(size_t), void (*free)(void*));
|
|
21
|
+
|
|
22
|
+
// rtree_free frees an rtree
|
|
23
|
+
void rtree_free(struct rtree *tr);
|
|
24
|
+
|
|
25
|
+
// rtree_clone makes an instant copy of the btree.
|
|
26
|
+
//
|
|
27
|
+
// This operation uses shadowing / copy-on-write.
|
|
28
|
+
struct rtree *rtree_clone(struct rtree *tr);
|
|
29
|
+
|
|
30
|
+
// rtree_insert inserts an item into the rtree.
|
|
31
|
+
//
|
|
32
|
+
// This operation performs a copy of the data that is pointed to in the second
|
|
33
|
+
// and third arguments. The R-tree expects a rectangle, which is two arrays of
|
|
34
|
+
// coord_ts. The first N values as the minimum corner of the rect, and the next
|
|
35
|
+
// N values as the maximum corner of the rect, where N is the number of
|
|
36
|
+
// dimensions.
|
|
37
|
+
//
|
|
38
|
+
// When inserting points, the max coordinates is optional (set to NULL).
|
|
39
|
+
//
|
|
40
|
+
// Returns false if the system is out of memory.
|
|
41
|
+
bool rtree_insert(struct rtree *tr, const coord_t *min, const coord_t *max, const item_t item);
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
// rtree_search searches the rtree and iterates over each item that intersect
|
|
45
|
+
// the provided rectangle.
|
|
46
|
+
//
|
|
47
|
+
// Returning false from the iter will stop the search.
|
|
48
|
+
void rtree_search(const struct rtree *tr, const coord_t *min, const coord_t *max,
|
|
49
|
+
bool (*iter)(const coord_t *min, const coord_t *max, const item_t item, void *udata),
|
|
50
|
+
void *udata);
|
|
51
|
+
|
|
52
|
+
// Find the nearest neighbors to the given query point.
|
|
53
|
+
//
|
|
54
|
+
// Returning false from the iter will stop the search.
|
|
55
|
+
bool rtree_nearest(struct rtree *tr, const coord_t *point,
|
|
56
|
+
bool (*iter)(const item_t item, coord_t distance, void *udata),
|
|
57
|
+
void *udata);
|
|
58
|
+
|
|
59
|
+
// rtree_scan iterates over every item in the rtree.
|
|
60
|
+
//
|
|
61
|
+
// Returning false from the iter will stop the scan.
|
|
62
|
+
void rtree_scan(const struct rtree *tr,
|
|
63
|
+
bool (*iter)(const coord_t *min, const coord_t *max, const item_t item, void *udata),
|
|
64
|
+
void *udata);
|
|
65
|
+
|
|
66
|
+
// rtree_count returns the number of items in the rtree.
|
|
67
|
+
size_t rtree_count(const struct rtree *tr);
|
|
68
|
+
|
|
69
|
+
// query the total bounding box of the rtree
|
|
70
|
+
void rtree_bb(const struct rtree *tr, coord_t* min, coord_t* max);
|
|
71
|
+
|
|
72
|
+
// rtree_delete deletes an item from the rtree.
|
|
73
|
+
//
|
|
74
|
+
// This searches the tree for an item that is contained within the provided
|
|
75
|
+
// rectangle, and perform a binary comparison of its data to the provided
|
|
76
|
+
// data. The first item that is found is deleted.
|
|
77
|
+
//
|
|
78
|
+
// Returns the number of deleted items (0 or 1) or -1 if an OOM error occured.
|
|
79
|
+
int rtree_delete(struct rtree *tr, const coord_t *min, const coord_t *max, const item_t item);
|
|
80
|
+
|
|
81
|
+
// rtree_delete_with_comparator deletes an item from the rtree.
|
|
82
|
+
// This searches the tree for an item that is contained within the provided
|
|
83
|
+
// rectangle, and perform a comparison of its data to the provided data using
|
|
84
|
+
// a compare function. The first item that is found is deleted.
|
|
85
|
+
//
|
|
86
|
+
// Returns false if the system is out of memory.
|
|
87
|
+
bool rtree_delete_with_comparator(struct rtree *tr, const coord_t *min,
|
|
88
|
+
const coord_t *max, const item_t item,
|
|
89
|
+
int (*compare)(const item_t a, const item_t b, void *udata),
|
|
90
|
+
void *udata);
|
|
91
|
+
|
|
92
|
+
// rtree_opt_relaxed_atomics activates memory_order_relaxed for all atomic
|
|
93
|
+
// loads. This may increase performance for single-threaded programs.
|
|
94
|
+
// Optionally, define RTREE_NOATOMICS to disbale all atomics.
|
|
95
|
+
void rtree_opt_relaxed_atomics(struct rtree *tr);
|
|
96
|
+
|
|
97
|
+
#endif // RTREE_H
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
from libc.stdint cimport *
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
ctypedef int bool
|
|
6
|
+
|
|
7
|
+
cdef extern from *:
|
|
8
|
+
"""
|
|
9
|
+
typedef int bool;
|
|
10
|
+
#define false 0
|
|
11
|
+
#define true 1
|
|
12
|
+
|
|
13
|
+
%if $c_distance_function
|
|
14
|
+
#define KNN_USE_EXACT_DISTANCE
|
|
15
|
+
%end if
|
|
16
|
+
#define DIMS $dims
|
|
17
|
+
|
|
18
|
+
typedef $coord_dtype.to_pyxtype() coord_t;
|
|
19
|
+
typedef $item_dtype.base_c_type item_base_t;
|
|
20
|
+
%if $item_dtype.is_array
|
|
21
|
+
typedef item_base_t pyx_item_t[$item_dtype.size];
|
|
22
|
+
%else
|
|
23
|
+
typedef item_base_t pyx_item_t;
|
|
24
|
+
%end if
|
|
25
|
+
typedef pyx_item_t* pyx_items_t;
|
|
26
|
+
|
|
27
|
+
%if $c_item_t_declaration
|
|
28
|
+
$c_item_t_declaration
|
|
29
|
+
%else
|
|
30
|
+
%if $item_dtype.is_array
|
|
31
|
+
typedef struct item_t {
|
|
32
|
+
item_base_t data[$item_dtype.size];
|
|
33
|
+
} item_t;
|
|
34
|
+
%else
|
|
35
|
+
typedef item_base_t item_t;
|
|
36
|
+
%end if
|
|
37
|
+
%end if
|
|
38
|
+
|
|
39
|
+
%if $c_equal_function
|
|
40
|
+
$c_equal_function
|
|
41
|
+
%else
|
|
42
|
+
inline bool equal(const item_t a, const item_t b) {
|
|
43
|
+
%if $item_dtype.is_array
|
|
44
|
+
return memcmp(&a, &b, sizeof(item_t));
|
|
45
|
+
%else
|
|
46
|
+
return a == b;
|
|
47
|
+
%end if
|
|
48
|
+
}
|
|
49
|
+
%end if
|
|
50
|
+
|
|
51
|
+
#include "src/rtree.h"
|
|
52
|
+
#include "src/rtree.c"
|
|
53
|
+
|
|
54
|
+
%if $c_converter_functions
|
|
55
|
+
$c_converter_functions
|
|
56
|
+
%else
|
|
57
|
+
%if $item_dtype.is_array
|
|
58
|
+
inline item_t convert_pyx_to_c_item(pyx_item_t *pyx_item, coord_t *min, coord_t *max) {
|
|
59
|
+
item_t c_item;
|
|
60
|
+
memcpy(&c_item, *pyx_item, sizeof(item_t));
|
|
61
|
+
return c_item;
|
|
62
|
+
}
|
|
63
|
+
inline void copy_c_to_pyx_item(const item_t c_item, pyx_item_t *pyx_item) {
|
|
64
|
+
memcpy(pyx_item, &c_item, sizeof(item_t));
|
|
65
|
+
}
|
|
66
|
+
%else
|
|
67
|
+
// default PYX<->C converters, just casting
|
|
68
|
+
inline item_t convert_pyx_to_c_item(pyx_item_t *pyx_item, coord_t *min, coord_t *max) {
|
|
69
|
+
return (item_t)*pyx_item;
|
|
70
|
+
}
|
|
71
|
+
inline void copy_c_to_pyx_item(const item_t c_item, pyx_item_t *pyx_item) {
|
|
72
|
+
memcpy(pyx_item, &c_item, sizeof(item_t));
|
|
73
|
+
}
|
|
74
|
+
%end if
|
|
75
|
+
%end if
|
|
76
|
+
|
|
77
|
+
%if $c_distance_function
|
|
78
|
+
$c_distance_function
|
|
79
|
+
%end if
|
|
80
|
+
"""
|
|
81
|
+
cdef enum:
|
|
82
|
+
DIMS = $dims
|
|
83
|
+
ctypedef $coord_dtype.to_pyxtype() coord_t
|
|
84
|
+
ctypedef $item_dtype.base_c_type item_base_t
|
|
85
|
+
%if $item_dtype.is_array
|
|
86
|
+
ctypedef item_base_t pyx_item_t[$item_dtype.size]
|
|
87
|
+
%else
|
|
88
|
+
ctypedef item_base_t pyx_item_t
|
|
89
|
+
%end if
|
|
90
|
+
ctypedef pyx_item_t* pyx_items_t
|
|
91
|
+
|
|
92
|
+
%if $pyx_item_t_declaration
|
|
93
|
+
$pyx_item_t_declaration
|
|
94
|
+
%else
|
|
95
|
+
%if $item_dtype.is_array
|
|
96
|
+
# item_t can't be an array in rtree, arrays can't be assigned to (and this
|
|
97
|
+
# is needed inside rtree). So we make item_t a struct with field `data` to
|
|
98
|
+
# hold the array.
|
|
99
|
+
cdef struct item_t:
|
|
100
|
+
item_base_t data[$item_dtype.size]
|
|
101
|
+
%else
|
|
102
|
+
ctypedef item_base_t item_t
|
|
103
|
+
%end if
|
|
104
|
+
%end if
|
|
105
|
+
|
|
106
|
+
# PYX <-> C converters
|
|
107
|
+
cdef item_t convert_pyx_to_c_item(pyx_item_t *pyx_item, coord_t *min, coord_t* max)
|
|
108
|
+
cdef void copy_c_to_pyx_item(const item_t c_item, pyx_item_t *pyx_item)
|
|
109
|
+
|
|
110
|
+
# rtree API
|
|
111
|
+
cdef struct rtree
|
|
112
|
+
cdef rtree *rtree_new()
|
|
113
|
+
cdef void rtree_free(rtree *tr)
|
|
114
|
+
cdef bool rtree_insert(
|
|
115
|
+
rtree *tr,
|
|
116
|
+
const coord_t *min,
|
|
117
|
+
const coord_t *max,
|
|
118
|
+
const item_t item)
|
|
119
|
+
cdef void rtree_search(
|
|
120
|
+
const rtree *tr,
|
|
121
|
+
const coord_t *min,
|
|
122
|
+
const coord_t *max,
|
|
123
|
+
bool (*iter)(
|
|
124
|
+
const coord_t *min,
|
|
125
|
+
const coord_t *max,
|
|
126
|
+
const item_t item,
|
|
127
|
+
void *udata),
|
|
128
|
+
void *udata)
|
|
129
|
+
cdef bool rtree_nearest(
|
|
130
|
+
rtree *tr,
|
|
131
|
+
const coord_t *point,
|
|
132
|
+
bool (*iter)(
|
|
133
|
+
const item_t item,
|
|
134
|
+
coord_t distance,
|
|
135
|
+
void *udata),
|
|
136
|
+
void *udata)
|
|
137
|
+
cdef int rtree_delete(
|
|
138
|
+
rtree *tr,
|
|
139
|
+
const coord_t *min,
|
|
140
|
+
const coord_t *max,
|
|
141
|
+
const item_t item)
|
|
142
|
+
cdef size_t rtree_count(const rtree *tr)
|
|
143
|
+
cdef void rtree_bb(const rtree *tr, coord_t *min, coord_t *max)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
cdef pyx_items_t memview_to_pyx_items_t($item_dtype.to_pyxtype(add_dim=True) items):
|
|
147
|
+
# implementation depends on dimension of item
|
|
148
|
+
%if $item_dtype.is_array
|
|
149
|
+
return <pyx_items_t>&items[0, 0]
|
|
150
|
+
%else
|
|
151
|
+
return <pyx_items_t>&items[0]
|
|
152
|
+
%end if
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
cdef bint count_iterator(
|
|
156
|
+
const coord_t* bb_min,
|
|
157
|
+
const coord_t* bb_max,
|
|
158
|
+
const item_t item,
|
|
159
|
+
void* udata
|
|
160
|
+
) noexcept:
|
|
161
|
+
|
|
162
|
+
cdef size_t* count = <size_t*>udata
|
|
163
|
+
count[0] = count[0] + 1
|
|
164
|
+
return True
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
cdef struct search_results:
|
|
168
|
+
size_t size
|
|
169
|
+
pyx_items_t items
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
cdef init_search_results_from_memview(search_results* r, $item_dtype.to_pyxtype(add_dim=True) items):
|
|
173
|
+
r.size = 0
|
|
174
|
+
r.items = memview_to_pyx_items_t(items)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
cdef bint search_iterator(
|
|
178
|
+
const coord_t* bb_min,
|
|
179
|
+
const coord_t* bb_max,
|
|
180
|
+
const item_t item,
|
|
181
|
+
void* udata
|
|
182
|
+
) noexcept:
|
|
183
|
+
|
|
184
|
+
cdef search_results* results = <search_results*>udata
|
|
185
|
+
copy_c_to_pyx_item(item, &results.items[results.size])
|
|
186
|
+
results.size += 1
|
|
187
|
+
return True
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
cdef struct nearest_results:
|
|
191
|
+
size_t size
|
|
192
|
+
size_t max_size
|
|
193
|
+
pyx_items_t items
|
|
194
|
+
coord_t *distances
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
cdef init_nearest_results_from_memview(nearest_results* r,
|
|
198
|
+
$item_dtype.to_pyxtype(add_dim=True) items,
|
|
199
|
+
coord_t[::1] distances):
|
|
200
|
+
r.size = 0
|
|
201
|
+
r.max_size = len(items)
|
|
202
|
+
r.items = memview_to_pyx_items_t(items)
|
|
203
|
+
r.distances = &distances[0] if distances is not None else NULL
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
cdef bint nearest_iterator(
|
|
207
|
+
const item_t item,
|
|
208
|
+
coord_t distance,
|
|
209
|
+
void* udata
|
|
210
|
+
) noexcept:
|
|
211
|
+
|
|
212
|
+
cdef nearest_results* results = <nearest_results*>udata
|
|
213
|
+
copy_c_to_pyx_item(item, &results.items[results.size])
|
|
214
|
+
if results.distances != NULL:
|
|
215
|
+
results.distances[results.size] = distance
|
|
216
|
+
results.size += 1
|
|
217
|
+
return results.size < results.max_size
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
cdef class RTree:
|
|
221
|
+
|
|
222
|
+
cdef rtree* _rtree
|
|
223
|
+
|
|
224
|
+
def __cinit__(self):
|
|
225
|
+
self._rtree = rtree_new()
|
|
226
|
+
|
|
227
|
+
def __dealloc__(self):
|
|
228
|
+
rtree_free(self._rtree)
|
|
229
|
+
|
|
230
|
+
def insert_point_items(
|
|
231
|
+
self,
|
|
232
|
+
$item_dtype.to_pyxtype(add_dim=True) items,
|
|
233
|
+
coord_t[:, ::1] points
|
|
234
|
+
):
|
|
235
|
+
|
|
236
|
+
cdef pyx_items_t pyx_items = memview_to_pyx_items_t(items)
|
|
237
|
+
|
|
238
|
+
for i in range(len(items)):
|
|
239
|
+
rtree_insert(
|
|
240
|
+
self._rtree,
|
|
241
|
+
&points[i, 0],
|
|
242
|
+
NULL,
|
|
243
|
+
convert_pyx_to_c_item(&pyx_items[i], &points[i, 0], NULL))
|
|
244
|
+
|
|
245
|
+
def insert_bb_items(
|
|
246
|
+
self,
|
|
247
|
+
$item_dtype.to_pyxtype(add_dim=True) items,
|
|
248
|
+
coord_t[:, ::1] bb_mins,
|
|
249
|
+
coord_t[:, ::1] bb_maxs
|
|
250
|
+
):
|
|
251
|
+
|
|
252
|
+
cdef pyx_items_t pyx_items = memview_to_pyx_items_t(items)
|
|
253
|
+
|
|
254
|
+
for i in range(len(items)):
|
|
255
|
+
rtree_insert(
|
|
256
|
+
self._rtree,
|
|
257
|
+
&bb_mins[i, 0],
|
|
258
|
+
&bb_maxs[i, 0],
|
|
259
|
+
convert_pyx_to_c_item(&pyx_items[i], &bb_mins[i, 0], &bb_maxs[i, 0]))
|
|
260
|
+
|
|
261
|
+
def count(self, coord_t[::1] bb_min, coord_t[::1] bb_max):
|
|
262
|
+
|
|
263
|
+
cdef size_t num = 0
|
|
264
|
+
rtree_search(
|
|
265
|
+
self._rtree,
|
|
266
|
+
&bb_min[0],
|
|
267
|
+
&bb_max[0],
|
|
268
|
+
&count_iterator,
|
|
269
|
+
&num)
|
|
270
|
+
|
|
271
|
+
return num
|
|
272
|
+
|
|
273
|
+
def bounding_box(self):
|
|
274
|
+
bb_min = np.empty(($dims,), dtype="$coord_dtype.base")
|
|
275
|
+
bb_max = np.empty(($dims,), dtype="$coord_dtype.base")
|
|
276
|
+
cdef coord_t[::1] _bb_min = bb_min
|
|
277
|
+
cdef coord_t[::1] _bb_max = bb_max
|
|
278
|
+
rtree_bb(self._rtree, &_bb_min[0], &_bb_max[0])
|
|
279
|
+
return (bb_min, bb_max)
|
|
280
|
+
|
|
281
|
+
def search(self, coord_t[::1] bb_min, coord_t[::1] bb_max):
|
|
282
|
+
|
|
283
|
+
cdef search_results results
|
|
284
|
+
cdef size_t num_results = self.count(bb_min, bb_max)
|
|
285
|
+
|
|
286
|
+
items = np.zeros((num_results, $item_dtype.size), dtype="$item_dtype.base")
|
|
287
|
+
if num_results == 0:
|
|
288
|
+
return items
|
|
289
|
+
init_search_results_from_memview(&results, items)
|
|
290
|
+
|
|
291
|
+
rtree_search(
|
|
292
|
+
self._rtree,
|
|
293
|
+
&bb_min[0],
|
|
294
|
+
&bb_max[0],
|
|
295
|
+
&search_iterator,
|
|
296
|
+
&results)
|
|
297
|
+
|
|
298
|
+
return items
|
|
299
|
+
|
|
300
|
+
def nearest(self, coord_t[::1] point, size_t k, return_distances=False):
|
|
301
|
+
|
|
302
|
+
cdef nearest_results results
|
|
303
|
+
|
|
304
|
+
items = np.zeros((k, $item_dtype.size), dtype="$item_dtype.base")
|
|
305
|
+
if return_distances:
|
|
306
|
+
distances = np.zeros((k,), dtype="$coord_dtype.base")
|
|
307
|
+
else:
|
|
308
|
+
distances = None
|
|
309
|
+
if k == 0:
|
|
310
|
+
return items
|
|
311
|
+
init_nearest_results_from_memview(&results, items, distances)
|
|
312
|
+
|
|
313
|
+
all_good = rtree_nearest(
|
|
314
|
+
self._rtree,
|
|
315
|
+
&point[0],
|
|
316
|
+
&nearest_iterator,
|
|
317
|
+
&results)
|
|
318
|
+
|
|
319
|
+
if not all_good:
|
|
320
|
+
raise RuntimeError("RTree nearest neighbor search ran out of memory.")
|
|
321
|
+
|
|
322
|
+
if return_distances:
|
|
323
|
+
return items[:results.size], distances[:results.size]
|
|
324
|
+
else:
|
|
325
|
+
return items[:results.size]
|
|
326
|
+
|
|
327
|
+
def delete_items(
|
|
328
|
+
self,
|
|
329
|
+
$item_dtype.to_pyxtype(add_dim=True) items,
|
|
330
|
+
coord_t[:, ::1] bb_mins,
|
|
331
|
+
coord_t[:, ::1] bb_maxs=None
|
|
332
|
+
):
|
|
333
|
+
|
|
334
|
+
if bb_maxs is None:
|
|
335
|
+
bb_maxs = bb_mins
|
|
336
|
+
|
|
337
|
+
cdef pyx_items_t pyx_items = memview_to_pyx_items_t(items)
|
|
338
|
+
|
|
339
|
+
total_deleted = 0
|
|
340
|
+
for i in range(len(items)):
|
|
341
|
+
num_deleted = rtree_delete(
|
|
342
|
+
self._rtree,
|
|
343
|
+
&bb_mins[i, 0],
|
|
344
|
+
&bb_maxs[i, 0],
|
|
345
|
+
convert_pyx_to_c_item(&pyx_items[i], &bb_mins[i, 0], &bb_maxs[i, 0]))
|
|
346
|
+
if num_deleted == -1:
|
|
347
|
+
raise RuntimeError("RTree delete ran out of memory.")
|
|
348
|
+
if num_deleted == 0:
|
|
349
|
+
print(f"Item {pyx_items[i]} not deleted!")
|
|
350
|
+
total_deleted += num_deleted
|
|
351
|
+
|
|
352
|
+
return total_deleted
|
|
353
|
+
|
|
354
|
+
def __len__(self):
|
|
355
|
+
|
|
356
|
+
return rtree_count(self._rtree)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from .graph import Graph
|
|
2
|
+
from .rtree import PointRTree, LineRTree
|
|
3
|
+
from .dtypes import DType
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SpatialGraph(Graph):
|
|
8
|
+
edge_inclusion_values = ["incident", "leaving", "entering"]
|
|
9
|
+
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
ndims,
|
|
13
|
+
node_dtype,
|
|
14
|
+
node_attr_dtypes,
|
|
15
|
+
edge_attr_dtypes,
|
|
16
|
+
position_attr,
|
|
17
|
+
directed=False,
|
|
18
|
+
):
|
|
19
|
+
if position_attr not in node_attr_dtypes:
|
|
20
|
+
raise ValueError(
|
|
21
|
+
f"position attribute {position_attr!r} not defined in 'node_attr_dtypes'"
|
|
22
|
+
)
|
|
23
|
+
super().__init__(node_dtype, node_attr_dtypes, edge_attr_dtypes, directed)
|
|
24
|
+
|
|
25
|
+
self.ndims = ndims
|
|
26
|
+
self.position_attr = position_attr
|
|
27
|
+
self.coord_dtype = DType(node_attr_dtypes[position_attr]).base
|
|
28
|
+
self._node_rtree = PointRTree(node_dtype, self.coord_dtype, ndims)
|
|
29
|
+
self._edge_rtree = LineRTree(f"{node_dtype}[2]", self.coord_dtype, ndims)
|
|
30
|
+
|
|
31
|
+
def add_node(self, node, **kwargs):
|
|
32
|
+
position = self._get_position(kwargs)
|
|
33
|
+
self._node_rtree.insert_point_item(node, position)
|
|
34
|
+
super().add_node(node, **kwargs)
|
|
35
|
+
|
|
36
|
+
def add_nodes(self, nodes, **kwargs):
|
|
37
|
+
positions = self._get_position(kwargs)
|
|
38
|
+
self._node_rtree.insert_point_items(nodes, positions)
|
|
39
|
+
super().add_nodes(nodes, **kwargs)
|
|
40
|
+
|
|
41
|
+
def add_edge(self, edge, **kwargs):
|
|
42
|
+
edge = np.array(edge, dtype=self.node_dtype)
|
|
43
|
+
position_u = getattr(self.node_attrs[edge[0]], self.position_attr)
|
|
44
|
+
position_v = getattr(self.node_attrs[edge[1]], self.position_attr)
|
|
45
|
+
self._edge_rtree.insert_line(edge, position_u, position_v)
|
|
46
|
+
super().add_edge(edge, **kwargs)
|
|
47
|
+
|
|
48
|
+
def add_edges(self, edges, **kwargs):
|
|
49
|
+
starts = getattr(self.node_attrs[edges[:, 0]], self.position_attr)
|
|
50
|
+
ends = getattr(self.node_attrs[edges[:, 1]], self.position_attr)
|
|
51
|
+
self._edge_rtree.insert_lines(edges, starts, ends)
|
|
52
|
+
super().add_edges(edges, **kwargs)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def roi(self):
|
|
56
|
+
return self._node_rtree.bounding_box()
|
|
57
|
+
|
|
58
|
+
def query_nodes_in_roi(self, roi):
|
|
59
|
+
return self._node_rtree.search(roi[0], roi[1])
|
|
60
|
+
|
|
61
|
+
def query_edges_in_roi(self, roi):
|
|
62
|
+
return self._edge_rtree.search(roi[0], roi[1])
|
|
63
|
+
|
|
64
|
+
def query_nearest_nodes(self, point, k, return_distances=False):
|
|
65
|
+
return self._node_rtree.nearest(point, k, return_distances)
|
|
66
|
+
|
|
67
|
+
def query_nearest_edges(self, point, k, return_distances=False):
|
|
68
|
+
return self._edge_rtree.nearest(point, k, return_distances)
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def nodes(self):
|
|
72
|
+
return super().nodes()
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def edges(self):
|
|
76
|
+
return self.query_edges_in_roi(self.roi)
|
|
77
|
+
|
|
78
|
+
def remove_nodes(self, nodes):
|
|
79
|
+
positions = getattr(self.node_attrs[nodes], self.position_attr)
|
|
80
|
+
self._node_rtree.delete_items(nodes, positions)
|
|
81
|
+
if not self.directed:
|
|
82
|
+
edges = self.edges_by_nodes(nodes)
|
|
83
|
+
else:
|
|
84
|
+
edges = np.concatenate(
|
|
85
|
+
self.in_edges_by_nodes(nodes), self.out_edges_by_nodes(nodes)
|
|
86
|
+
)
|
|
87
|
+
positions_u = getattr(self.node_attrs[edges[:, 0]], self.position_attr)
|
|
88
|
+
positions_v = getattr(self.node_attrs[edges[:, 1]], self.position_attr)
|
|
89
|
+
self._edge_rtree.delete_items(edges, positions_u, positions_v)
|
|
90
|
+
super().remove_nodes(nodes)
|
|
91
|
+
|
|
92
|
+
def _get_position(self, kwargs):
|
|
93
|
+
if self.position_attr in kwargs:
|
|
94
|
+
return kwargs[self.position_attr]
|
|
95
|
+
raise RuntimeError(f"position attribute '{self.position_attr}' not given")
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spatial-graph
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A spatial graph datastructure for python.
|
|
5
|
+
Project-URL: homepage, https://github.com/funkelab/spatial_graph
|
|
6
|
+
Project-URL: repository, https://github.com/funkelab/spatial_graph
|
|
7
|
+
Author-email: Jan Funke <funkej@janelia.hhmi.org>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Requires-Dist: cheetah3
|
|
12
|
+
Requires-Dist: numpy
|
|
13
|
+
Requires-Dist: setuptools>=75.8.0
|
|
14
|
+
Requires-Dist: witty>=v0.2.0
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=8.3.4; extra == 'dev'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# spatial_graph
|
|
20
|
+
|
|
21
|
+
[](https://github.com/funkelab/spatial_graph/actions/workflows/ci.yaml)
|
|
22
|
+
|
|
23
|
+
`spatial_graph` provides a data structure for directed and undirected graphs,
|
|
24
|
+
where each node has an nD position (in time or space).
|
|
25
|
+
|
|
26
|
+
Design Principles
|
|
27
|
+
=================
|
|
28
|
+
|
|
29
|
+
Goals
|
|
30
|
+
-----
|
|
31
|
+
|
|
32
|
+
* support for arbitrary number of dimensions
|
|
33
|
+
* typed node identifiers and attributes
|
|
34
|
+
* any fixed-length type that is supported by `numpy`
|
|
35
|
+
* efficient node/edge queries by
|
|
36
|
+
* ROI
|
|
37
|
+
* kNN (by points / lines)
|
|
38
|
+
* numpy-like interface for efficient:
|
|
39
|
+
* graph population and manipulation
|
|
40
|
+
* query results
|
|
41
|
+
* attribute access
|
|
42
|
+
* minimal memory footprint
|
|
43
|
+
* minimal dependencies
|
|
44
|
+
* `cython` / `witty` / `cheetah3` for runtime compilation
|
|
45
|
+
* numpy for array interfaces
|
|
46
|
+
* PYX API for graph algorithms in C/C++
|
|
47
|
+
|
|
48
|
+
Non-Goals
|
|
49
|
+
---------
|
|
50
|
+
|
|
51
|
+
* graph algorithms
|
|
52
|
+
* I/O
|
|
53
|
+
* non-typed arguments
|
|
54
|
+
* non-spatial graphs
|
|
55
|
+
* out-of-memory support
|
|
56
|
+
* networkx compatibility
|
|
57
|
+
|
|
58
|
+
Python API
|
|
59
|
+
==========
|
|
60
|
+
|
|
61
|
+
Graph creation:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
graph = sg.SpatialGraph(
|
|
65
|
+
ndims=3,
|
|
66
|
+
node_dtype="uint64",
|
|
67
|
+
node_attr_dtypes={"position": "double[3]"},
|
|
68
|
+
edge_attr_dtypes={"score": "float32"},
|
|
69
|
+
position_attr="position",
|
|
70
|
+
directed=False,
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Adding nodes/edges:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
graph.add_nodes(
|
|
78
|
+
np.array([1, 2, 3, 4, 5], dtype="uint64"),
|
|
79
|
+
position=np.array(
|
|
80
|
+
[
|
|
81
|
+
[0.1, 0.1, 0.1],
|
|
82
|
+
[0.2, 0.2, 0.2],
|
|
83
|
+
[0.3, 0.3, 0.3],
|
|
84
|
+
[0.4, 0.4, 0.4],
|
|
85
|
+
[0.5, 0.5, 0.5],
|
|
86
|
+
],
|
|
87
|
+
dtype="double",
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
graph.add_edges(
|
|
92
|
+
np.array([[1, 2], [3, 4], [5, 1]], dtype="uint64"),
|
|
93
|
+
score=np.array([0.2, 0.3, 0.4], dtype="float32"),
|
|
94
|
+
)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Query nodes/edges in ROI:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
# nodes/edges will be numpy arrays of dtype uint64 and shape (n,)/(n, 2)
|
|
101
|
+
nodes = graph.query_nodes_in_roi(np.array([[0.0, 0.0, 0.0], [0.25, 0.25, 0.25]]))
|
|
102
|
+
edges = graph.query_edges_in_roi(np.array([[0.0, 0.0, 0.0], [0.25, 0.25, 0.25]]))
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Query nodes/edges by position:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
nodes = graph.query_nearest_nodes(np.array([0.3, 0.3, 0.3]), k=3)
|
|
109
|
+
edges = graph.query_nearest_edges(np.array([0.3, 0.3, 0.3]), k=3)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Access node/edge attributes:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
node_positions = graph.node_attrs[nodes].position
|
|
116
|
+
edge_scores = graph.edge_attrs[edges].score
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Delete nodes/edges:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
graph.remove_nodes(nodes[:1000])
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Implementation Details
|
|
126
|
+
======================
|
|
127
|
+
|
|
128
|
+
A `SpatialGraph` consists of three data structures:
|
|
129
|
+
* The `Graph` itself, holding nodes, edges, and their attributes ([graphlite](https://github.com/haasdo95/graphlite)).
|
|
130
|
+
* Two R-trees for spatial node and edge queries (based on [rtree.c](https://github.com/tidwall/rtree.c)).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
spatial_graph/__init__.py,sha256=pxk-5VDpsCozjvu7hxBODsfOktnslCoHz2ZDFkMjgj4,358
|
|
2
|
+
spatial_graph/dtypes.py,sha256=doDBC6ytT0maH8xL4mxDGhZj_MaTGWDoismuKA7zxDw,4110
|
|
3
|
+
spatial_graph/spatial_graph.py,sha256=yFf_7fucdwPBSVY0NvRZQAbK4hr8q3Gqzx16spl3Qzg,3528
|
|
4
|
+
spatial_graph/graph/__init__.py,sha256=IiYvwf3SFD6a8CM-EuVPf-ljAAQp-iFyjIx3bMxvuAs,46
|
|
5
|
+
spatial_graph/graph/graph.py,sha256=yrHiWu_MwAjksQQ_JNg5wz32RUig-uDfPDfrr0jE_Ho,8050
|
|
6
|
+
spatial_graph/graph/wrapper_template.pyx,sha256=P9yXWFKOm0cF8Gyc_arMaGHIPHKItOmWf3SLymY2sMo,20065
|
|
7
|
+
spatial_graph/graph/src/LICENSE.txt,sha256=VGv2QnX5wQ9uezPVb1HoLKPGpwSa3gN3TnCH16moz6k,1050
|
|
8
|
+
spatial_graph/graph/src/graph_lite.h,sha256=DYQbILPOyfHRJK8Bo5uuQ2ST1o99axWHLZLhqcXuxQQ,67913
|
|
9
|
+
spatial_graph/rtree/__init__.py,sha256=WQlRnGUdl0PqBuce0tetIRHAEjegH3QW9ZUTD7420KQ,110
|
|
10
|
+
spatial_graph/rtree/line_rtree.py,sha256=tx31eCzULcJ35N1XEu4448p8qLylv_zuNFs8bn3kFEQ,4260
|
|
11
|
+
spatial_graph/rtree/point_rtree.py,sha256=o5acJoSFYWNoJGTBFhGs24zIK4bVHUgRgKxmTVs_RxQ,102
|
|
12
|
+
spatial_graph/rtree/rtree.py,sha256=sR2L7f0hhpk9HLJ8xLMGM6FmYGENfyd13RuhOq77NDE,4238
|
|
13
|
+
spatial_graph/rtree/wrapper_template.pyx,sha256=FVooO-EE8oxDvnhTtDOkvYH3FYGJuAQNpl_KArYabhs,9683
|
|
14
|
+
spatial_graph/rtree/src/ARCHITECTURE.md,sha256=TwQruizoeGjjcdxhz_H-a9jh3XvvRG7QfouSvdEKt6o,4712
|
|
15
|
+
spatial_graph/rtree/src/LICENSE,sha256=5eZvRGXb-y4p7Wev7djIMtc_q-5j_SdL6pjbNNF-lUE,1081
|
|
16
|
+
spatial_graph/rtree/src/config.h,sha256=65s3NLcvpBXGgnRYjigWs4cExN5T6gm7Vu7CsxDvD08,348
|
|
17
|
+
spatial_graph/rtree/src/rtree.c,sha256=ykXltnXOVx4Rqxgu0Yq2ATZWVk_4k6RZNgp417vQN9g,25135
|
|
18
|
+
spatial_graph/rtree/src/rtree.h,sha256=melC2Fl6o-72c0I6S4Z22op2U6NpnA6hgGK81GT1hI0,3663
|
|
19
|
+
spatial_graph-0.0.1.dist-info/METADATA,sha256=G6lm2a7WnRjmBabfPJ66Oh4Qvfho8uUEokovWuafalk,3284
|
|
20
|
+
spatial_graph-0.0.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
21
|
+
spatial_graph-0.0.1.dist-info/licenses/LICENSE,sha256=51KA-SVg7xhkZxV512EmrwJninM0t0h_UqqDf653NSk,1066
|
|
22
|
+
spatial_graph-0.0.1.dist-info/RECORD,,
|