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,15 @@
|
|
|
1
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
__version__ = version("spatial_graph")
|
|
5
|
+
except PackageNotFoundError:
|
|
6
|
+
__version__ = "unknown"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
from .rtree import PointRTree
|
|
10
|
+
from .rtree import LineRTree
|
|
11
|
+
from .graph import Graph
|
|
12
|
+
from .spatial_graph import SpatialGraph
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ = ["PointRTree", "LineRTree", "Graph", "SpatialGraph"]
|
spatial_graph/dtypes.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DType:
|
|
5
|
+
def __init__(self, dtype_str):
|
|
6
|
+
self.as_string = dtype_str
|
|
7
|
+
self.is_array = self.__is_array(dtype_str)
|
|
8
|
+
|
|
9
|
+
if self.is_array:
|
|
10
|
+
self.base, self.size = self.__parse_array_dtype(dtype_str)
|
|
11
|
+
self.shape = (self.size,)
|
|
12
|
+
else:
|
|
13
|
+
self.base = dtype_str
|
|
14
|
+
self.size = None
|
|
15
|
+
self.shape = ()
|
|
16
|
+
|
|
17
|
+
def __is_array(self, dtype):
|
|
18
|
+
if "[" in dtype:
|
|
19
|
+
if "]" not in dtype:
|
|
20
|
+
raise RuntimeError(f"invalid array(?) dtype {dtype}")
|
|
21
|
+
return True
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
def __parse_array_dtype(self, dtype):
|
|
25
|
+
dtype, size = dtype.split("[")
|
|
26
|
+
size = int(size.split("]")[0])
|
|
27
|
+
|
|
28
|
+
return dtype, size
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def base_c_type(self):
|
|
32
|
+
"""Convert the base of this DType into the equivalent C/C++ type."""
|
|
33
|
+
|
|
34
|
+
if self.base == "float32" or self.base == "float":
|
|
35
|
+
return "float"
|
|
36
|
+
elif self.base == "float64" or self.base == "double":
|
|
37
|
+
return "double"
|
|
38
|
+
else:
|
|
39
|
+
# this might not work for all of them, this is just a fallback
|
|
40
|
+
return np.dtype(self.base).name + "_t"
|
|
41
|
+
|
|
42
|
+
def to_c_decl(self, name):
|
|
43
|
+
"""Convert this dtype to the equivalent C/C++ declaration with the
|
|
44
|
+
given name:
|
|
45
|
+
|
|
46
|
+
"base_c_type name" if not an array
|
|
47
|
+
"base_c_type name[size]" if an array type
|
|
48
|
+
"""
|
|
49
|
+
# is this an array type?
|
|
50
|
+
if self.is_array:
|
|
51
|
+
suffix = f"[{self.size}]"
|
|
52
|
+
else:
|
|
53
|
+
suffix = ""
|
|
54
|
+
|
|
55
|
+
return self.base_c_type + " " + name + suffix
|
|
56
|
+
|
|
57
|
+
def to_pyxtype(self, use_memory_view=False, add_dim=False):
|
|
58
|
+
"""Convert this dtype to the equivalent PYX type.
|
|
59
|
+
|
|
60
|
+
"base_c_type"
|
|
61
|
+
"base_c_type[size]" if an array type
|
|
62
|
+
"base_c_type[::1]" if an array type and use_memory_view
|
|
63
|
+
"base_c_type[::1]" if not an array type and add_dim
|
|
64
|
+
"base_c_type[:, ::1]" if an array type and add_dim
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
|
|
68
|
+
use_memory_view:
|
|
69
|
+
|
|
70
|
+
If set, will produce "dtype[::1]" instead of "dtype[dim]" for
|
|
71
|
+
array types.
|
|
72
|
+
|
|
73
|
+
add_dim:
|
|
74
|
+
|
|
75
|
+
Append a dim to the type, e.g., "int32_t[::1]" instead of
|
|
76
|
+
"int32_t" for dtype "int32". If this DType is already an array,
|
|
77
|
+
will create a 2D array, e.g., "int32_t[:, ::1]".
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
# is this an array type?
|
|
81
|
+
if self.is_array:
|
|
82
|
+
if add_dim:
|
|
83
|
+
suffix = "[:, ::1]"
|
|
84
|
+
else:
|
|
85
|
+
if use_memory_view:
|
|
86
|
+
suffix = "[::1]"
|
|
87
|
+
else:
|
|
88
|
+
suffix = f"[{self.size}]"
|
|
89
|
+
else:
|
|
90
|
+
suffix = "[::1]" if add_dim else ""
|
|
91
|
+
|
|
92
|
+
return self.base_c_type + suffix
|
|
93
|
+
|
|
94
|
+
def to_rvalue(self, name, array_index=None):
|
|
95
|
+
"""Convert this dtype into an r-value to be used in PYX files for
|
|
96
|
+
assignments.
|
|
97
|
+
|
|
98
|
+
"name" default
|
|
99
|
+
"name[array_index]" if array_index is given
|
|
100
|
+
"{name[0], ..., name[size-1]}"
|
|
101
|
+
if an array type
|
|
102
|
+
"{name[array_index, 0], ..., name[array_index, size-1]}"
|
|
103
|
+
if an array type and array_index is given
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
if self.is_array:
|
|
107
|
+
if array_index:
|
|
108
|
+
return (
|
|
109
|
+
"{"
|
|
110
|
+
+ ", ".join(
|
|
111
|
+
[name + f"[{array_index}, {i}]" for i in range(self.size)]
|
|
112
|
+
)
|
|
113
|
+
+ "}"
|
|
114
|
+
)
|
|
115
|
+
else:
|
|
116
|
+
return (
|
|
117
|
+
"{" + ", ".join([name + f"[{i}]" for i in range(self.size)]) + "}"
|
|
118
|
+
)
|
|
119
|
+
else:
|
|
120
|
+
if array_index:
|
|
121
|
+
return f"{name}[{array_index}]"
|
|
122
|
+
else:
|
|
123
|
+
return name
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def dtypes_to_struct(struct_name, dtypes):
|
|
127
|
+
pyx_code = f"cdef struct {struct_name}:\n"
|
|
128
|
+
for name, dtype in dtypes.items():
|
|
129
|
+
pyx_code += f" {dtype.to_pyxtype()} {name}\n"
|
|
130
|
+
return pyx_code
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import witty
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import numpy as np
|
|
5
|
+
from Cheetah.Template import Template
|
|
6
|
+
from ..dtypes import DType
|
|
7
|
+
|
|
8
|
+
# Set platform-specific compile arguments
|
|
9
|
+
if sys.platform == "win32":
|
|
10
|
+
# Use /O2 for optimization and /std:c++20 for C++20
|
|
11
|
+
EXTRA_COMPILE_ARGS = ["/O2", "/std:c++20"]
|
|
12
|
+
else:
|
|
13
|
+
# -O3 for optimization and -std=c++20 for C++20
|
|
14
|
+
EXTRA_COMPILE_ARGS = ["-O3", "-std=c++20"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Graph:
|
|
18
|
+
def __new__(
|
|
19
|
+
cls,
|
|
20
|
+
node_dtype,
|
|
21
|
+
node_attr_dtypes=None,
|
|
22
|
+
edge_attr_dtypes=None,
|
|
23
|
+
directed=False,
|
|
24
|
+
*args,
|
|
25
|
+
**kwargs,
|
|
26
|
+
):
|
|
27
|
+
if node_attr_dtypes is None:
|
|
28
|
+
node_attr_dtypes = {}
|
|
29
|
+
if edge_attr_dtypes is None:
|
|
30
|
+
edge_attr_dtypes = {}
|
|
31
|
+
|
|
32
|
+
node_dtype = DType(node_dtype)
|
|
33
|
+
node_attr_dtypes = {
|
|
34
|
+
name: DType(dtype) for name, dtype in node_attr_dtypes.items()
|
|
35
|
+
}
|
|
36
|
+
edge_attr_dtypes = {
|
|
37
|
+
name: DType(dtype) for name, dtype in edge_attr_dtypes.items()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
src_dir = Path(__file__).parent
|
|
41
|
+
wrapper_template = Template(
|
|
42
|
+
file=str(src_dir / "wrapper_template.pyx"),
|
|
43
|
+
compilerSettings={"directiveStartToken": "%"},
|
|
44
|
+
)
|
|
45
|
+
wrapper_template.node_dtype = node_dtype
|
|
46
|
+
wrapper_template.node_attr_dtypes = node_attr_dtypes
|
|
47
|
+
wrapper_template.edge_attr_dtypes = edge_attr_dtypes
|
|
48
|
+
wrapper_template.directed = directed
|
|
49
|
+
|
|
50
|
+
wrapper = witty.compile_module(
|
|
51
|
+
str(wrapper_template),
|
|
52
|
+
source_files=[str(src_dir / "src" / "graph_lite.h")],
|
|
53
|
+
extra_compile_args=EXTRA_COMPILE_ARGS,
|
|
54
|
+
include_dirs=[str(src_dir)],
|
|
55
|
+
language="c++",
|
|
56
|
+
quiet=True,
|
|
57
|
+
)
|
|
58
|
+
GraphType = type(cls.__name__, (cls, wrapper.Graph), {})
|
|
59
|
+
return wrapper.Graph.__new__(GraphType)
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self, node_dtype, node_attr_dtypes=None, edge_attr_dtypes=None, directed=False
|
|
63
|
+
):
|
|
64
|
+
if node_attr_dtypes is None:
|
|
65
|
+
node_attr_dtypes = {}
|
|
66
|
+
if edge_attr_dtypes is None:
|
|
67
|
+
edge_attr_dtypes = {}
|
|
68
|
+
super().__init__()
|
|
69
|
+
self.node_dtype = node_dtype
|
|
70
|
+
self.node_attr_dtypes = node_attr_dtypes
|
|
71
|
+
self.edge_attr_dtypes = edge_attr_dtypes
|
|
72
|
+
self.directed = directed
|
|
73
|
+
|
|
74
|
+
self.node_attrs = NodeAttrs(self)
|
|
75
|
+
self.edge_attrs = EdgeAttrs(self)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class NodeAttrsView:
|
|
79
|
+
def __init__(self, graph, nodes):
|
|
80
|
+
super().__setattr__("graph", graph)
|
|
81
|
+
for name in graph.node_attr_dtypes.keys():
|
|
82
|
+
super().__setattr__(
|
|
83
|
+
f"get_attr_{name}", getattr(graph, f"get_nodes_data_{name}")
|
|
84
|
+
)
|
|
85
|
+
super().__setattr__(
|
|
86
|
+
f"set_attr_{name}", getattr(graph, f"set_nodes_data_{name}")
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if nodes is not None and not isinstance(nodes, np.ndarray):
|
|
90
|
+
# nodes is not an ndarray, can it be converted into one?
|
|
91
|
+
try:
|
|
92
|
+
# does it have a length?
|
|
93
|
+
_ = len(nodes)
|
|
94
|
+
# if so, convert to ndarray
|
|
95
|
+
nodes = np.array(nodes, dtype=graph.node_dtype)
|
|
96
|
+
except Exception:
|
|
97
|
+
# must be a single node
|
|
98
|
+
for name in graph.node_attr_dtypes.keys():
|
|
99
|
+
super().__setattr__(
|
|
100
|
+
f"set_attr_{name}", getattr(graph, f"set_node_data_{name}")
|
|
101
|
+
)
|
|
102
|
+
super().__setattr__(
|
|
103
|
+
f"get_attr_{name}", getattr(graph, f"get_node_data_{name}")
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# at this point, nodes is either
|
|
107
|
+
# 1. a numpy array
|
|
108
|
+
# 2. a scalar (python or numpy)
|
|
109
|
+
# 3. None
|
|
110
|
+
super().__setattr__("nodes", nodes)
|
|
111
|
+
|
|
112
|
+
def __getattr__(self, name):
|
|
113
|
+
if name in self.graph.node_attr_dtypes:
|
|
114
|
+
return getattr(self, f"get_attr_{name}")(self.nodes)
|
|
115
|
+
else:
|
|
116
|
+
raise AttributeError(name)
|
|
117
|
+
|
|
118
|
+
def __setattr__(self, name, values):
|
|
119
|
+
if name in self.graph.node_attr_dtypes:
|
|
120
|
+
return getattr(self, f"set_attr_{name}")(self.nodes, values)
|
|
121
|
+
else:
|
|
122
|
+
return super().__setattr__(name, values)
|
|
123
|
+
|
|
124
|
+
def __iter__(self):
|
|
125
|
+
# TODO: shouldn't be possible if nodes is a single node
|
|
126
|
+
yield from self.graph.nodes_data(self.nodes)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class EdgeAttrsView:
|
|
130
|
+
def __init__(self, graph, edges):
|
|
131
|
+
super().__setattr__("graph", graph)
|
|
132
|
+
for name in graph.edge_attr_dtypes.keys():
|
|
133
|
+
super().__setattr__(
|
|
134
|
+
f"get_attr_{name}", getattr(graph, f"get_edges_data_{name}")
|
|
135
|
+
)
|
|
136
|
+
super().__setattr__(
|
|
137
|
+
f"set_attr_{name}", getattr(graph, f"set_edges_data_{name}")
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# edges types we support:
|
|
141
|
+
#
|
|
142
|
+
# 1. edges = None all edges leave as is
|
|
143
|
+
# 2. edges = iteratible of 2-tuples selected edges to (n,2) ndarray
|
|
144
|
+
# 3. edges = iteratible of 2-lists selected edges to (n,2) ndarray
|
|
145
|
+
# 4. edges = (n,2) ndarray selected edges leave as is
|
|
146
|
+
# 5. edges = 2-tuple a single edge leave as is
|
|
147
|
+
# 6. edges = (2,) ndarray a single edge to 2-tuple
|
|
148
|
+
|
|
149
|
+
if edges is not None:
|
|
150
|
+
if isinstance(edges, np.ndarray):
|
|
151
|
+
if len(edges) == 2 and len(edges.shape) == 1:
|
|
152
|
+
# case 6
|
|
153
|
+
edges = tuple(edges)
|
|
154
|
+
else:
|
|
155
|
+
# case 4 with multiple edges
|
|
156
|
+
edges = edges.astype(graph.node_dtype)
|
|
157
|
+
elif isinstance(edges, tuple):
|
|
158
|
+
# case 5
|
|
159
|
+
assert len(edges) == 2, "Single edges should be given as a 2-tuple"
|
|
160
|
+
else:
|
|
161
|
+
# edges should be an iteratable
|
|
162
|
+
try:
|
|
163
|
+
# does it have a length?
|
|
164
|
+
len(edges)
|
|
165
|
+
# case 2 and 3
|
|
166
|
+
edges = np.array(edges, dtype=graph.node_dtype)
|
|
167
|
+
except Exception:
|
|
168
|
+
raise RuntimeError(f"Can not handle edges type {type(edges)}")
|
|
169
|
+
|
|
170
|
+
if isinstance(edges, np.ndarray):
|
|
171
|
+
if len(edges) == 0:
|
|
172
|
+
edges = edges.reshape((0, 2))
|
|
173
|
+
assert edges.shape[1] == 2, "Edge arrays should have shape (n, 2)"
|
|
174
|
+
edges = np.ascontiguousarray(edges.T)
|
|
175
|
+
elif isinstance(edges, tuple):
|
|
176
|
+
# a single edge
|
|
177
|
+
for name in graph.edge_attr_dtypes.keys():
|
|
178
|
+
super().__setattr__(
|
|
179
|
+
f"get_attr_{name}", getattr(graph, f"get_edge_data_{name}")
|
|
180
|
+
)
|
|
181
|
+
super().__setattr__(
|
|
182
|
+
f"set_attr_{name}", getattr(graph, f"set_edge_data_{name}")
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# at this point, edges is either
|
|
186
|
+
# 1. a nx2 numpy array
|
|
187
|
+
# 2. a 2-tuple of scalars (python or numpy)
|
|
188
|
+
# 3. None
|
|
189
|
+
super().__setattr__("edges", edges)
|
|
190
|
+
|
|
191
|
+
def __getattr__(self, name):
|
|
192
|
+
if name in self.graph.edge_attr_dtypes:
|
|
193
|
+
if self.edges is not None:
|
|
194
|
+
return getattr(self, f"get_attr_{name}")(self.edges[0], self.edges[1])
|
|
195
|
+
else:
|
|
196
|
+
return getattr(self, f"get_attr_{name}")(None, None)
|
|
197
|
+
else:
|
|
198
|
+
raise AttributeError(name)
|
|
199
|
+
|
|
200
|
+
def __setattr__(self, name, values):
|
|
201
|
+
if name in self.graph.edge_attr_dtypes:
|
|
202
|
+
return getattr(self, f"set_attr_{name}")(
|
|
203
|
+
self.edges[0], self.edges[1], values
|
|
204
|
+
)
|
|
205
|
+
else:
|
|
206
|
+
return super().__setattr__(name, values)
|
|
207
|
+
|
|
208
|
+
def __iter__(self):
|
|
209
|
+
# TODO: shouldn't be possible if edges is a single edge
|
|
210
|
+
yield from self.graph.edges_data(self.edges)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class NodeAttrs(NodeAttrsView):
|
|
214
|
+
def __init__(self, graph):
|
|
215
|
+
super().__init__(graph, nodes=None)
|
|
216
|
+
|
|
217
|
+
def __getitem__(self, nodes):
|
|
218
|
+
return NodeAttrsView(self.graph, nodes)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class EdgeAttrs(EdgeAttrsView):
|
|
222
|
+
def __init__(self, graph):
|
|
223
|
+
super().__init__(graph, edges=None)
|
|
224
|
+
|
|
225
|
+
def __getitem__(self, edges):
|
|
226
|
+
return EdgeAttrsView(self.graph, edges)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2021 Guohao Dou
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|