net2 0.0.1__tar.gz

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.
net2-0.0.1/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2026 The Python Packaging Authority
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
net2-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: net2
3
+ Version: 0.0.1
4
+ Summary: Package for working with spatial networks
5
+ Project-URL: Homepage, https://github.com/michaeldorman/net2/
6
+ Project-URL: Issues, https://github.com/michaeldorman/net2/issues
7
+ Author-email: Michael Dorman <dorman@post.bgu.ac.il>
8
+ License: Copyright (c) 2026 The Python Packaging Authority
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ License-File: LICENSE
28
+ Classifier: Operating System :: OS Independent
29
+ Classifier: Programming Language :: Python :: 3
30
+ Requires-Python: >=3.12
31
+ Description-Content-Type: text/markdown
32
+
33
+ # `net2`
34
+
35
+ `net2` is a Python package for working with spatial networks.
net2-0.0.1/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # `net2`
2
+
3
+ `net2` is a Python package for working with spatial networks.
@@ -0,0 +1,24 @@
1
+ [build-system]
2
+ requires = ["hatchling >= 1.26"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "net2"
7
+ version = "0.0.1"
8
+ authors = [
9
+ { name="Michael Dorman", email="dorman@post.bgu.ac.il" },
10
+ ]
11
+ description = "Package for working with spatial networks"
12
+ readme = "README.md"
13
+ requires-python = ">=3.12"
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Operating System :: OS Independent",
17
+ ]
18
+
19
+ [project.license]
20
+ file = "LICENSE"
21
+
22
+ [project.urls]
23
+ Homepage = "https://github.com/michaeldorman/net2/"
24
+ Issues = "https://github.com/michaeldorman/net2/issues"
File without changes
@@ -0,0 +1,210 @@
1
+ #!/usr/bin/python3
2
+ import numpy as np
3
+ import geopandas as gpd
4
+ import shapely
5
+ import networkx as nx
6
+
7
+ # Prepare network
8
+ def prepare(G, ids_to_int=True):
9
+ # IDs to 'int'
10
+ if ids_to_int:
11
+ mapping = {i: int(i) for i in G.nodes()}
12
+ G = nx.relabel_nodes(G, mapping)
13
+ # nodes
14
+ for i in G.nodes:
15
+ # 'geometry' to 'shapely'
16
+ if 'geometry' in G.nodes[i]:
17
+ if isinstance(G.nodes[i]['geometry'], str):
18
+ G.nodes[i]['geometry'] = shapely.from_wkt((G.nodes[i]['geometry']))
19
+ # edges
20
+ for u,v in G.edges:
21
+ # 'geometry' to 'shapely'
22
+ if 'geometry' in G[u][v]:
23
+ if isinstance(G[u][v]['geometry'], str):
24
+ G[u][v]['geometry'] = shapely.from_wkt((G[u][v]['geometry']))
25
+ # 'length' to 'float'
26
+ if 'length' in G[u][v]:
27
+ G[u][v]['length'] = float(G[u][v]['length'])
28
+ # rename 'travel_time' to 'time'
29
+ if 'travel_time' in G[u][v]:
30
+ G[u][v]['time'] = G[u][v]['travel_time']
31
+ del G[u][v]['travel_time']
32
+ # 'time' to 'float'
33
+ if 'time' in G[u][v]:
34
+ G[u][v]['time'] = float(G[u][v]['time'])
35
+ return G
36
+
37
+ # Calculate 'pos' dictionary with node 'x' and 'y'
38
+ def pos(G):
39
+ return {i: [G.nodes[i]['geometry'].x, G.nodes[i]['geometry'].y] for i in G.nodes}
40
+
41
+ # Network nodes to 'GeoDataFrame'
42
+ def nodes_to_gdf(network, crs=None):
43
+ geom = []
44
+ node_id = []
45
+ for i in network.nodes:
46
+ geom.append(network.nodes[i]['geometry'])
47
+ node_id.append(i)
48
+ nodes = gpd.GeoDataFrame({'id':node_id, 'geometry':geom}, crs=crs)
49
+ return nodes
50
+
51
+ # Network edges to 'GeoDataFrame'
52
+ def edges_to_gdf(network, crs=None):
53
+ edges = nx.to_pandas_edgelist(network)
54
+ edges = gpd.GeoDataFrame(edges, crs=crs)
55
+ return edges
56
+
57
+ # Nearest node to given point
58
+ def nearest_node(G: nx.Graph, pnt: shapely.geometry.Point) -> tuple:
59
+ """
60
+ Find the nearest network node to the specified point
61
+
62
+ :param G: `networkx` Network
63
+ :param pnt: `shapely` geometry of type 'Point'
64
+ """
65
+ min_distance = float('inf')
66
+ nearest_node = None
67
+ for i in G.nodes:
68
+ distance = pnt.distance(G.nodes[i]['geometry'])
69
+ if distance < min_distance:
70
+ min_distance = distance
71
+ nearest_node = i
72
+ return nearest_node, min_distance
73
+
74
+ # Nearest edge to given point
75
+ def nearest_edge(G: nx.Graph, pnt: shapely.geometry.Point) -> tuple:
76
+ """
77
+ Find the nearest network edge to the specified point
78
+
79
+ :param G: `networkx` Network
80
+ :param pnt: `shapely` geometry of type 'Point'
81
+ """
82
+ min_distance = float('inf')
83
+ nearest_edge = None
84
+ for i in G.edges:
85
+ geom_edge = G.edges[i]['geometry']
86
+ distance = pnt.distance(geom_edge)
87
+ if distance < min_distance:
88
+ min_distance = distance
89
+ nearest_edge = i
90
+ return nearest_edge, min_distance
91
+
92
+ # Split edge by point
93
+ def split_edge(G, node_id, e, pnt_on_line, buffer_size):
94
+ pnt_on_line_b = pnt_on_line.buffer(buffer_size)
95
+ first_seg, buff_seg, last_seg = shapely.ops.split(G.edges[e]['geometry'], pnt_on_line_b).geoms
96
+ G.edges[e]['geometry'] = shapely.LineString(list(first_seg.coords) + list(pnt_on_line.coords) + list(last_seg.coords))
97
+ lines = shapely.ops.split(G.edges[e]['geometry'], pnt_on_line)
98
+ p = G.nodes[e[0]]['geometry']
99
+ line1 = filter(shapely.prepared.prep(p).intersects, lines.geoms)
100
+ line1 = list(line1)[0]
101
+ p = G.nodes[e[1]]['geometry']
102
+ line2 = filter(shapely.prepared.prep(p).intersects, lines.geoms)
103
+ line2 = list(line2)[0]
104
+ G.add_edge(e[0], node_id, geometry=line1, length=line1.length, time=G.edges[e]['time'] * (line1.length / G.edges[e]['geometry'].length))
105
+ G.add_edge(node_id, e[1], geometry=line2, length=line2.length, time=G.edges[e]['time'] * (line2.length / G.edges[e]['geometry'].length))
106
+ G.remove_edge(*e)
107
+ return G
108
+ # Check if number is similar
109
+ def is_same(a, b, treshold=0.001):
110
+ return abs(a - b) < treshold
111
+ # Add node to network
112
+ def add_node(network, pnt, buffer_size=1e-8):
113
+ G = network.copy()
114
+ # Detect nearest edge
115
+ e, dist = nearest_edge(G, pnt)
116
+ # Detect nearest node (from within the nearest edge)
117
+ node_id, dist = nearest_node(G.subgraph(e), pnt)
118
+ # If 'pnt' is on edisting node -> return that node
119
+ if dist == 0:
120
+ return G, node_id, dist
121
+ # Detect nearest point on the edge
122
+ pnt_on_line = shapely.ops.nearest_points(G.edges[e]['geometry'], pnt)
123
+ pnt_on_line = pnt_on_line[0]
124
+ # If 'pnt_on_line' is on edisting node -> return that node
125
+ if is_same(pnt_on_line.x, G.nodes[node_id]['geometry'].x) and is_same(pnt_on_line.y, G.nodes[node_id]['geometry'].y):
126
+ return G, node_id, dist
127
+ # Else - create new node
128
+ node_ids = [i for i in G.nodes if isinstance(i, (int, float))]
129
+ if(len(node_ids) > 0):
130
+ node_id = min(node_ids)-1
131
+ else:
132
+ node_id = -1
133
+ if node_id >= 0:
134
+ node_id = -1
135
+ G.add_node(node_id, geometry=pnt_on_line)
136
+ # Split edge
137
+ G = split_edge(G, node_id, e, pnt_on_line, buffer_size)
138
+ e = (e[1], e[0])
139
+ if e in G.edges and G.edges[e]['geometry'].intersects(pnt_on_line.buffer(buffer_size)):
140
+ G = split_edge(G, node_id, e, pnt_on_line, buffer_size)
141
+ return G, node_id, dist
142
+
143
+ # Shortest path 1 (between existing nodes)
144
+ def route1(network, node_start, node_end, weight):
145
+ try:
146
+ route = nx.shortest_path(network, node_start, node_end, weight)
147
+ weight_sum = nx.path_weight(network, route, weight=weight)
148
+ return {'route': route, 'weight': weight_sum}
149
+ except:
150
+ return {'route': np.nan, 'weight': np.nan}
151
+
152
+ # Shortest path 2 (inserting new nodes)
153
+ def route2(network, start, end, weight):
154
+ network, node_start, dist_start = add_node(network, start)
155
+ network, node_end, dist_end = add_node(network, end)
156
+ try:
157
+ route = nx.shortest_path(network, node_start, node_end, weight)
158
+ weight_sum = nx.path_weight(network, route, weight=weight)
159
+ return {'route': route, 'weight': weight_sum, 'dist_start': dist_start, 'dist_end': dist_end, 'network': network}
160
+ except:
161
+ return {'route': np.nan, 'weight': np.nan, 'dist_start': dist_start, 'dist_end': dist_end, 'network': network}
162
+
163
+ # Shortest path 3 (inserting new nodes + walk to nodes + comparison w/ walking mode)
164
+ def route3(network, start, end, time_weight, walking_speed=1.4): # m/s
165
+ import math
166
+ dist = math.sqrt(((start.x - end.x) ** 2) + ((start.y - end.y) ** 2))
167
+ time_walking = dist / walking_speed
168
+ try:
169
+ result = route2(network, start, end, time_weight)
170
+ time_driving_and_walking = result['dist_start']/walking_speed + result['weight'] + result['dist_end']/walking_speed
171
+ except:
172
+ return {'weight': np.nan}
173
+ if time_driving_and_walking <= time_walking:
174
+ return {'weight': time_driving_and_walking, 'mode': 'walking+driving'}
175
+ else:
176
+ return {'weight': time_walking, 'mode': 'walking'}
177
+
178
+ # Create regular grid
179
+ def create_grid(bounds, res, crs=None):
180
+ xmin, ymin, xmax, ymax = bounds
181
+ cols = list(np.arange(int(np.floor(xmin)), int(np.ceil(xmax+res)), res))
182
+ rows = list(np.arange(int(np.floor(ymin)), int(np.ceil(ymax+res)), res))
183
+ rows.reverse()
184
+ polygons = []
185
+ for x in cols:
186
+ for y in rows:
187
+ polygons.append(
188
+ shapely.Polygon([(x,y), (x+res, y), (x+res, y-res), (x, y-res)])
189
+ )
190
+ grid = gpd.GeoDataFrame({'geometry': polygons}, crs=crs)
191
+ sel = grid.intersects(shapely.box(*bounds))
192
+ grid = grid[sel]
193
+ return grid
194
+
195
+ # Route to 'GeoDataFrame'
196
+ def route_to_gdf(network, route, crs=None):
197
+ route_edges = nx.path_graph(route).edges
198
+ if len(route) == 1:
199
+ result = []
200
+ pnt = network.nodes[route[0]]['geometry']
201
+ line = shapely.LineString([pnt, pnt])
202
+ result = gpd.GeoDataFrame([{'from': route[0], 'to': route[0], 'geometry': line}], crs=crs)
203
+ if len(route) > 1:
204
+ result = []
205
+ for u,v in route_edges:
206
+ x = network.edges[u,v]['geometry']
207
+ result.append({'from': u, 'to': v, 'geometry': x})
208
+ result = gpd.GeoDataFrame(result, crs=crs)
209
+ return result
210
+