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.

Files changed (35) hide show
  1. objectnat/__init__.py +9 -13
  2. objectnat/_api.py +14 -13
  3. objectnat/_config.py +47 -47
  4. objectnat/_version.py +1 -1
  5. objectnat/methods/coverage_zones/__init__.py +3 -3
  6. objectnat/methods/coverage_zones/graph_coverage.py +98 -108
  7. objectnat/methods/coverage_zones/radius_voronoi_coverage.py +37 -45
  8. objectnat/methods/coverage_zones/stepped_coverage.py +126 -142
  9. objectnat/methods/isochrones/__init__.py +1 -1
  10. objectnat/methods/isochrones/isochrone_utils.py +167 -167
  11. objectnat/methods/isochrones/isochrones.py +262 -299
  12. objectnat/methods/noise/__init__.py +3 -3
  13. objectnat/methods/noise/noise_init_data.py +10 -10
  14. objectnat/methods/noise/noise_reduce.py +155 -155
  15. objectnat/methods/noise/{noise_sim.py → noise_simulation.py} +452 -448
  16. objectnat/methods/noise/noise_simulation_simplified.py +209 -0
  17. objectnat/methods/point_clustering/__init__.py +1 -1
  18. objectnat/methods/point_clustering/cluster_points_in_polygons.py +115 -116
  19. objectnat/methods/provision/__init__.py +1 -1
  20. objectnat/methods/provision/provision.py +117 -110
  21. objectnat/methods/provision/provision_exceptions.py +59 -59
  22. objectnat/methods/provision/provision_model.py +337 -337
  23. objectnat/methods/utils/__init__.py +1 -0
  24. objectnat/methods/utils/geom_utils.py +173 -130
  25. objectnat/methods/utils/graph_utils.py +306 -206
  26. objectnat/methods/utils/math_utils.py +32 -32
  27. objectnat/methods/visibility/__init__.py +6 -6
  28. objectnat/methods/visibility/visibility_analysis.py +470 -511
  29. {objectnat-1.1.0.dist-info → objectnat-1.2.1.dist-info}/LICENSE.txt +28 -28
  30. objectnat-1.2.1.dist-info/METADATA +115 -0
  31. objectnat-1.2.1.dist-info/RECORD +33 -0
  32. objectnat/methods/noise/noise_exceptions.py +0 -14
  33. objectnat-1.1.0.dist-info/METADATA +0 -148
  34. objectnat-1.1.0.dist-info/RECORD +0 -33
  35. {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
- graph : nx.MultiDiGraph
57
- The graph to convert.
58
- edges: bool, default to True
59
- Keep edges in GoeDataFrame.
60
- nodes: bool, default to True
61
- Keep nodes in GoeDataFrame.
62
- restore_edge_geom: bool, default to False
63
- if True, will try to restore edge geometry from nodes.
64
- Returns
65
- -------
66
- gpd.GeoDataFrame | tuple[gpd.GeoDataFrame, gpd.GeoDataFrame]
67
- Graph representation in GeoDataFrame format, either nodes or nodes,or tuple of them nodes,edges.
68
- """
69
- try:
70
- crs = graph.graph["crs"]
71
- except KeyError as exc:
72
- raise ValueError("Graph does not have crs attribute") from exc
73
- if not edges and not nodes:
74
- raise AttributeError("Neither edges or nodes were selected")
75
- if nodes and not edges:
76
- nodes_gdf = _nodes_to_gdf(graph, crs)
77
- return nodes_gdf
78
- if not nodes and edges:
79
- edges_gdf = _edges_to_gdf(graph, crs)
80
- if restore_edge_geom:
81
- nodes_gdf = _nodes_to_gdf(graph, crs)
82
- edges_gdf = _restore_edges_geom(nodes_gdf, edges_gdf)
83
- return edges_gdf
84
-
85
- nodes_gdf = _nodes_to_gdf(graph, crs)
86
- edges_gdf = _edges_to_gdf(graph, crs)
87
- if restore_edge_geom:
88
- edges_gdf = _restore_edges_geom(nodes_gdf, edges_gdf)
89
- return nodes_gdf, edges_gdf
90
-
91
-
92
- def get_closest_nodes_from_gdf(gdf: gpd.GeoDataFrame, nx_graph: nx.Graph) -> tuple:
93
- """
94
- Finds the closest graph nodes to the geometries in a GeoDataFrame.
95
-
96
- Parameters
97
- ----------
98
- gdf : gpd.GeoDataFrame
99
- GeoDataFrame with geometries for which the nearest graph nodes will be found.
100
- nx_graph : nx.Graph
101
- A NetworkX graph where nodes have 'x' and 'y' attributes (coordinates).
102
-
103
- Returns
104
- -------
105
- tuple
106
- A tuple of (distances, nearest_nodes), where:
107
- - distances: List of distances from each geometry to the nearest node.
108
- - nearest_nodes: List of node IDs closest to each geometry in the input GeoDataFrame.
109
-
110
- Raises
111
- ------
112
- ValueError
113
- If any node in the graph is missing 'x' or 'y' attributes.
114
- """
115
- nodes_with_data = list(nx_graph.nodes(data=True))
116
- try:
117
- coordinates = np.array([(data["x"], data["y"]) for node, data in nodes_with_data])
118
- except KeyError as e:
119
- raise ValueError("Graph does not have coordinates attribute") from e
120
- tree = KDTree(coordinates)
121
- target_coord = [(p.x, p.y) for p in gdf.representative_point()]
122
- distances, indices = tree.query(target_coord)
123
- nearest_nodes = [nodes_with_data[idx][0] for idx in indices]
124
- return distances, nearest_nodes
125
-
126
-
127
- def remove_weakly_connected_nodes(graph: nx.DiGraph) -> nx.DiGraph:
128
- """
129
- Removes all nodes that are not part of the largest strongly connected component in the graph.
130
-
131
- Parameters
132
- ----------
133
- graph : nx.DiGraph
134
- A directed NetworkX graph.
135
-
136
- Returns
137
- -------
138
- nx.DiGraph
139
- A new graph with only the largest strongly connected component retained.
140
-
141
- Notes
142
- -----
143
- - Also logs a warning if multiple weakly connected components are detected.
144
- - Logs the number of nodes removed and size of the remaining component.
145
- """
146
- graph = graph.copy()
147
-
148
- weakly_connected_components = list(nx.weakly_connected_components(graph))
149
- if len(weakly_connected_components) > 1:
150
- logger.warning(
151
- f"Found {len(weakly_connected_components)} disconnected subgraphs in the network. "
152
- f"These are isolated groups of nodes with no connections between them. "
153
- f"Size of components: {[len(c) for c in weakly_connected_components]}"
154
- )
155
-
156
- all_scc = sorted(nx.strongly_connected_components(graph), key=len)
157
- nodes_to_del = set().union(*all_scc[:-1])
158
-
159
- if nodes_to_del:
160
- logger.warning(
161
- f"Removing {len(nodes_to_del)} nodes that form {len(all_scc) - 1} trap components. "
162
- f"These are groups where you can enter but can't exit (or vice versa). "
163
- f"Keeping the largest strongly connected component ({len(all_scc[-1])} nodes)."
164
- )
165
- graph.remove_nodes_from(nodes_to_del)
166
-
167
- return graph
168
-
169
-
170
- def reverse_graph(nx_graph: nx.Graph, weight: str) -> tuple[nx.Graph, nx.DiGraph]:
171
- """
172
- Generate a reversed version of a directed or weighted graph.
173
-
174
- If the input graph is undirected, the original graph is returned as-is.
175
- For directed graphs, the function returns a new graph with all edge directions reversed,
176
- preserving the specified edge weight.
177
-
178
- Parameters
179
- ----------
180
- nx_graph : nx.Graph
181
- Input NetworkX graph (can be directed or undirected).
182
- weight : str
183
- Name of the edge attribute to use as weight in graph conversion.
184
-
185
- Returns
186
- -------
187
- tuple[nx.Graph, nx.DiGraph]
188
- A tuple containing:
189
- - normalized_graph: Original graph with relabeled nodes (if needed)
190
- - reversed_graph: Directed graph with reversed edges and preserved weights
191
- """
192
-
193
- if nx_graph.is_multigraph():
194
- nx_graph = nx.DiGraph(nx_graph) if nx_graph.is_directed() else nx.Graph(nx_graph)
195
- if not nx_graph.is_multigraph() and not nx_graph.is_directed():
196
- return nx_graph, nx_graph
197
-
198
- nx_graph = remove_weakly_connected_nodes(nx_graph)
199
-
200
- mapping = {old_label: new_label for new_label, old_label in enumerate(nx_graph.nodes())}
201
- nx_graph = nx.relabel_nodes(nx_graph, mapping)
202
-
203
- sparse_matrix = nx.to_scipy_sparse_array(nx_graph, weight=weight)
204
- transposed_matrix = sparse_matrix.transpose()
205
- reversed_graph = nx.from_scipy_sparse_array(transposed_matrix, edge_attribute=weight, create_using=type(nx_graph))
206
- return nx_graph, reversed_graph
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
+ )