ObjectNat 1.1.0__py3-none-any.whl → 1.2.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.
Potentially problematic release.
This version of ObjectNat might be problematic. Click here for more details.
- objectnat/__init__.py +9 -13
- objectnat/_api.py +14 -13
- objectnat/_config.py +47 -47
- objectnat/_version.py +1 -1
- objectnat/methods/coverage_zones/__init__.py +3 -3
- objectnat/methods/coverage_zones/graph_coverage.py +98 -108
- objectnat/methods/coverage_zones/radius_voronoi_coverage.py +37 -45
- objectnat/methods/coverage_zones/stepped_coverage.py +126 -142
- objectnat/methods/isochrones/__init__.py +1 -1
- objectnat/methods/isochrones/isochrone_utils.py +167 -167
- objectnat/methods/isochrones/isochrones.py +262 -299
- objectnat/methods/noise/__init__.py +3 -3
- objectnat/methods/noise/noise_init_data.py +10 -10
- objectnat/methods/noise/noise_reduce.py +155 -155
- objectnat/methods/noise/{noise_sim.py → noise_simulation.py} +452 -448
- objectnat/methods/noise/noise_simulation_simplified.py +209 -0
- objectnat/methods/point_clustering/__init__.py +1 -1
- objectnat/methods/point_clustering/cluster_points_in_polygons.py +115 -116
- objectnat/methods/provision/__init__.py +1 -1
- objectnat/methods/provision/provision.py +117 -110
- objectnat/methods/provision/provision_exceptions.py +59 -59
- objectnat/methods/provision/provision_model.py +337 -337
- objectnat/methods/utils/__init__.py +1 -0
- objectnat/methods/utils/geom_utils.py +173 -130
- objectnat/methods/utils/graph_utils.py +306 -206
- objectnat/methods/utils/math_utils.py +32 -32
- objectnat/methods/visibility/__init__.py +6 -6
- objectnat/methods/visibility/visibility_analysis.py +470 -511
- {objectnat-1.1.0.dist-info → objectnat-1.2.1.dist-info}/LICENSE.txt +28 -28
- objectnat-1.2.1.dist-info/METADATA +115 -0
- objectnat-1.2.1.dist-info/RECORD +33 -0
- objectnat/methods/noise/noise_exceptions.py +0 -14
- objectnat-1.1.0.dist-info/METADATA +0 -148
- objectnat-1.1.0.dist-info/RECORD +0 -33
- {objectnat-1.1.0.dist-info → objectnat-1.2.1.dist-info}/WHEEL +0 -0
|
@@ -1,206 +1,306 @@
|
|
|
1
|
-
import geopandas as gpd
|
|
2
|
-
import networkx as nx
|
|
3
|
-
import numpy as np
|
|
4
|
-
import pandas as pd
|
|
5
|
-
from loguru import logger
|
|
6
|
-
from scipy.spatial import KDTree
|
|
7
|
-
from shapely import LineString
|
|
8
|
-
from shapely.geometry.point import Point
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def _edges_to_gdf(graph: nx.Graph, crs) -> gpd.GeoDataFrame:
|
|
12
|
-
"""
|
|
13
|
-
Converts nx graph to gpd.GeoDataFrame as edges.
|
|
14
|
-
"""
|
|
15
|
-
graph_df = pd.DataFrame(list(graph.edges(data=True)), columns=["u", "v", "data"])
|
|
16
|
-
edge_data_expanded = pd.json_normalize(graph_df["data"])
|
|
17
|
-
graph_df = pd.concat([graph_df.drop(columns=["data"]), edge_data_expanded], axis=1)
|
|
18
|
-
graph_df = gpd.GeoDataFrame(graph_df, geometry="geometry", crs=crs).set_index(["u", "v"])
|
|
19
|
-
graph_df["geometry"] = graph_df["geometry"].fillna(LineString())
|
|
20
|
-
return graph_df
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def _nodes_to_gdf(graph: nx.Graph, crs: int) -> gpd.GeoDataFrame:
|
|
24
|
-
"""
|
|
25
|
-
Converts nx graph to gpd.GeoDataFrame as nodes.
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
-
ind, data = zip(*graph.nodes(data=True))
|
|
29
|
-
node_geoms = (Point(d["x"], d["y"]) for d in data)
|
|
30
|
-
gdf_nodes = gpd.GeoDataFrame(data, index=ind, crs=crs, geometry=list(node_geoms))
|
|
31
|
-
|
|
32
|
-
return gdf_nodes
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def _restore_edges_geom(nodes_gdf, edges_gdf) -> gpd.GeoDataFrame:
|
|
36
|
-
edges_wout_geom = edges_gdf[edges_gdf["geometry"].is_empty].reset_index()
|
|
37
|
-
edges_wout_geom["geometry"] = [
|
|
38
|
-
LineString((s, e))
|
|
39
|
-
for s, e in zip(
|
|
40
|
-
nodes_gdf.loc[edges_wout_geom["u"], "geometry"], nodes_gdf.loc[edges_wout_geom["v"], "geometry"]
|
|
41
|
-
)
|
|
42
|
-
]
|
|
43
|
-
edges_wout_geom.set_index(["u", "v"], inplace=True)
|
|
44
|
-
edges_gdf.update(edges_wout_geom)
|
|
45
|
-
return edges_gdf
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def graph_to_gdf(
|
|
49
|
-
graph: nx.MultiDiGraph | nx.Graph | nx.DiGraph, edges: bool = True, nodes: bool = True, restore_edge_geom=False
|
|
50
|
-
) -> gpd.GeoDataFrame | tuple[gpd.GeoDataFrame, gpd.GeoDataFrame]:
|
|
51
|
-
"""
|
|
52
|
-
Converts nx graph to gpd.GeoDataFrame as edges.
|
|
53
|
-
|
|
54
|
-
Parameters
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
raise
|
|
73
|
-
if
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
"""
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
1
|
+
import geopandas as gpd
|
|
2
|
+
import networkx as nx
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from loguru import logger
|
|
6
|
+
from scipy.spatial import KDTree
|
|
7
|
+
from shapely import LineString, MultiLineString, line_merge, node
|
|
8
|
+
from shapely.geometry.point import Point
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _edges_to_gdf(graph: nx.Graph, crs) -> gpd.GeoDataFrame:
|
|
12
|
+
"""
|
|
13
|
+
Converts nx graph to gpd.GeoDataFrame as edges.
|
|
14
|
+
"""
|
|
15
|
+
graph_df = pd.DataFrame(list(graph.edges(data=True)), columns=["u", "v", "data"])
|
|
16
|
+
edge_data_expanded = pd.json_normalize(graph_df["data"])
|
|
17
|
+
graph_df = pd.concat([graph_df.drop(columns=["data"]), edge_data_expanded], axis=1)
|
|
18
|
+
graph_df = gpd.GeoDataFrame(graph_df, geometry="geometry", crs=crs).set_index(["u", "v"])
|
|
19
|
+
graph_df["geometry"] = graph_df["geometry"].fillna(LineString())
|
|
20
|
+
return graph_df
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _nodes_to_gdf(graph: nx.Graph, crs: int) -> gpd.GeoDataFrame:
|
|
24
|
+
"""
|
|
25
|
+
Converts nx graph to gpd.GeoDataFrame as nodes.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
ind, data = zip(*graph.nodes(data=True))
|
|
29
|
+
node_geoms = (Point(d["x"], d["y"]) for d in data)
|
|
30
|
+
gdf_nodes = gpd.GeoDataFrame(data, index=ind, crs=crs, geometry=list(node_geoms))
|
|
31
|
+
|
|
32
|
+
return gdf_nodes
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _restore_edges_geom(nodes_gdf, edges_gdf) -> gpd.GeoDataFrame:
|
|
36
|
+
edges_wout_geom = edges_gdf[edges_gdf["geometry"].is_empty].reset_index()
|
|
37
|
+
edges_wout_geom["geometry"] = [
|
|
38
|
+
LineString((s, e))
|
|
39
|
+
for s, e in zip(
|
|
40
|
+
nodes_gdf.loc[edges_wout_geom["u"], "geometry"], nodes_gdf.loc[edges_wout_geom["v"], "geometry"]
|
|
41
|
+
)
|
|
42
|
+
]
|
|
43
|
+
edges_wout_geom.set_index(["u", "v"], inplace=True)
|
|
44
|
+
edges_gdf.update(edges_wout_geom)
|
|
45
|
+
return edges_gdf
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def graph_to_gdf(
|
|
49
|
+
graph: nx.MultiDiGraph | nx.Graph | nx.DiGraph, edges: bool = True, nodes: bool = True, restore_edge_geom=False
|
|
50
|
+
) -> gpd.GeoDataFrame | tuple[gpd.GeoDataFrame, gpd.GeoDataFrame]:
|
|
51
|
+
"""
|
|
52
|
+
Converts nx graph to gpd.GeoDataFrame as edges.
|
|
53
|
+
|
|
54
|
+
Parameters:
|
|
55
|
+
graph (nx.MultiDiGraph):
|
|
56
|
+
The graph to convert.
|
|
57
|
+
edges (bool):
|
|
58
|
+
Keep edges in GoeDataFrame.
|
|
59
|
+
nodes (bool):
|
|
60
|
+
Keep nodes in GoeDataFrame.
|
|
61
|
+
restore_edge_geom (bool):
|
|
62
|
+
if True, will try to restore edge geometry from nodes.
|
|
63
|
+
Returns:
|
|
64
|
+
(gpd.GeoDataFrame | tuple[gpd.GeoDataFrame, gpd.GeoDataFrame]):
|
|
65
|
+
Graph representation in GeoDataFrame format, either nodes or nodes,or tuple of them nodes,edges.
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
crs = graph.graph["crs"]
|
|
69
|
+
except KeyError as exc:
|
|
70
|
+
raise ValueError("Graph does not have crs attribute") from exc
|
|
71
|
+
if not edges and not nodes:
|
|
72
|
+
raise AttributeError("Neither edges or nodes were selected")
|
|
73
|
+
if nodes and not edges:
|
|
74
|
+
nodes_gdf = _nodes_to_gdf(graph, crs)
|
|
75
|
+
return nodes_gdf
|
|
76
|
+
if not nodes and edges:
|
|
77
|
+
edges_gdf = _edges_to_gdf(graph, crs)
|
|
78
|
+
if restore_edge_geom:
|
|
79
|
+
nodes_gdf = _nodes_to_gdf(graph, crs)
|
|
80
|
+
edges_gdf = _restore_edges_geom(nodes_gdf, edges_gdf)
|
|
81
|
+
return edges_gdf
|
|
82
|
+
|
|
83
|
+
nodes_gdf = _nodes_to_gdf(graph, crs)
|
|
84
|
+
edges_gdf = _edges_to_gdf(graph, crs)
|
|
85
|
+
if restore_edge_geom:
|
|
86
|
+
edges_gdf = _restore_edges_geom(nodes_gdf, edges_gdf)
|
|
87
|
+
return nodes_gdf, edges_gdf
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def gdf_to_graph(
|
|
91
|
+
gdf: gpd.GeoDataFrame, project_gdf_attr=True, reproject_to_utm_crs=True, speed=5, check_intersections=True
|
|
92
|
+
) -> nx.DiGraph:
|
|
93
|
+
"""
|
|
94
|
+
Converts a GeoDataFrame of LineStrings into a directed graph (nx.DiGraph).
|
|
95
|
+
|
|
96
|
+
This function transforms a set of linear geometries (which may or may not form a planar graph)
|
|
97
|
+
into a directed graph where each edge corresponds to a LineString (or its segment) from the GeoDataFrame.
|
|
98
|
+
Intersections are optionally checked and merged. Attributes from the original GeoDataFrame
|
|
99
|
+
can be projected onto the graph edges using spatial matching.
|
|
100
|
+
|
|
101
|
+
Parameters:
|
|
102
|
+
gdf (gpd.GeoDataFrame): A GeoDataFrame containing at least one LineString geometry.
|
|
103
|
+
project_gdf_attr (bool): If True, attributes from the input GeoDataFrame will be spatially
|
|
104
|
+
projected onto the resulting graph edges. This can be an expensive operation for large datasets.
|
|
105
|
+
reproject_to_utm_crs (bool): If True, reprojects the GeoDataFrame to the estimated local UTM CRS to ensure
|
|
106
|
+
accurate edge length calculations in meters. If False, edge lengths are still computed in UTM CRS,
|
|
107
|
+
but the final graph will remain in the original CRS of the input GeoDataFrame.
|
|
108
|
+
speed (float): Assumed travel speed in km/h used to compute edge traversal time (in minutes).
|
|
109
|
+
check_intersections (bool): If True, merges geometries to ensure topological correctness.
|
|
110
|
+
Can be disabled if the input geometries already form a proper planar graph with no unintended intersections.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
(nx.DiGraph): A directed graph where each edge corresponds to a line segment from the input GeoDataFrame.
|
|
114
|
+
Edge attributes include geometry, length in meters, travel time (in minutes), and any additional projected
|
|
115
|
+
attributes from the original GeoDataFrame.
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
ValueError: If the input GeoDataFrame contains no valid LineStrings.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
def unique_list(agg_vals):
|
|
122
|
+
agg_vals = list(set(agg_vals.dropna()))
|
|
123
|
+
if len(agg_vals) == 1:
|
|
124
|
+
return agg_vals[0]
|
|
125
|
+
return agg_vals
|
|
126
|
+
|
|
127
|
+
original_crs = gdf.crs
|
|
128
|
+
gdf = gdf.to_crs(gdf.estimate_utm_crs())
|
|
129
|
+
|
|
130
|
+
gdf = gdf.explode(ignore_index=True)
|
|
131
|
+
gdf = gdf[gdf.geom_type == "LineString"]
|
|
132
|
+
|
|
133
|
+
if len(gdf) == 0:
|
|
134
|
+
raise ValueError("Provided GeoDataFrame contains no valid LineStrings")
|
|
135
|
+
|
|
136
|
+
if check_intersections:
|
|
137
|
+
lines = line_merge(node(MultiLineString(gdf.geometry.to_list())))
|
|
138
|
+
else:
|
|
139
|
+
lines = line_merge(MultiLineString(gdf.geometry.to_list()))
|
|
140
|
+
|
|
141
|
+
lines = gpd.GeoDataFrame(geometry=list(lines.geoms), crs=gdf.crs)
|
|
142
|
+
|
|
143
|
+
if len(gdf.columns) > 1 and project_gdf_attr:
|
|
144
|
+
lines_centroids = lines.copy()
|
|
145
|
+
lines_centroids.geometry = lines_centroids.apply(
|
|
146
|
+
lambda row: row.geometry.line_interpolate_point(row.geometry.length / 2), axis=1
|
|
147
|
+
).buffer(0.05, resolution=2)
|
|
148
|
+
lines_with_attrs = gpd.sjoin(lines_centroids, gdf, how="left", predicate="intersects")
|
|
149
|
+
aggregated_attrs = (
|
|
150
|
+
lines_with_attrs.drop(columns=["geometry", "index_right"]) # убрать геометрию буфера
|
|
151
|
+
.groupby(lines_with_attrs.index)
|
|
152
|
+
.agg(unique_list)
|
|
153
|
+
)
|
|
154
|
+
lines = pd.concat([lines, aggregated_attrs], axis=1)
|
|
155
|
+
|
|
156
|
+
lines["length_meter"] = np.round(lines.length, 2)
|
|
157
|
+
if not reproject_to_utm_crs:
|
|
158
|
+
lines = lines.to_crs(original_crs)
|
|
159
|
+
|
|
160
|
+
coords = lines.geometry.get_coordinates()
|
|
161
|
+
coords_grouped_by_index = coords.reset_index(names="old_index").groupby("old_index")
|
|
162
|
+
start_coords = coords_grouped_by_index.head(1).apply(lambda a: (a.x, a.y), axis=1).rename("start")
|
|
163
|
+
end_coords = coords_grouped_by_index.tail(1).apply(lambda a: (a.x, a.y), axis=1).rename("end")
|
|
164
|
+
coords = pd.concat([start_coords.reset_index(), end_coords.reset_index()], axis=1)[["start", "end"]]
|
|
165
|
+
lines = pd.concat([lines, coords], axis=1)
|
|
166
|
+
unique_coords = pd.concat([coords["start"], coords["end"]], ignore_index=True).unique()
|
|
167
|
+
coord_to_index = {coord: idx for idx, coord in enumerate(unique_coords)}
|
|
168
|
+
|
|
169
|
+
lines["u"] = lines["start"].map(coord_to_index)
|
|
170
|
+
lines["v"] = lines["end"].map(coord_to_index)
|
|
171
|
+
|
|
172
|
+
speed = speed * 1000 / 60
|
|
173
|
+
lines["time_min"] = np.round(lines["length_meter"] / speed, 2)
|
|
174
|
+
|
|
175
|
+
graph = nx.Graph()
|
|
176
|
+
for coords, node_id in coord_to_index.items():
|
|
177
|
+
x, y = coords
|
|
178
|
+
graph.add_node(node_id, x=float(x), y=float(y))
|
|
179
|
+
|
|
180
|
+
columns_to_attr = lines.columns.difference(["start", "end", "u", "v"])
|
|
181
|
+
for _, row in lines.iterrows():
|
|
182
|
+
edge_attrs = {}
|
|
183
|
+
for col in columns_to_attr:
|
|
184
|
+
edge_attrs[col] = row[col]
|
|
185
|
+
graph.add_edge(row.u, row.v, **edge_attrs)
|
|
186
|
+
|
|
187
|
+
graph.graph["crs"] = lines.crs
|
|
188
|
+
graph.graph["speed m/min"] = speed
|
|
189
|
+
return nx.DiGraph(graph)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def get_closest_nodes_from_gdf(gdf: gpd.GeoDataFrame, nx_graph: nx.Graph) -> tuple:
|
|
193
|
+
"""
|
|
194
|
+
Finds the closest graph nodes to the geometries in a GeoDataFrame.
|
|
195
|
+
|
|
196
|
+
Parameters
|
|
197
|
+
----------
|
|
198
|
+
gdf : gpd.GeoDataFrame
|
|
199
|
+
GeoDataFrame with geometries for which the nearest graph nodes will be found.
|
|
200
|
+
nx_graph : nx.Graph
|
|
201
|
+
A NetworkX graph where nodes have 'x' and 'y' attributes (coordinates).
|
|
202
|
+
|
|
203
|
+
Returns
|
|
204
|
+
-------
|
|
205
|
+
tuple
|
|
206
|
+
A tuple of (distances, nearest_nodes), where:
|
|
207
|
+
- distances: List of distances from each geometry to the nearest node.
|
|
208
|
+
- nearest_nodes: List of node IDs closest to each geometry in the input GeoDataFrame.
|
|
209
|
+
|
|
210
|
+
Raises
|
|
211
|
+
------
|
|
212
|
+
ValueError
|
|
213
|
+
If any node in the graph is missing 'x' or 'y' attributes.
|
|
214
|
+
"""
|
|
215
|
+
nodes_with_data = list(nx_graph.nodes(data=True))
|
|
216
|
+
try:
|
|
217
|
+
coordinates = np.array([(data["x"], data["y"]) for node, data in nodes_with_data])
|
|
218
|
+
except KeyError as e:
|
|
219
|
+
raise ValueError("Graph does not have coordinates attribute") from e
|
|
220
|
+
tree = KDTree(coordinates)
|
|
221
|
+
target_coord = [(p.x, p.y) for p in gdf.representative_point()]
|
|
222
|
+
distances, indices = tree.query(target_coord)
|
|
223
|
+
nearest_nodes = [nodes_with_data[idx][0] for idx in indices]
|
|
224
|
+
return distances, nearest_nodes
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def remove_weakly_connected_nodes(graph: nx.DiGraph) -> nx.DiGraph:
|
|
228
|
+
"""
|
|
229
|
+
Removes all nodes that are not part of the largest strongly connected component in the graph.
|
|
230
|
+
|
|
231
|
+
Parameters
|
|
232
|
+
----------
|
|
233
|
+
graph : nx.DiGraph
|
|
234
|
+
A directed NetworkX graph.
|
|
235
|
+
|
|
236
|
+
Returns
|
|
237
|
+
-------
|
|
238
|
+
nx.DiGraph
|
|
239
|
+
A new graph with only the largest strongly connected component retained.
|
|
240
|
+
|
|
241
|
+
Notes
|
|
242
|
+
-----
|
|
243
|
+
- Also logs a warning if multiple weakly connected components are detected.
|
|
244
|
+
- Logs the number of nodes removed and size of the remaining component.
|
|
245
|
+
"""
|
|
246
|
+
graph = graph.copy()
|
|
247
|
+
|
|
248
|
+
weakly_connected_components = list(nx.weakly_connected_components(graph))
|
|
249
|
+
if len(weakly_connected_components) > 1:
|
|
250
|
+
logger.warning(
|
|
251
|
+
f"Found {len(weakly_connected_components)} disconnected subgraphs in the network. "
|
|
252
|
+
f"These are isolated groups of nodes with no connections between them. "
|
|
253
|
+
f"Size of components: {[len(c) for c in weakly_connected_components]}"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
all_scc = sorted(nx.strongly_connected_components(graph), key=len)
|
|
257
|
+
nodes_to_del = set().union(*all_scc[:-1])
|
|
258
|
+
|
|
259
|
+
if nodes_to_del:
|
|
260
|
+
logger.warning(
|
|
261
|
+
f"Removing {len(nodes_to_del)} nodes that form {len(all_scc) - 1} trap components. "
|
|
262
|
+
f"These are groups where you can enter but can't exit (or vice versa). "
|
|
263
|
+
f"Keeping the largest strongly connected component ({len(all_scc[-1])} nodes)."
|
|
264
|
+
)
|
|
265
|
+
graph.remove_nodes_from(nodes_to_del)
|
|
266
|
+
|
|
267
|
+
return graph
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def reverse_graph(nx_graph: nx.Graph, weight: str) -> tuple[nx.Graph, nx.DiGraph]:
|
|
271
|
+
"""
|
|
272
|
+
Generate a reversed version of a directed or weighted graph.
|
|
273
|
+
|
|
274
|
+
If the input graph is undirected, the original graph is returned as-is.
|
|
275
|
+
For directed graphs, the function returns a new graph with all edge directions reversed,
|
|
276
|
+
preserving the specified edge weight.
|
|
277
|
+
|
|
278
|
+
Parameters
|
|
279
|
+
----------
|
|
280
|
+
nx_graph : nx.Graph
|
|
281
|
+
Input NetworkX graph (can be directed or undirected).
|
|
282
|
+
weight : str
|
|
283
|
+
Name of the edge attribute to use as weight in graph conversion.
|
|
284
|
+
|
|
285
|
+
Returns
|
|
286
|
+
-------
|
|
287
|
+
tuple[nx.Graph, nx.DiGraph]
|
|
288
|
+
A tuple containing:
|
|
289
|
+
- normalized_graph: Original graph with relabeled nodes (if needed)
|
|
290
|
+
- reversed_graph: Directed graph with reversed edges and preserved weights
|
|
291
|
+
"""
|
|
292
|
+
|
|
293
|
+
if nx_graph.is_multigraph():
|
|
294
|
+
nx_graph = nx.DiGraph(nx_graph) if nx_graph.is_directed() else nx.Graph(nx_graph)
|
|
295
|
+
if not nx_graph.is_multigraph() and not nx_graph.is_directed():
|
|
296
|
+
return nx_graph, nx_graph
|
|
297
|
+
|
|
298
|
+
nx_graph = remove_weakly_connected_nodes(nx_graph)
|
|
299
|
+
|
|
300
|
+
mapping = {old_label: new_label for new_label, old_label in enumerate(nx_graph.nodes())}
|
|
301
|
+
nx_graph = nx.relabel_nodes(nx_graph, mapping)
|
|
302
|
+
|
|
303
|
+
sparse_matrix = nx.to_scipy_sparse_array(nx_graph, weight=weight)
|
|
304
|
+
transposed_matrix = sparse_matrix.transpose()
|
|
305
|
+
reversed_graph = nx.from_scipy_sparse_array(transposed_matrix, edge_attribute=weight, create_using=type(nx_graph))
|
|
306
|
+
return nx_graph, reversed_graph
|
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def min_max_normalization(data, new_min=0, new_max=1):
|
|
5
|
-
"""
|
|
6
|
-
Min-max normalization for a given array of data.
|
|
7
|
-
|
|
8
|
-
Parameters
|
|
9
|
-
----------
|
|
10
|
-
data: numpy.ndarray
|
|
11
|
-
Input data to be normalized.
|
|
12
|
-
new_min: float, optional
|
|
13
|
-
New minimum value for normalization. Defaults to 0.
|
|
14
|
-
new_max: float, optional
|
|
15
|
-
New maximum value for normalization. Defaults to 1.
|
|
16
|
-
|
|
17
|
-
Returns
|
|
18
|
-
-------
|
|
19
|
-
numpy.ndarray
|
|
20
|
-
Normalized data.
|
|
21
|
-
|
|
22
|
-
Examples
|
|
23
|
-
--------
|
|
24
|
-
>>> import numpy as np
|
|
25
|
-
>>> data = np.array([1, 2, 3, 4, 5])
|
|
26
|
-
>>> normalized_data = min_max_normalization(data, new_min=0, new_max=1)
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
|
-
min_value = np.min(data)
|
|
30
|
-
max_value = np.max(data)
|
|
31
|
-
normalized_data = (data - min_value) / (max_value - min_value) * (new_max - new_min) + new_min
|
|
32
|
-
return normalized_data
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def min_max_normalization(data, new_min=0, new_max=1):
|
|
5
|
+
"""
|
|
6
|
+
Min-max normalization for a given array of data.
|
|
7
|
+
|
|
8
|
+
Parameters
|
|
9
|
+
----------
|
|
10
|
+
data: numpy.ndarray
|
|
11
|
+
Input data to be normalized.
|
|
12
|
+
new_min: float, optional
|
|
13
|
+
New minimum value for normalization. Defaults to 0.
|
|
14
|
+
new_max: float, optional
|
|
15
|
+
New maximum value for normalization. Defaults to 1.
|
|
16
|
+
|
|
17
|
+
Returns
|
|
18
|
+
-------
|
|
19
|
+
numpy.ndarray
|
|
20
|
+
Normalized data.
|
|
21
|
+
|
|
22
|
+
Examples
|
|
23
|
+
--------
|
|
24
|
+
>>> import numpy as np
|
|
25
|
+
>>> data = np.array([1, 2, 3, 4, 5])
|
|
26
|
+
>>> normalized_data = min_max_normalization(data, new_min=0, new_max=1)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
min_value = np.min(data)
|
|
30
|
+
max_value = np.max(data)
|
|
31
|
+
normalized_data = (data - min_value) / (max_value - min_value) * (new_max - new_min) + new_min
|
|
32
|
+
return normalized_data
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
from .visibility_analysis import (
|
|
2
|
-
calculate_visibility_catchment_area,
|
|
3
|
-
get_visibilities_from_points,
|
|
4
|
-
get_visibility,
|
|
5
|
-
get_visibility_accurate,
|
|
6
|
-
)
|
|
1
|
+
from .visibility_analysis import (
|
|
2
|
+
calculate_visibility_catchment_area,
|
|
3
|
+
get_visibilities_from_points,
|
|
4
|
+
get_visibility,
|
|
5
|
+
get_visibility_accurate,
|
|
6
|
+
)
|