ObjectNat 1.0.1__py3-none-any.whl → 1.2.0__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/_api.py +3 -2
- objectnat/_version.py +1 -1
- objectnat/methods/coverage_zones/__init__.py +2 -1
- objectnat/methods/coverage_zones/graph_coverage.py +8 -18
- objectnat/methods/coverage_zones/stepped_coverage.py +142 -0
- objectnat/methods/isochrones/isochrone_utils.py +37 -0
- objectnat/methods/isochrones/isochrones.py +3 -29
- objectnat/methods/noise/__init__.py +2 -1
- objectnat/methods/noise/{noise_sim.py → noise_simulation.py} +103 -86
- objectnat/methods/noise/noise_simulation_simplified.py +135 -0
- objectnat/methods/utils/__init__.py +1 -0
- objectnat/methods/utils/geom_utils.py +45 -2
- objectnat/methods/utils/graph_utils.py +194 -1
- objectnat/methods/visibility/visibility_analysis.py +2 -2
- {objectnat-1.0.1.dist-info → objectnat-1.2.0.dist-info}/METADATA +22 -16
- objectnat-1.2.0.dist-info/RECORD +34 -0
- objectnat-1.0.1.dist-info/RECORD +0 -32
- /objectnat/methods/coverage_zones/{radius_voronoi.py → radius_voronoi_coverage.py} +0 -0
- {objectnat-1.0.1.dist-info → objectnat-1.2.0.dist-info}/LICENSE.txt +0 -0
- {objectnat-1.0.1.dist-info → objectnat-1.2.0.dist-info}/WHEEL +0 -0
objectnat/_api.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# pylint: disable=unused-import,wildcard-import,unused-wildcard-import
|
|
2
2
|
|
|
3
|
-
from .methods.coverage_zones import get_graph_coverage, get_radius_coverage
|
|
3
|
+
from .methods.coverage_zones import get_graph_coverage, get_radius_coverage, get_stepped_graph_coverage
|
|
4
4
|
from .methods.isochrones import get_accessibility_isochrone_stepped, get_accessibility_isochrones
|
|
5
|
-
from .methods.noise import simulate_noise
|
|
5
|
+
from .methods.noise import calculate_simplified_noise_frame, simulate_noise
|
|
6
6
|
from .methods.point_clustering import get_clusters_polygon
|
|
7
7
|
from .methods.provision import clip_provision, get_service_provision, recalculate_links
|
|
8
|
+
from .methods.utils import gdf_to_graph, graph_to_gdf
|
|
8
9
|
from .methods.visibility import (
|
|
9
10
|
calculate_visibility_catchment_area,
|
|
10
11
|
get_visibilities_from_points,
|
objectnat/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
VERSION = "1.0
|
|
1
|
+
VERSION = "1.2.0"
|
|
@@ -6,11 +6,11 @@ import pandas as pd
|
|
|
6
6
|
from pyproj.exceptions import CRSError
|
|
7
7
|
from shapely import Point, concave_hull
|
|
8
8
|
|
|
9
|
-
from objectnat.methods.utils.graph_utils import get_closest_nodes_from_gdf,
|
|
9
|
+
from objectnat.methods.utils.graph_utils import get_closest_nodes_from_gdf, reverse_graph
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def get_graph_coverage(
|
|
13
|
-
|
|
13
|
+
gdf_to: gpd.GeoDataFrame,
|
|
14
14
|
nx_graph: nx.Graph,
|
|
15
15
|
weight_type: Literal["time_min", "length_meter"],
|
|
16
16
|
weight_value_cutoff: float = None,
|
|
@@ -29,8 +29,8 @@ def get_graph_coverage(
|
|
|
29
29
|
|
|
30
30
|
Parameters
|
|
31
31
|
----------
|
|
32
|
-
|
|
33
|
-
Source points
|
|
32
|
+
gdf_to : gpd.GeoDataFrame
|
|
33
|
+
Source points to which coverage is calculated.
|
|
34
34
|
nx_graph : nx.Graph
|
|
35
35
|
NetworkX graph representing the transportation network.
|
|
36
36
|
weight_type : Literal["time_min", "length_meter"]
|
|
@@ -58,29 +58,19 @@ def get_graph_coverage(
|
|
|
58
58
|
>>> graph = get_intermodal_graph(osm_id=1114252)
|
|
59
59
|
>>> coverage = get_graph_coverage(points, graph, "time_min", 15)
|
|
60
60
|
"""
|
|
61
|
-
original_crs =
|
|
61
|
+
original_crs = gdf_to.crs
|
|
62
62
|
try:
|
|
63
63
|
local_crs = nx_graph.graph["crs"]
|
|
64
64
|
except KeyError as exc:
|
|
65
65
|
raise ValueError("Graph does not have crs attribute") from exc
|
|
66
66
|
|
|
67
67
|
try:
|
|
68
|
-
points =
|
|
68
|
+
points = gdf_to.copy()
|
|
69
69
|
points.to_crs(local_crs, inplace=True)
|
|
70
70
|
except CRSError as e:
|
|
71
71
|
raise CRSError(f"Graph crs ({local_crs}) has invalid format.") from e
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
if nx_graph.is_directed():
|
|
75
|
-
nx_graph = nx.DiGraph(nx_graph)
|
|
76
|
-
else:
|
|
77
|
-
nx_graph = nx.Graph(nx_graph)
|
|
78
|
-
nx_graph = remove_weakly_connected_nodes(nx_graph)
|
|
79
|
-
sparse_matrix = nx.to_scipy_sparse_array(nx_graph, weight=weight_type)
|
|
80
|
-
transposed_matrix = sparse_matrix.transpose()
|
|
81
|
-
reversed_graph = nx.from_scipy_sparse_array(
|
|
82
|
-
transposed_matrix, edge_attribute=weight_type, create_using=type(nx_graph)
|
|
83
|
-
)
|
|
73
|
+
nx_graph, reversed_graph = reverse_graph(nx_graph, weight_type)
|
|
84
74
|
|
|
85
75
|
points.geometry = points.representative_point()
|
|
86
76
|
|
|
@@ -88,7 +78,7 @@ def get_graph_coverage(
|
|
|
88
78
|
|
|
89
79
|
points["nearest_node"] = nearest_nodes
|
|
90
80
|
|
|
91
|
-
|
|
81
|
+
nearest_paths = nx.multi_source_dijkstra_path(
|
|
92
82
|
reversed_graph, nearest_nodes, weight=weight_type, cutoff=weight_value_cutoff
|
|
93
83
|
)
|
|
94
84
|
reachable_nodes = list(nearest_paths.keys())
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
import geopandas as gpd
|
|
4
|
+
import networkx as nx
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from pyproj.exceptions import CRSError
|
|
8
|
+
from shapely import Point, concave_hull
|
|
9
|
+
|
|
10
|
+
from objectnat.methods.isochrones.isochrone_utils import create_separated_dist_polygons
|
|
11
|
+
from objectnat.methods.utils.graph_utils import get_closest_nodes_from_gdf, reverse_graph
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_stepped_graph_coverage(
|
|
15
|
+
gdf_to: gpd.GeoDataFrame,
|
|
16
|
+
nx_graph: nx.Graph,
|
|
17
|
+
weight_type: Literal["time_min", "length_meter"],
|
|
18
|
+
step_type: Literal["voronoi", "separate"],
|
|
19
|
+
weight_value_cutoff: float = None,
|
|
20
|
+
zone: gpd.GeoDataFrame = None,
|
|
21
|
+
step: float = None,
|
|
22
|
+
):
|
|
23
|
+
"""
|
|
24
|
+
Calculate stepped coverage zones from source points through a graph network using Dijkstra's algorithm
|
|
25
|
+
and Voronoi-based or buffer-based isochrone steps.
|
|
26
|
+
|
|
27
|
+
This function combines graph-based accessibility with stepped isochrone logic. It:
|
|
28
|
+
1. Finds nearest graph nodes for each input point
|
|
29
|
+
2. Computes reachability for increasing weights (e.g. time or distance) in defined steps
|
|
30
|
+
3. Generates Voronoi-based or separate buffer zones around network nodes
|
|
31
|
+
4. Aggregates zones into stepped coverage layers
|
|
32
|
+
5. Optionally clips results to a boundary zone
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
gdf_to : gpd.GeoDataFrame
|
|
37
|
+
Source points from which stepped coverage is calculated.
|
|
38
|
+
nx_graph : nx.Graph
|
|
39
|
+
NetworkX graph representing the transportation network.
|
|
40
|
+
weight_type : Literal["time_min", "length_meter"]
|
|
41
|
+
Type of edge weight to use for path calculation:
|
|
42
|
+
- "time_min": Edge travel time in minutes
|
|
43
|
+
- "length_meter": Edge length in meters
|
|
44
|
+
step_type : Literal["voronoi", "separate"]
|
|
45
|
+
Method for generating stepped zones:
|
|
46
|
+
- "voronoi": Stepped zones based on Voronoi polygons around graph nodes
|
|
47
|
+
- "separate": Independent buffer zones per step
|
|
48
|
+
weight_value_cutoff : float, optional
|
|
49
|
+
Maximum weight value (e.g., max travel time or distance) to limit the coverage extent.
|
|
50
|
+
zone : gpd.GeoDataFrame, optional
|
|
51
|
+
Optional boundary polygon to clip resulting stepped zones. If None, concave hull of reachable area is used.
|
|
52
|
+
step : float, optional
|
|
53
|
+
Step interval for coverage zone construction. Defaults to:
|
|
54
|
+
- 100 meters for distance-based weight
|
|
55
|
+
- 1 minute for time-based weight
|
|
56
|
+
|
|
57
|
+
Returns
|
|
58
|
+
-------
|
|
59
|
+
gpd.GeoDataFrame
|
|
60
|
+
GeoDataFrame with polygons representing stepped coverage zones for each input point, annotated by step range.
|
|
61
|
+
|
|
62
|
+
Notes
|
|
63
|
+
-----
|
|
64
|
+
- Input graph must have a valid CRS defined.
|
|
65
|
+
- MultiGraph or MultiDiGraph inputs will be simplified.
|
|
66
|
+
- Designed for accessibility and spatial equity analyses over multimodal networks.
|
|
67
|
+
|
|
68
|
+
Examples
|
|
69
|
+
--------
|
|
70
|
+
>>> from iduedu import get_intermodal_graph
|
|
71
|
+
>>> points = gpd.read_file('destinations.geojson')
|
|
72
|
+
>>> graph = get_intermodal_graph(osm_id=1114252)
|
|
73
|
+
>>> stepped_coverage = get_stepped_graph_coverage(
|
|
74
|
+
... points, graph, "time_min", step_type="voronoi", weight_value_cutoff=30, step=5
|
|
75
|
+
... )
|
|
76
|
+
>>> # Using buffer-style zones
|
|
77
|
+
>>> stepped_separate = get_stepped_graph_coverage(
|
|
78
|
+
... points, graph, "length_meter", step_type="separate", weight_value_cutoff=1000, step=200
|
|
79
|
+
... )
|
|
80
|
+
"""
|
|
81
|
+
if step is None:
|
|
82
|
+
if weight_type == "length_meter":
|
|
83
|
+
step = 100
|
|
84
|
+
else:
|
|
85
|
+
step = 1
|
|
86
|
+
original_crs = gdf_to.crs
|
|
87
|
+
try:
|
|
88
|
+
local_crs = nx_graph.graph["crs"]
|
|
89
|
+
except KeyError as exc:
|
|
90
|
+
raise ValueError("Graph does not have crs attribute") from exc
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
points = gdf_to.copy()
|
|
94
|
+
points.to_crs(local_crs, inplace=True)
|
|
95
|
+
except CRSError as e:
|
|
96
|
+
raise CRSError(f"Graph crs ({local_crs}) has invalid format.") from e
|
|
97
|
+
|
|
98
|
+
nx_graph, reversed_graph = reverse_graph(nx_graph, weight_type)
|
|
99
|
+
|
|
100
|
+
points.geometry = points.representative_point()
|
|
101
|
+
|
|
102
|
+
distances, nearest_nodes = get_closest_nodes_from_gdf(points, nx_graph)
|
|
103
|
+
|
|
104
|
+
points["nearest_node"] = nearest_nodes
|
|
105
|
+
points["distance"] = distances
|
|
106
|
+
|
|
107
|
+
dist = nx.multi_source_dijkstra_path_length(
|
|
108
|
+
reversed_graph, nearest_nodes, weight=weight_type, cutoff=weight_value_cutoff
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
graph_points = pd.DataFrame(
|
|
112
|
+
data=[{"node": node, "geometry": Point(data["x"], data["y"])} for node, data in nx_graph.nodes(data=True)]
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
nearest_nodes = pd.DataFrame.from_dict(dist, orient="index", columns=["dist"]).reset_index()
|
|
116
|
+
|
|
117
|
+
graph_nodes_gdf = gpd.GeoDataFrame(
|
|
118
|
+
graph_points.merge(nearest_nodes, left_on="node", right_on="index", how="left").reset_index(drop=True),
|
|
119
|
+
geometry="geometry",
|
|
120
|
+
crs=local_crs,
|
|
121
|
+
)
|
|
122
|
+
graph_nodes_gdf.drop(columns=["index", "node"], inplace=True)
|
|
123
|
+
if weight_value_cutoff is None:
|
|
124
|
+
weight_value_cutoff = max(nearest_nodes["dist"])
|
|
125
|
+
if step_type == "voronoi":
|
|
126
|
+
graph_nodes_gdf["dist"] = np.minimum(np.ceil(graph_nodes_gdf["dist"] / step) * step, weight_value_cutoff)
|
|
127
|
+
voronois = gpd.GeoDataFrame(geometry=graph_nodes_gdf.voronoi_polygons(), crs=local_crs)
|
|
128
|
+
zone_coverages = voronois.sjoin(graph_nodes_gdf).dissolve(by="dist", as_index=False, dropna=False)
|
|
129
|
+
zone_coverages = zone_coverages[["dist", "geometry"]].explode(ignore_index=True)
|
|
130
|
+
if zone is None:
|
|
131
|
+
zone = concave_hull(graph_nodes_gdf[~graph_nodes_gdf["node_to"].isna()].union_all(), ratio=0.5)
|
|
132
|
+
else:
|
|
133
|
+
zone = zone.to_crs(local_crs)
|
|
134
|
+
zone_coverages = zone_coverages.clip(zone).to_crs(original_crs)
|
|
135
|
+
else: # step_type == 'separate':
|
|
136
|
+
speed = 83.33 # TODO HARDCODED WALK SPEED
|
|
137
|
+
weight_value = weight_value_cutoff
|
|
138
|
+
zone_coverages = create_separated_dist_polygons(graph_nodes_gdf, weight_value, weight_type, step, speed)
|
|
139
|
+
if zone is not None:
|
|
140
|
+
zone = zone.to_crs(local_crs)
|
|
141
|
+
zone_coverages = zone_coverages.clip(zone).to_crs(original_crs)
|
|
142
|
+
return zone_coverages
|
|
@@ -5,8 +5,10 @@ import networkx as nx
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
import pandas as pd
|
|
7
7
|
from pyproj.exceptions import CRSError
|
|
8
|
+
from shapely.ops import polygonize
|
|
8
9
|
|
|
9
10
|
from objectnat import config
|
|
11
|
+
from objectnat.methods.utils.geom_utils import polygons_to_multilinestring
|
|
10
12
|
from objectnat.methods.utils.graph_utils import get_closest_nodes_from_gdf, remove_weakly_connected_nodes
|
|
11
13
|
|
|
12
14
|
logger = config.logger
|
|
@@ -128,3 +130,38 @@ def _create_isochrones_gdf(
|
|
|
128
130
|
isochrones["weight_type"] = weight_type
|
|
129
131
|
isochrones["weight_value"] = weight_value
|
|
130
132
|
return isochrones
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def create_separated_dist_polygons(
|
|
136
|
+
points: gpd.GeoDataFrame, weight_value, weight_type, step, speed
|
|
137
|
+
) -> gpd.GeoDataFrame:
|
|
138
|
+
points["dist"] = points["dist"].clip(lower=0.1)
|
|
139
|
+
steps = np.arange(0, weight_value + step, step)
|
|
140
|
+
if steps[-1] > weight_value:
|
|
141
|
+
steps[-1] = weight_value # Ensure last step doesn't exceed weight_value
|
|
142
|
+
for i in range(len(steps) - 1):
|
|
143
|
+
min_dist = steps[i]
|
|
144
|
+
max_dist = steps[i + 1]
|
|
145
|
+
nodes_in_step = points["dist"].between(min_dist, max_dist, inclusive="left")
|
|
146
|
+
nodes_in_step = nodes_in_step[nodes_in_step].index
|
|
147
|
+
if not nodes_in_step.empty:
|
|
148
|
+
buffer_size = (max_dist - points.loc[nodes_in_step, "dist"]) * 0.7
|
|
149
|
+
if weight_type == "time_min":
|
|
150
|
+
buffer_size = buffer_size * speed
|
|
151
|
+
points.loc[nodes_in_step, "buffer_size"] = buffer_size
|
|
152
|
+
points.geometry = points.geometry.buffer(points["buffer_size"])
|
|
153
|
+
points["dist"] = np.minimum(np.ceil(points["dist"] / step) * step, weight_value)
|
|
154
|
+
points = points.dissolve(by="dist", as_index=False)
|
|
155
|
+
polygons = gpd.GeoDataFrame(
|
|
156
|
+
geometry=list(polygonize(points.geometry.apply(polygons_to_multilinestring).union_all())),
|
|
157
|
+
crs=points.crs,
|
|
158
|
+
)
|
|
159
|
+
polygons_points = polygons.copy()
|
|
160
|
+
polygons_points.geometry = polygons.representative_point()
|
|
161
|
+
stepped_polygons = polygons_points.sjoin(points, predicate="within").reset_index()
|
|
162
|
+
stepped_polygons = stepped_polygons.groupby("index").agg({"dist": "mean"})
|
|
163
|
+
stepped_polygons["dist"] = np.minimum(np.floor(stepped_polygons["dist"] / step) * step, weight_value)
|
|
164
|
+
stepped_polygons["geometry"] = polygons
|
|
165
|
+
stepped_polygons = gpd.GeoDataFrame(stepped_polygons, geometry="geometry", crs=points.crs).reset_index(drop=True)
|
|
166
|
+
stepped_polygons = stepped_polygons.dissolve(by="dist", as_index=False).explode(ignore_index=True)
|
|
167
|
+
return stepped_polygons
|
|
@@ -3,7 +3,6 @@ from typing import Literal
|
|
|
3
3
|
import geopandas as gpd
|
|
4
4
|
import networkx as nx
|
|
5
5
|
import numpy as np
|
|
6
|
-
from shapely.ops import polygonize
|
|
7
6
|
|
|
8
7
|
from objectnat import config
|
|
9
8
|
from objectnat.methods.isochrones.isochrone_utils import (
|
|
@@ -12,8 +11,9 @@ from objectnat.methods.isochrones.isochrone_utils import (
|
|
|
12
11
|
_prepare_graph_and_nodes,
|
|
13
12
|
_process_pt_data,
|
|
14
13
|
_validate_inputs,
|
|
14
|
+
create_separated_dist_polygons,
|
|
15
15
|
)
|
|
16
|
-
from objectnat.methods.utils.geom_utils import
|
|
16
|
+
from objectnat.methods.utils.geom_utils import remove_inner_geom
|
|
17
17
|
from objectnat.methods.utils.graph_utils import graph_to_gdf
|
|
18
18
|
|
|
19
19
|
logger = config.logger
|
|
@@ -116,35 +116,9 @@ def get_accessibility_isochrone_stepped(
|
|
|
116
116
|
logger.info("Building isochrones geometry...")
|
|
117
117
|
nodes, edges = graph_to_gdf(subgraph)
|
|
118
118
|
nodes.loc[dist_matrix.columns, "dist"] = dist_matrix.iloc[0]
|
|
119
|
-
steps = np.arange(0, weight_value + step, step)
|
|
120
|
-
if steps[-1] > weight_value:
|
|
121
|
-
steps[-1] = weight_value # Ensure last step doesn't exceed weight_value
|
|
122
119
|
|
|
123
120
|
if isochrone_type == "separate":
|
|
124
|
-
|
|
125
|
-
min_dist = steps[i]
|
|
126
|
-
max_dist = steps[i + 1]
|
|
127
|
-
nodes_in_step = nodes["dist"].between(min_dist, max_dist, inclusive="left")
|
|
128
|
-
nodes_in_step = nodes_in_step[nodes_in_step].index
|
|
129
|
-
if not nodes_in_step.empty:
|
|
130
|
-
buffer_size = (max_dist - nodes.loc[nodes_in_step, "dist"]) * 0.7
|
|
131
|
-
if weight_type == "time_min":
|
|
132
|
-
buffer_size = buffer_size * speed
|
|
133
|
-
nodes.loc[nodes_in_step, "buffer_size"] = buffer_size
|
|
134
|
-
nodes.geometry = nodes.geometry.buffer(nodes["buffer_size"])
|
|
135
|
-
nodes["dist"] = np.round(nodes["dist"], 0)
|
|
136
|
-
nodes = nodes.dissolve(by="dist", as_index=False)
|
|
137
|
-
polygons = gpd.GeoDataFrame(
|
|
138
|
-
geometry=list(polygonize(nodes.geometry.apply(polygons_to_multilinestring).union_all())),
|
|
139
|
-
crs=local_crs,
|
|
140
|
-
)
|
|
141
|
-
polygons_points = polygons.copy()
|
|
142
|
-
polygons_points.geometry = polygons.representative_point()
|
|
143
|
-
|
|
144
|
-
stepped_iso = polygons_points.sjoin(nodes, predicate="within").reset_index()
|
|
145
|
-
stepped_iso = stepped_iso.groupby("index").agg({"dist": "mean"})
|
|
146
|
-
stepped_iso["geometry"] = polygons
|
|
147
|
-
stepped_iso = gpd.GeoDataFrame(stepped_iso, geometry="geometry", crs=local_crs).reset_index(drop=True)
|
|
121
|
+
stepped_iso = create_separated_dist_polygons(nodes, weight_value, weight_type, step, speed)
|
|
148
122
|
else:
|
|
149
123
|
if isochrone_type == "radius":
|
|
150
124
|
isochrone_geoms = _build_radius_isochrones(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
from .
|
|
1
|
+
from .noise_simulation import simulate_noise
|
|
2
2
|
from .noise_reduce import dist_to_target_db, green_noise_reduce_db
|
|
3
3
|
from .noise_exceptions import InvalidStepError
|
|
4
|
+
from .noise_simulation_simplified import calculate_simplified_noise_frame
|
|
@@ -13,6 +13,7 @@ from tqdm import tqdm
|
|
|
13
13
|
from objectnat import config
|
|
14
14
|
from objectnat.methods.noise.noise_exceptions import InvalidStepError
|
|
15
15
|
from objectnat.methods.noise.noise_reduce import dist_to_target_db, green_noise_reduce_db
|
|
16
|
+
from objectnat.methods.noise.noise_simulation_simplified import _eval_donuts_gdf
|
|
16
17
|
from objectnat.methods.utils.geom_utils import (
|
|
17
18
|
gdf_to_circle_zones_from_point,
|
|
18
19
|
get_point_from_a_thorough_b,
|
|
@@ -22,26 +23,33 @@ from objectnat.methods.visibility.visibility_analysis import get_visibility_accu
|
|
|
22
23
|
|
|
23
24
|
logger = config.logger
|
|
24
25
|
|
|
26
|
+
MAX_DB_VALUE = 194
|
|
27
|
+
|
|
25
28
|
|
|
26
29
|
def simulate_noise(
|
|
27
|
-
source_points: gpd.GeoDataFrame,
|
|
30
|
+
source_points: gpd.GeoDataFrame,
|
|
31
|
+
obstacles: gpd.GeoDataFrame,
|
|
32
|
+
source_noise_db: float = None,
|
|
33
|
+
geometric_mean_freq_hz: float = None,
|
|
34
|
+
**kwargs,
|
|
28
35
|
):
|
|
29
36
|
"""
|
|
30
37
|
Simulates noise propagation from a set of source points considering obstacles, trees, and environmental factors.
|
|
31
38
|
|
|
32
39
|
Args:
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
source_points (gpd.GeoDataFrame): A GeoDataFrame with one or more point geometries representing noise sources.
|
|
41
|
+
Optionally, it can include 'source_noise_db' and 'geometric_mean_freq_hz' columns for per-point simulation.
|
|
35
42
|
obstacles (gpd.GeoDataFrame): A GeoDataFrame representing obstacles in the environment. If a column with
|
|
36
43
|
sound absorption coefficients is present, its name should be provided in the `absorb_ratio_column` argument.
|
|
37
44
|
Missing values will be filled with the `standart_absorb_ratio`.
|
|
38
|
-
source_noise_db
|
|
39
|
-
used to measure sound intensity. A value of 20 dB represents a barely audible whisper,
|
|
40
|
-
is comparable to the noise of jet engines.
|
|
41
|
-
geometric_mean_freq_hz (float):
|
|
42
|
-
the sound wave's propagation and scattering in the presence of trees.
|
|
43
|
-
distances than higher frequencies.
|
|
44
|
-
|
|
45
|
+
source_noise_db (float, optional): Default noise level (dB) to use if not specified per-point. Decibels are
|
|
46
|
+
logarithmic units used to measure sound intensity. A value of 20 dB represents a barely audible whisper,
|
|
47
|
+
while 140 dB is comparable to the noise of jet engines.
|
|
48
|
+
geometric_mean_freq_hz (float, optional): Default frequency (Hz) to use if not specified per-point.
|
|
49
|
+
This parameter influences the sound wave's propagation and scattering in the presence of trees.
|
|
50
|
+
Lower frequencies travel longer distances than higher frequencies.
|
|
51
|
+
It's recommended to use values between 63 Hz and 8000 Hz; values outside this range will be clamped to the
|
|
52
|
+
nearest boundary for the sound absorption coefficient calculation.
|
|
45
53
|
|
|
46
54
|
Optional kwargs:
|
|
47
55
|
absorb_ratio_column (str, optional): The name of the column in the `obstacles` GeoDataFrame that contains the
|
|
@@ -88,12 +96,46 @@ def simulate_noise(
|
|
|
88
96
|
reflection_n = kwargs.get("reflection_n", 3)
|
|
89
97
|
dead_area_r = kwargs.get("dead_area_r", 5)
|
|
90
98
|
|
|
99
|
+
# Validate optional columns or default values
|
|
100
|
+
use_column_db = False
|
|
101
|
+
if "source_noise_db" in source_points.columns:
|
|
102
|
+
if (source_points["source_noise_db"] > MAX_DB_VALUE).any():
|
|
103
|
+
raise ValueError(
|
|
104
|
+
f"One or more values in 'source_noise_db' column exceed the physical limit of {MAX_DB_VALUE} dB."
|
|
105
|
+
)
|
|
106
|
+
if source_points["source_noise_db"].isnull().any():
|
|
107
|
+
raise ValueError(f"Column 'source_noise_db' contains missing (NaN) values")
|
|
108
|
+
use_column_db = True
|
|
109
|
+
|
|
110
|
+
use_column_freq = False
|
|
111
|
+
if "geometric_mean_freq_hz" in source_points.columns:
|
|
112
|
+
if source_points["geometric_mean_freq_hz"].isnull().any():
|
|
113
|
+
raise ValueError(f"Column 'geometric_mean_freq_hz' contains missing (NaN) values")
|
|
114
|
+
use_column_freq = True
|
|
115
|
+
|
|
116
|
+
if not use_column_db:
|
|
117
|
+
if source_noise_db is None:
|
|
118
|
+
raise ValueError(
|
|
119
|
+
"Either `source_noise_db` must be provided or the `source_points` must contain a 'source_noise_db' column."
|
|
120
|
+
)
|
|
121
|
+
if source_noise_db > MAX_DB_VALUE:
|
|
122
|
+
raise ValueError(
|
|
123
|
+
f"source_noise_db ({source_noise_db} dB) exceeds the physical limit of {MAX_DB_VALUE} dB in air."
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if not use_column_freq:
|
|
127
|
+
if geometric_mean_freq_hz is None:
|
|
128
|
+
raise ValueError(
|
|
129
|
+
"Either `geometric_mean_freq_hz` must be provided or the `source_points` must contain a 'geometric_mean_freq_hz' column."
|
|
130
|
+
)
|
|
131
|
+
if not use_column_db and not use_column_freq and len(source_points) > 1:
|
|
132
|
+
logger.warning(
|
|
133
|
+
"`source_noise_db` and `geometric_mean_freq_hz` will be used for all points. Per-point simulation parameters not found."
|
|
134
|
+
)
|
|
135
|
+
|
|
91
136
|
original_crs = source_points.crs
|
|
137
|
+
source_points = source_points.copy()
|
|
92
138
|
|
|
93
|
-
div_ = (source_noise_db - target_noise_db) % db_sim_step
|
|
94
|
-
if div_ != 0:
|
|
95
|
-
raise InvalidStepError(source_noise_db, target_noise_db, db_sim_step, div_)
|
|
96
|
-
# Choosing crs and simplifying obs if any
|
|
97
139
|
source_points = source_points.copy()
|
|
98
140
|
if len(obstacles) > 0:
|
|
99
141
|
obstacles = obstacles.copy()
|
|
@@ -118,63 +160,54 @@ def simulate_noise(
|
|
|
118
160
|
if absorb_ratio_column is None:
|
|
119
161
|
obstacles["absorb_ratio"] = standart_absorb_ratio
|
|
120
162
|
else:
|
|
121
|
-
obstacles["absorb_ratio"] = obstacles[absorb_ratio_column]
|
|
122
|
-
obstacles["absorb_ratio"] = obstacles["absorb_ratio"].fillna(standart_absorb_ratio)
|
|
163
|
+
obstacles["absorb_ratio"] = obstacles[absorb_ratio_column].fillna(standart_absorb_ratio)
|
|
123
164
|
obstacles = obstacles[["absorb_ratio", "geometry"]]
|
|
124
165
|
|
|
125
|
-
logger.info(
|
|
126
|
-
dist_to_target_db(
|
|
127
|
-
source_noise_db,
|
|
128
|
-
target_noise_db,
|
|
129
|
-
geometric_mean_freq_hz,
|
|
130
|
-
air_temperature,
|
|
131
|
-
return_desc=True,
|
|
132
|
-
check_temp_freq=True,
|
|
133
|
-
)
|
|
134
|
-
)
|
|
135
|
-
# calculating layer dist and db values
|
|
136
|
-
dist_db = [(0, source_noise_db)]
|
|
137
|
-
cur_db = source_noise_db - db_sim_step
|
|
138
|
-
while cur_db != target_noise_db - db_sim_step:
|
|
139
|
-
max_dist = dist_to_target_db(source_noise_db, cur_db, geometric_mean_freq_hz, air_temperature)
|
|
140
|
-
dist_db.append((max_dist, cur_db))
|
|
141
|
-
cur_db = cur_db - db_sim_step
|
|
142
|
-
|
|
143
166
|
# creating initial task and simulating for each point
|
|
144
|
-
|
|
167
|
+
task_queue = multiprocessing.Queue()
|
|
168
|
+
dead_area_dict = {}
|
|
145
169
|
for ind, row in source_points.iterrows():
|
|
146
|
-
logger.info(f"Started simulation for point {ind+1} / {len(source_points)}")
|
|
147
170
|
source_point = row.geometry
|
|
148
|
-
|
|
171
|
+
local_db = row["source_noise_db"] if use_column_db else source_noise_db
|
|
172
|
+
local_freq = row["geometric_mean_freq_hz"] if use_column_freq else geometric_mean_freq_hz
|
|
173
|
+
div_ = (local_db - target_noise_db) % db_sim_step
|
|
174
|
+
if div_ != 0:
|
|
175
|
+
raise InvalidStepError(local_db, target_noise_db, db_sim_step, div_)
|
|
176
|
+
# calculating layer dist and db values
|
|
177
|
+
dist_db = [(0, local_db)]
|
|
178
|
+
cur_db = local_db - db_sim_step
|
|
179
|
+
while cur_db != target_noise_db - db_sim_step:
|
|
180
|
+
max_dist = dist_to_target_db(local_db, cur_db, local_freq, air_temperature)
|
|
181
|
+
dist_db.append((max_dist, cur_db))
|
|
182
|
+
cur_db -= db_sim_step
|
|
183
|
+
|
|
149
184
|
args = (source_point, obstacles, trees, 0, 0, dist_db)
|
|
150
185
|
kwargs = {
|
|
151
186
|
"reflection_n": reflection_n,
|
|
152
|
-
"geometric_mean_freq_hz":
|
|
187
|
+
"geometric_mean_freq_hz": local_freq,
|
|
153
188
|
"tree_res": tree_res,
|
|
154
189
|
"min_db": target_noise_db,
|
|
190
|
+
"simulation_ind": ind,
|
|
155
191
|
}
|
|
156
192
|
task_queue.put((_noise_from_point_task, args, kwargs))
|
|
193
|
+
dead_area_dict[ind] = source_point.buffer(dead_area_r, resolution=2)
|
|
157
194
|
|
|
158
|
-
|
|
159
|
-
task_queue, dead_area=source_point.buffer(dead_area_r, resolution=2), dead_area_r=dead_area_r
|
|
160
|
-
)
|
|
195
|
+
noise_gdf = _parallel_split_queue(task_queue, dead_area_dict=dead_area_dict, dead_area_r=dead_area_r)
|
|
161
196
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
sim_result["source_point_ind"] = ind
|
|
175
|
-
all_p_res.append(sim_result)
|
|
197
|
+
noise_gdf = gpd.GeoDataFrame(pd.concat(noise_gdf, ignore_index=True), crs=local_crs)
|
|
198
|
+
polygons = gpd.GeoDataFrame(
|
|
199
|
+
geometry=list(polygonize(noise_gdf.geometry.apply(polygons_to_multilinestring).union_all())), crs=local_crs
|
|
200
|
+
)
|
|
201
|
+
polygons_points = polygons.copy()
|
|
202
|
+
polygons_points.geometry = polygons.representative_point()
|
|
203
|
+
sim_result = polygons_points.sjoin(noise_gdf, predicate="within").reset_index()
|
|
204
|
+
sim_result = sim_result.groupby("index").agg({"noise_level": "max"})
|
|
205
|
+
sim_result["geometry"] = polygons
|
|
206
|
+
sim_result = (
|
|
207
|
+
gpd.GeoDataFrame(sim_result, geometry="geometry", crs=local_crs).dissolve(by="noise_level").reset_index()
|
|
208
|
+
)
|
|
176
209
|
|
|
177
|
-
return
|
|
210
|
+
return sim_result.to_crs(original_crs)
|
|
178
211
|
|
|
179
212
|
|
|
180
213
|
def _noise_from_point_task(task, **kwargs) -> tuple[gpd.GeoDataFrame, list[tuple] | None]: # pragma: no cover
|
|
@@ -272,22 +305,7 @@ def _noise_from_point_task(task, **kwargs) -> tuple[gpd.GeoDataFrame, list[tuple
|
|
|
272
305
|
noise_reduce = int(round(green_noise_reduce_db(geometric_mean_freq_hz, r_tree_new)))
|
|
273
306
|
reduce_polygons.append((red_polygon, noise_reduce))
|
|
274
307
|
|
|
275
|
-
|
|
276
|
-
donuts = []
|
|
277
|
-
don_values = []
|
|
278
|
-
to_cut_off = point_from
|
|
279
|
-
for i in range(len(donuts_dist_values[:-1])):
|
|
280
|
-
cur_buffer = point_from.buffer(donuts_dist_values[i + 1][0])
|
|
281
|
-
donuts.append(cur_buffer.difference(to_cut_off))
|
|
282
|
-
don_values.append(donuts_dist_values[i][1])
|
|
283
|
-
to_cut_off = cur_buffer
|
|
284
|
-
|
|
285
|
-
noise_from_point = (
|
|
286
|
-
gpd.GeoDataFrame(geometry=donuts, data={"noise_level": don_values}, crs=local_crs)
|
|
287
|
-
.clip(vis_poly, keep_geom_type=True)
|
|
288
|
-
.explode(ignore_index=True)
|
|
289
|
-
)
|
|
290
|
-
|
|
308
|
+
noise_from_point = _eval_donuts_gdf(point_from, donuts_dist_values, local_crs, vis_poly)
|
|
291
309
|
# intersect noise poly with noise reduce
|
|
292
310
|
if len(reduce_polygons) > 0:
|
|
293
311
|
reduce_polygons = gpd.GeoDataFrame(
|
|
@@ -383,34 +401,34 @@ def _noise_from_point_task(task, **kwargs) -> tuple[gpd.GeoDataFrame, list[tuple
|
|
|
383
401
|
return noise_from_point, new_tasks
|
|
384
402
|
|
|
385
403
|
|
|
386
|
-
def _parallel_split_queue(task_queue: multiprocessing.Queue,
|
|
404
|
+
def _parallel_split_queue(task_queue: multiprocessing.Queue, dead_area_dict: dict, dead_area_r: int):
|
|
387
405
|
results = []
|
|
388
406
|
total_tasks = task_queue.qsize()
|
|
389
407
|
|
|
390
408
|
with tqdm(total=total_tasks, desc="Simulating noise") as pbar:
|
|
391
409
|
with concurrent.futures.ProcessPoolExecutor() as executor:
|
|
410
|
+
# with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
392
411
|
future_to_task = {}
|
|
393
412
|
while True:
|
|
394
413
|
while not task_queue.empty() and len(future_to_task) < executor._max_workers:
|
|
395
414
|
func, task, kwargs = task_queue.get_nowait()
|
|
396
415
|
future = executor.submit(func, task, **kwargs)
|
|
397
|
-
future_to_task[future] =
|
|
398
|
-
|
|
416
|
+
future_to_task[future] = kwargs["simulation_ind"]
|
|
399
417
|
done, _ = concurrent.futures.wait(future_to_task.keys(), return_when=concurrent.futures.FIRST_COMPLETED)
|
|
400
|
-
|
|
401
418
|
for future in done:
|
|
402
|
-
future_to_task.pop(future)
|
|
419
|
+
simulation_ind = future_to_task.pop(future)
|
|
403
420
|
result, new_tasks = future.result()
|
|
404
421
|
if new_tasks:
|
|
405
422
|
new_tasks_n = 0
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
423
|
+
local_dead_area = dead_area_dict.get(simulation_ind)
|
|
424
|
+
new_dead_area_points = [local_dead_area]
|
|
425
|
+
for func, new_task, new_kwargs in new_tasks:
|
|
426
|
+
new_point = new_task[0]
|
|
427
|
+
if not local_dead_area.covers(new_point):
|
|
428
|
+
task_queue.put((func, new_task, new_kwargs))
|
|
429
|
+
new_dead_area_points.append(new_point.buffer(dead_area_r, resolution=2))
|
|
430
|
+
new_tasks_n += 1
|
|
431
|
+
dead_area_dict[simulation_ind] = unary_union(new_dead_area_points)
|
|
414
432
|
total_tasks += new_tasks_n
|
|
415
433
|
pbar.total = total_tasks
|
|
416
434
|
pbar.refresh()
|
|
@@ -419,5 +437,4 @@ def _parallel_split_queue(task_queue: multiprocessing.Queue, dead_area: Polygon,
|
|
|
419
437
|
time.sleep(0.01)
|
|
420
438
|
if not future_to_task and task_queue.empty():
|
|
421
439
|
break
|
|
422
|
-
|
|
423
440
|
return results
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# simplified version
|
|
2
|
+
import geopandas as gpd
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from shapely.ops import polygonize, unary_union
|
|
5
|
+
|
|
6
|
+
from objectnat.methods.noise.noise_reduce import dist_to_target_db
|
|
7
|
+
from objectnat.methods.utils.geom_utils import (
|
|
8
|
+
distribute_points_on_linestrings,
|
|
9
|
+
distribute_points_on_polygons,
|
|
10
|
+
polygons_to_multilinestring,
|
|
11
|
+
)
|
|
12
|
+
from objectnat.methods.visibility.visibility_analysis import get_visibility_accurate
|
|
13
|
+
|
|
14
|
+
MAX_DB_VALUE = 194
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def calculate_simplified_noise_frame(
|
|
18
|
+
noise_sources: gpd.GeoDataFrame, obstacles: gpd.GeoDataFrame, air_temperature, **kwargs
|
|
19
|
+
) -> gpd.GeoDataFrame:
|
|
20
|
+
target_noise_db = kwargs.get("target_noise_db", 40)
|
|
21
|
+
db_sim_step = kwargs.get("db_sim_step", 5)
|
|
22
|
+
linestring_point_radius = kwargs.get("linestring_point_radius", 20)
|
|
23
|
+
polygon_point_radius = kwargs.get("polygon_point_radius", 10)
|
|
24
|
+
|
|
25
|
+
required_columns = ["source_noise_db", "geometric_mean_freq_hz"]
|
|
26
|
+
for col in required_columns:
|
|
27
|
+
if col not in noise_sources.columns:
|
|
28
|
+
raise ValueError(f"'{col}' column is missing in provided GeoDataFrame")
|
|
29
|
+
if noise_sources[col].isnull().any():
|
|
30
|
+
raise ValueError(f"Column '{col}' contains missing (NaN) values")
|
|
31
|
+
if (noise_sources["source_noise_db"] > MAX_DB_VALUE).any():
|
|
32
|
+
raise ValueError(
|
|
33
|
+
f"One or more values in 'source_noise_db' column exceed the physical limit of {MAX_DB_VALUE} dB."
|
|
34
|
+
)
|
|
35
|
+
original_crs = noise_sources.crs
|
|
36
|
+
if len(obstacles) > 0:
|
|
37
|
+
obstacles = obstacles.copy()
|
|
38
|
+
obstacles.geometry = obstacles.geometry.simplify(tolerance=1)
|
|
39
|
+
local_crs = obstacles.estimate_utm_crs()
|
|
40
|
+
obstacles.to_crs(local_crs, inplace=True)
|
|
41
|
+
noise_sources.to_crs(local_crs, inplace=True)
|
|
42
|
+
else:
|
|
43
|
+
local_crs = noise_sources.estimate_utm_crs()
|
|
44
|
+
noise_sources.to_crs(local_crs, inplace=True)
|
|
45
|
+
noise_sources.reset_index(drop=True)
|
|
46
|
+
|
|
47
|
+
noise_sources = noise_sources.explode(ignore_index=True)
|
|
48
|
+
noise_sources["geom_type"] = noise_sources.geom_type
|
|
49
|
+
|
|
50
|
+
grouped_sources = noise_sources.groupby(by=["source_noise_db", "geometric_mean_freq_hz", "geom_type"])
|
|
51
|
+
|
|
52
|
+
frame_result = []
|
|
53
|
+
|
|
54
|
+
for (source_db, freq_hz, geom_type), group_gdf in grouped_sources:
|
|
55
|
+
# calculating layer dist and db values
|
|
56
|
+
dist_db = [(0, source_db)]
|
|
57
|
+
cur_db = source_db - db_sim_step
|
|
58
|
+
max_dist = 0
|
|
59
|
+
while cur_db > target_noise_db - db_sim_step:
|
|
60
|
+
if cur_db - db_sim_step < target_noise_db:
|
|
61
|
+
cur_db = target_noise_db
|
|
62
|
+
max_dist = dist_to_target_db(source_db, cur_db, freq_hz, air_temperature)
|
|
63
|
+
dist_db.append((max_dist, cur_db))
|
|
64
|
+
cur_db -= db_sim_step
|
|
65
|
+
|
|
66
|
+
if geom_type == "Point":
|
|
67
|
+
for _, row in group_gdf.iterrows():
|
|
68
|
+
point_from = row.geometry
|
|
69
|
+
point_buffer = point_from.buffer(max_dist, resolution=16)
|
|
70
|
+
local_obstacles = obstacles[obstacles.intersects(point_buffer)]
|
|
71
|
+
vis_poly = get_visibility_accurate(point_from, obstacles=local_obstacles, view_distance=max_dist)
|
|
72
|
+
noise_from_feature = _eval_donuts_gdf(point_from, dist_db, local_crs, vis_poly)
|
|
73
|
+
frame_result.append(noise_from_feature)
|
|
74
|
+
|
|
75
|
+
elif geom_type == "LineString":
|
|
76
|
+
layer_points = distribute_points_on_linestrings(group_gdf, radius=linestring_point_radius, lloyd_relax_n=1)
|
|
77
|
+
noise_from_feature = _process_lines_or_polygons(
|
|
78
|
+
group_gdf, max_dist, obstacles, layer_points, dist_db, local_crs
|
|
79
|
+
)
|
|
80
|
+
frame_result.append(noise_from_feature)
|
|
81
|
+
elif geom_type == "Polygon":
|
|
82
|
+
group_gdf.geometry = group_gdf.buffer(0.1, resolution=1)
|
|
83
|
+
layer_points = distribute_points_on_polygons(
|
|
84
|
+
group_gdf, only_exterior=False, radius=polygon_point_radius, lloyd_relax_n=1
|
|
85
|
+
)
|
|
86
|
+
noise_from_feature = _process_lines_or_polygons(
|
|
87
|
+
group_gdf, max_dist, obstacles, layer_points, dist_db, local_crs
|
|
88
|
+
)
|
|
89
|
+
frame_result.append(noise_from_feature)
|
|
90
|
+
else:
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
noise_gdf = gpd.GeoDataFrame(pd.concat(frame_result, ignore_index=True), crs=local_crs)
|
|
94
|
+
polygons = gpd.GeoDataFrame(
|
|
95
|
+
geometry=list(polygonize(noise_gdf.geometry.apply(polygons_to_multilinestring).union_all())), crs=local_crs
|
|
96
|
+
)
|
|
97
|
+
polygons_points = polygons.copy()
|
|
98
|
+
polygons_points.geometry = polygons.representative_point()
|
|
99
|
+
sim_result = polygons_points.sjoin(noise_gdf, predicate="within").reset_index()
|
|
100
|
+
sim_result = sim_result.groupby("index").agg({"noise_level": "max"})
|
|
101
|
+
sim_result["geometry"] = polygons
|
|
102
|
+
sim_result = (
|
|
103
|
+
gpd.GeoDataFrame(sim_result, geometry="geometry", crs=local_crs).dissolve(by="noise_level").reset_index()
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return sim_result.to_crs(original_crs)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _process_lines_or_polygons(group_gdf, max_dist, obstacles, layer_points, dist_db, local_crs) -> gpd.GeoDataFrame:
|
|
110
|
+
features_vision_polys = []
|
|
111
|
+
layer_buffer = group_gdf.buffer(max_dist, resolution=16).union_all()
|
|
112
|
+
local_obstacles = obstacles[obstacles.intersects(layer_buffer)]
|
|
113
|
+
for _, row in layer_points.iterrows():
|
|
114
|
+
point_from = row.geometry
|
|
115
|
+
vis_poly = get_visibility_accurate(point_from, obstacles=local_obstacles, view_distance=max_dist)
|
|
116
|
+
features_vision_polys.append(vis_poly)
|
|
117
|
+
features_vision_polys = unary_union(features_vision_polys)
|
|
118
|
+
return _eval_donuts_gdf(group_gdf.union_all(), dist_db, local_crs, features_vision_polys)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _eval_donuts_gdf(initial_geometry, dist_db, local_crs, clip_poly) -> gpd.GeoDataFrame:
|
|
122
|
+
donuts = []
|
|
123
|
+
don_values = []
|
|
124
|
+
to_cut_off = initial_geometry
|
|
125
|
+
for i in range(len(dist_db[:-1])):
|
|
126
|
+
cur_buffer = initial_geometry.buffer(dist_db[i + 1][0])
|
|
127
|
+
donuts.append(cur_buffer.difference(to_cut_off))
|
|
128
|
+
don_values.append(dist_db[i][1])
|
|
129
|
+
to_cut_off = cur_buffer
|
|
130
|
+
noise_from_feature = (
|
|
131
|
+
gpd.GeoDataFrame(geometry=donuts, data={"noise_level": don_values}, crs=local_crs)
|
|
132
|
+
.clip(clip_poly, keep_geom_type=True)
|
|
133
|
+
.explode(ignore_index=True)
|
|
134
|
+
)
|
|
135
|
+
return noise_from_feature
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .graph_utils import gdf_to_graph, graph_to_gdf
|
|
@@ -15,9 +15,9 @@ def polygons_to_multilinestring(geom: Polygon | MultiPolygon):
|
|
|
15
15
|
|
|
16
16
|
def convert_polygon(polygon: Polygon):
|
|
17
17
|
lines = []
|
|
18
|
-
exterior = LineString(polygon.exterior
|
|
18
|
+
exterior = LineString(polygon.exterior)
|
|
19
19
|
lines.append(exterior)
|
|
20
|
-
interior = [LineString(p
|
|
20
|
+
interior = [LineString(p) for p in polygon.interiors]
|
|
21
21
|
lines = lines + interior
|
|
22
22
|
return lines
|
|
23
23
|
|
|
@@ -128,3 +128,46 @@ def combine_geometry(gdf: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
|
|
|
128
128
|
joined["geometry"] = enclosures
|
|
129
129
|
joined = gpd.GeoDataFrame(joined, geometry="geometry", crs=crs)
|
|
130
130
|
return joined
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def distribute_points_on_linestrings(lines: gpd.GeoDataFrame, radius, lloyd_relax_n=2) -> gpd.GeoDataFrame:
|
|
134
|
+
lines = lines.copy()
|
|
135
|
+
lines = lines.explode(ignore_index=True)
|
|
136
|
+
lines = lines[lines.geom_type == "LineString"]
|
|
137
|
+
original_crs = lines.crs
|
|
138
|
+
lines = lines.to_crs(crs=lines.estimate_utm_crs())
|
|
139
|
+
lines = lines.reset_index(drop=True)
|
|
140
|
+
lines = lines[["geometry"]]
|
|
141
|
+
radius = radius * 1.1
|
|
142
|
+
segmentized = lines.geometry.apply(lambda x: x.simplify(radius).segmentize(radius))
|
|
143
|
+
points = [Point(pt) for line in segmentized for pt in line.coords]
|
|
144
|
+
|
|
145
|
+
points = gpd.GeoDataFrame(geometry=points, crs=lines.crs)
|
|
146
|
+
lines["lines"] = lines.geometry
|
|
147
|
+
geom_concave = lines.buffer(5, resolution=1).union_all()
|
|
148
|
+
|
|
149
|
+
for i in range(lloyd_relax_n):
|
|
150
|
+
points.geometry = points.voronoi_polygons().clip(geom_concave).centroid
|
|
151
|
+
points = points.sjoin_nearest(lines, how="left")
|
|
152
|
+
points = points[~points.index.duplicated(keep="first")]
|
|
153
|
+
points["geometry"] = points["lines"].interpolate(points["lines"].project(points.geometry))
|
|
154
|
+
points.drop(columns=["lines", "index_right"], inplace=True)
|
|
155
|
+
|
|
156
|
+
return points.dropna().to_crs(original_crs)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def distribute_points_on_polygons(
|
|
160
|
+
polygons: gpd.GeoDataFrame, radius, only_exterior=True, lloyd_relax_n=2
|
|
161
|
+
) -> gpd.GeoDataFrame:
|
|
162
|
+
polygons = polygons.copy()
|
|
163
|
+
polygons = polygons.explode(ignore_index=True)
|
|
164
|
+
polygons = polygons[polygons.geom_type == "Polygon"]
|
|
165
|
+
|
|
166
|
+
if only_exterior:
|
|
167
|
+
polygons.geometry = polygons.geometry.apply(lambda x: LineString(x.exterior))
|
|
168
|
+
else:
|
|
169
|
+
polygons = gpd.GeoDataFrame(
|
|
170
|
+
geometry=list(polygons.geometry.apply(polygons_to_multilinestring)), crs=polygons.crs
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
return distribute_points_on_linestrings(polygons, radius, lloyd_relax_n=lloyd_relax_n)
|
|
@@ -4,7 +4,7 @@ import numpy as np
|
|
|
4
4
|
import pandas as pd
|
|
5
5
|
from loguru import logger
|
|
6
6
|
from scipy.spatial import KDTree
|
|
7
|
-
from shapely import LineString
|
|
7
|
+
from shapely import LineString, MultiLineString, line_merge, node
|
|
8
8
|
from shapely.geometry.point import Point
|
|
9
9
|
|
|
10
10
|
|
|
@@ -89,7 +89,143 @@ def graph_to_gdf(
|
|
|
89
89
|
return nodes_gdf, edges_gdf
|
|
90
90
|
|
|
91
91
|
|
|
92
|
+
def gdf_to_graph(
|
|
93
|
+
gdf: gpd.GeoDataFrame, project_gdf_attr=True, reproject_to_utm_crs=True, speed=5, check_intersections=True
|
|
94
|
+
) -> nx.DiGraph:
|
|
95
|
+
"""
|
|
96
|
+
Converts a GeoDataFrame of LineStrings into a directed graph (nx.DiGraph).
|
|
97
|
+
|
|
98
|
+
This function transforms a set of linear geometries (which may or may not form a planar graph)
|
|
99
|
+
into a directed graph where each edge corresponds to a LineString (or its segment) from the GeoDataFrame.
|
|
100
|
+
Intersections are optionally checked and merged. Attributes from the original GeoDataFrame
|
|
101
|
+
can be projected onto the graph edges using spatial matching.
|
|
102
|
+
|
|
103
|
+
Parameters
|
|
104
|
+
----------
|
|
105
|
+
gdf : gpd.GeoDataFrame
|
|
106
|
+
A GeoDataFrame containing at least one LineString geometry.
|
|
107
|
+
project_gdf_attr : bool, default=True
|
|
108
|
+
If True, attributes from the input GeoDataFrame will be spatially projected
|
|
109
|
+
onto the resulting graph edges. This can be an expensive operation for large datasets.
|
|
110
|
+
reproject_to_utm_crs : bool, default=True
|
|
111
|
+
If True, reprojects the GeoDataFrame to the estimated local UTM CRS
|
|
112
|
+
to ensure accurate edge length calculations in meters.
|
|
113
|
+
If False, edge lengths are still computed in UTM CRS, but the final graph
|
|
114
|
+
will remain in the original CRS of the input GeoDataFrame.
|
|
115
|
+
speed : float, default=5
|
|
116
|
+
Assumed travel speed in km/h used to compute edge traversal time (in minutes).
|
|
117
|
+
check_intersections : bool, default=True
|
|
118
|
+
If True, merges geometries to ensure topological correctness.
|
|
119
|
+
Can be disabled if the input geometries already form a proper planar graph
|
|
120
|
+
with no unintended intersections.
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
nx.DiGraph
|
|
125
|
+
A directed graph where each edge corresponds to a line segment from the input GeoDataFrame.
|
|
126
|
+
Edge attributes include geometry, length in meters, travel time (in minutes), and any
|
|
127
|
+
additional projected attributes from the original GeoDataFrame.
|
|
128
|
+
|
|
129
|
+
Raises
|
|
130
|
+
------
|
|
131
|
+
ValueError
|
|
132
|
+
If the input GeoDataFrame contains no valid LineStrings.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
def unique_list(agg_vals):
|
|
136
|
+
agg_vals = list(set(agg_vals.dropna()))
|
|
137
|
+
if len(agg_vals) == 1:
|
|
138
|
+
return agg_vals[0]
|
|
139
|
+
return agg_vals
|
|
140
|
+
|
|
141
|
+
original_crs = gdf.crs
|
|
142
|
+
gdf = gdf.to_crs(gdf.estimate_utm_crs())
|
|
143
|
+
|
|
144
|
+
gdf = gdf.explode(ignore_index=True)
|
|
145
|
+
gdf = gdf[gdf.geom_type == "LineString"]
|
|
146
|
+
|
|
147
|
+
if len(gdf) == 0:
|
|
148
|
+
raise ValueError("Provided GeoDataFrame contains no valid LineStrings")
|
|
149
|
+
|
|
150
|
+
if check_intersections:
|
|
151
|
+
lines = line_merge(node(MultiLineString(gdf.geometry.to_list())))
|
|
152
|
+
else:
|
|
153
|
+
lines = line_merge(MultiLineString(gdf.geometry.to_list()))
|
|
154
|
+
|
|
155
|
+
lines = gpd.GeoDataFrame(geometry=list(lines.geoms), crs=gdf.crs)
|
|
156
|
+
|
|
157
|
+
if len(gdf.columns) > 1 and project_gdf_attr:
|
|
158
|
+
lines_centroids = lines.copy()
|
|
159
|
+
lines_centroids.geometry = lines_centroids.apply(
|
|
160
|
+
lambda row: row.geometry.line_interpolate_point(row.geometry.length / 2), axis=1
|
|
161
|
+
).buffer(0.05, resolution=2)
|
|
162
|
+
lines_with_attrs = gpd.sjoin(lines_centroids, gdf, how="left", predicate="intersects")
|
|
163
|
+
aggregated_attrs = (
|
|
164
|
+
lines_with_attrs.drop(columns=["geometry", "index_right"]) # убрать геометрию буфера
|
|
165
|
+
.groupby(lines_with_attrs.index)
|
|
166
|
+
.agg(unique_list)
|
|
167
|
+
)
|
|
168
|
+
lines = pd.concat([lines, aggregated_attrs], axis=1)
|
|
169
|
+
|
|
170
|
+
lines["length_meter"] = np.round(lines.length, 2)
|
|
171
|
+
if not reproject_to_utm_crs:
|
|
172
|
+
lines = lines.to_crs(original_crs)
|
|
173
|
+
|
|
174
|
+
coords = lines.geometry.get_coordinates()
|
|
175
|
+
coords_grouped_by_index = coords.reset_index(names="old_index").groupby("old_index")
|
|
176
|
+
start_coords = coords_grouped_by_index.head(1).apply(lambda a: (a.x, a.y), axis=1).rename("start")
|
|
177
|
+
end_coords = coords_grouped_by_index.tail(1).apply(lambda a: (a.x, a.y), axis=1).rename("end")
|
|
178
|
+
coords = pd.concat([start_coords.reset_index(), end_coords.reset_index()], axis=1)[["start", "end"]]
|
|
179
|
+
lines = pd.concat([lines, coords], axis=1)
|
|
180
|
+
unique_coords = pd.concat([coords["start"], coords["end"]], ignore_index=True).unique()
|
|
181
|
+
coord_to_index = {coord: idx for idx, coord in enumerate(unique_coords)}
|
|
182
|
+
|
|
183
|
+
lines["u"] = lines["start"].map(coord_to_index)
|
|
184
|
+
lines["v"] = lines["end"].map(coord_to_index)
|
|
185
|
+
|
|
186
|
+
speed = speed * 1000 / 60
|
|
187
|
+
lines["time_min"] = np.round(lines["length_meter"] / speed, 2)
|
|
188
|
+
|
|
189
|
+
graph = nx.Graph()
|
|
190
|
+
for coords, node_id in coord_to_index.items():
|
|
191
|
+
x, y = coords
|
|
192
|
+
graph.add_node(node_id, x=float(x), y=float(y))
|
|
193
|
+
|
|
194
|
+
columns_to_attr = lines.columns.difference(["start", "end", "u", "v"])
|
|
195
|
+
for _, row in lines.iterrows():
|
|
196
|
+
edge_attrs = {}
|
|
197
|
+
for col in columns_to_attr:
|
|
198
|
+
edge_attrs[col] = row[col]
|
|
199
|
+
graph.add_edge(row.u, row.v, **edge_attrs)
|
|
200
|
+
|
|
201
|
+
graph.graph["crs"] = lines.crs
|
|
202
|
+
graph.graph["speed m/min"] = speed
|
|
203
|
+
return nx.DiGraph(graph)
|
|
204
|
+
|
|
205
|
+
|
|
92
206
|
def get_closest_nodes_from_gdf(gdf: gpd.GeoDataFrame, nx_graph: nx.Graph) -> tuple:
|
|
207
|
+
"""
|
|
208
|
+
Finds the closest graph nodes to the geometries in a GeoDataFrame.
|
|
209
|
+
|
|
210
|
+
Parameters
|
|
211
|
+
----------
|
|
212
|
+
gdf : gpd.GeoDataFrame
|
|
213
|
+
GeoDataFrame with geometries for which the nearest graph nodes will be found.
|
|
214
|
+
nx_graph : nx.Graph
|
|
215
|
+
A NetworkX graph where nodes have 'x' and 'y' attributes (coordinates).
|
|
216
|
+
|
|
217
|
+
Returns
|
|
218
|
+
-------
|
|
219
|
+
tuple
|
|
220
|
+
A tuple of (distances, nearest_nodes), where:
|
|
221
|
+
- distances: List of distances from each geometry to the nearest node.
|
|
222
|
+
- nearest_nodes: List of node IDs closest to each geometry in the input GeoDataFrame.
|
|
223
|
+
|
|
224
|
+
Raises
|
|
225
|
+
------
|
|
226
|
+
ValueError
|
|
227
|
+
If any node in the graph is missing 'x' or 'y' attributes.
|
|
228
|
+
"""
|
|
93
229
|
nodes_with_data = list(nx_graph.nodes(data=True))
|
|
94
230
|
try:
|
|
95
231
|
coordinates = np.array([(data["x"], data["y"]) for node, data in nodes_with_data])
|
|
@@ -103,6 +239,24 @@ def get_closest_nodes_from_gdf(gdf: gpd.GeoDataFrame, nx_graph: nx.Graph) -> tup
|
|
|
103
239
|
|
|
104
240
|
|
|
105
241
|
def remove_weakly_connected_nodes(graph: nx.DiGraph) -> nx.DiGraph:
|
|
242
|
+
"""
|
|
243
|
+
Removes all nodes that are not part of the largest strongly connected component in the graph.
|
|
244
|
+
|
|
245
|
+
Parameters
|
|
246
|
+
----------
|
|
247
|
+
graph : nx.DiGraph
|
|
248
|
+
A directed NetworkX graph.
|
|
249
|
+
|
|
250
|
+
Returns
|
|
251
|
+
-------
|
|
252
|
+
nx.DiGraph
|
|
253
|
+
A new graph with only the largest strongly connected component retained.
|
|
254
|
+
|
|
255
|
+
Notes
|
|
256
|
+
-----
|
|
257
|
+
- Also logs a warning if multiple weakly connected components are detected.
|
|
258
|
+
- Logs the number of nodes removed and size of the remaining component.
|
|
259
|
+
"""
|
|
106
260
|
graph = graph.copy()
|
|
107
261
|
|
|
108
262
|
weakly_connected_components = list(nx.weakly_connected_components(graph))
|
|
@@ -125,3 +279,42 @@ def remove_weakly_connected_nodes(graph: nx.DiGraph) -> nx.DiGraph:
|
|
|
125
279
|
graph.remove_nodes_from(nodes_to_del)
|
|
126
280
|
|
|
127
281
|
return graph
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def reverse_graph(nx_graph: nx.Graph, weight: str) -> tuple[nx.Graph, nx.DiGraph]:
|
|
285
|
+
"""
|
|
286
|
+
Generate a reversed version of a directed or weighted graph.
|
|
287
|
+
|
|
288
|
+
If the input graph is undirected, the original graph is returned as-is.
|
|
289
|
+
For directed graphs, the function returns a new graph with all edge directions reversed,
|
|
290
|
+
preserving the specified edge weight.
|
|
291
|
+
|
|
292
|
+
Parameters
|
|
293
|
+
----------
|
|
294
|
+
nx_graph : nx.Graph
|
|
295
|
+
Input NetworkX graph (can be directed or undirected).
|
|
296
|
+
weight : str
|
|
297
|
+
Name of the edge attribute to use as weight in graph conversion.
|
|
298
|
+
|
|
299
|
+
Returns
|
|
300
|
+
-------
|
|
301
|
+
tuple[nx.Graph, nx.DiGraph]
|
|
302
|
+
A tuple containing:
|
|
303
|
+
- normalized_graph: Original graph with relabeled nodes (if needed)
|
|
304
|
+
- reversed_graph: Directed graph with reversed edges and preserved weights
|
|
305
|
+
"""
|
|
306
|
+
|
|
307
|
+
if nx_graph.is_multigraph():
|
|
308
|
+
nx_graph = nx.DiGraph(nx_graph) if nx_graph.is_directed() else nx.Graph(nx_graph)
|
|
309
|
+
if not nx_graph.is_multigraph() and not nx_graph.is_directed():
|
|
310
|
+
return nx_graph, nx_graph
|
|
311
|
+
|
|
312
|
+
nx_graph = remove_weakly_connected_nodes(nx_graph)
|
|
313
|
+
|
|
314
|
+
mapping = {old_label: new_label for new_label, old_label in enumerate(nx_graph.nodes())}
|
|
315
|
+
nx_graph = nx.relabel_nodes(nx_graph, mapping)
|
|
316
|
+
|
|
317
|
+
sparse_matrix = nx.to_scipy_sparse_array(nx_graph, weight=weight)
|
|
318
|
+
transposed_matrix = sparse_matrix.transpose()
|
|
319
|
+
reversed_graph = nx.from_scipy_sparse_array(transposed_matrix, edge_attribute=weight, create_using=type(nx_graph))
|
|
320
|
+
return nx_graph, reversed_graph
|
|
@@ -87,9 +87,9 @@ def get_visibility_accurate(
|
|
|
87
87
|
point_from = point_from.iloc[0].geometry
|
|
88
88
|
else:
|
|
89
89
|
obstacles = obstacles.copy()
|
|
90
|
-
|
|
90
|
+
if obstacles.contains(point_from).any():
|
|
91
|
+
return Polygon()
|
|
91
92
|
obstacles.reset_index(inplace=True, drop=True)
|
|
92
|
-
|
|
93
93
|
point_buffer = point_from.buffer(view_distance, resolution=32)
|
|
94
94
|
allowed_geom_types = ["MultiPolygon", "Polygon", "LineString", "MultiLineString"]
|
|
95
95
|
obstacles = obstacles[obstacles.geom_type.isin(allowed_geom_types)]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ObjectNat
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: ObjectNat is an open-source library created for geospatial analysis created by IDU team
|
|
5
5
|
License: BSD-3-Clause
|
|
6
6
|
Author: DDonnyy
|
|
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Requires-Dist: geopandas (>=1.0.1,<2.0.0)
|
|
15
|
+
Requires-Dist: loguru (>=0.7.3,<0.8.0)
|
|
15
16
|
Requires-Dist: networkx (>=3.4.2,<4.0.0)
|
|
16
17
|
Requires-Dist: numpy (>=2.1.3,<3.0.0)
|
|
17
18
|
Requires-Dist: pandarallel (>=1.6.5,<2.0.0)
|
|
@@ -44,21 +45,26 @@ Description-Content-Type: text/markdown
|
|
|
44
45
|
- **Stepped isochrones**: show accessibility ranges divided into time intervals (e.g., 5, 10, 15 minutes).
|
|
45
46
|
|
|
46
47
|
<p align="center">
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/isochrone_ways_15_min.png" alt="isochrone_ways_15_min" width="300">
|
|
49
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/isochrone_radius_15_min.png" alt="isochrone_radius_15_min" width="300">
|
|
50
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/isochrone_3points_radius_8_min.png" alt="isochrone_3points_radius_8_min" width="300">
|
|
49
51
|
</p>
|
|
50
52
|
<p align="center">
|
|
51
|
-
<img src="https://
|
|
52
|
-
<img src="https://
|
|
53
|
-
<img src="https://
|
|
53
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/stepped_isochrone_ways_15_min.png" alt="stepped_isochrone_ways_15_min" width="300">
|
|
54
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/stepped_isochrone_radius_15_min.png" alt="stepped_isochrone_radius_15_min" width="300">
|
|
55
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/stepped_isochrone_separate_15_min.png" alt="stepped_isochrone_separate_15_min" width="300">
|
|
54
56
|
</p>
|
|
55
57
|
|
|
58
|
+
2. **[Coverage Zones](./examples/coverage_zones.ipynb)** — Function for generating **coverage zones** from a set of source points using a transport network. It calculates the area each point can reach based on **travel time** or **distance**, then builds polygons via **Voronoi diagrams** and clips them to a custom boundary if provided.
|
|
56
59
|
|
|
57
|
-
2. **[Coverage Zones](./examples/graph_coverage.ipynb)** — Function for generating **coverage zones** from a set of source points using a transport network. It calculates the area each point can reach based on **travel time** or **distance**, then builds polygons via **Voronoi diagrams** and clips them to a custom boundary if provided.
|
|
58
|
-
|
|
59
60
|
<p align="center">
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/coverage_zones_time_10min.png" alt="coverage_zones_time_10min" width="350">
|
|
62
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/coverage_zones_distance_600m.png" alt="coverage_zones_distance_600m" width="350">
|
|
63
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/coverage_zones_radius_distance_800m.png" alt="coverage_zones_distance_radius_voronoi" width="350">
|
|
64
|
+
</p>
|
|
65
|
+
<p align="center">
|
|
66
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/stepped_coverage_zones_separate.png" alt="stepped_coverage_zones_separate" width="350">
|
|
67
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/stepped_coverage_zones_voronoi.png" alt="stepped_coverage_zones_voronoi" width="350">
|
|
62
68
|
</p>
|
|
63
69
|
|
|
64
70
|
3. **[Service Provision Analysis](./examples/calculate_provision.ipynb)** — Function for evaluating the provision of residential buildings and their population with services (e.g., schools, clinics)
|
|
@@ -69,9 +75,9 @@ Description-Content-Type: text/markdown
|
|
|
69
75
|
- **Clipping** of provision results to a custom analysis area (e.g., administrative boundaries).
|
|
70
76
|
|
|
71
77
|
<p align="center">
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
78
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/service_provision_initial.png" alt="service_provision_initial" width="300">
|
|
79
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/service_provision_recalculated.png" alt="service_provision_recalculated" width="300">
|
|
80
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/service_provision_clipped.png" alt="service_provision_clipped" width="300">
|
|
75
81
|
</p>
|
|
76
82
|
|
|
77
83
|
4. **[Visibility Analysis](./examples/visibility_analysis.ipynb)** — Function for estimating visibility from a given point or multiple points to nearby buildings within a certain distance.
|
|
@@ -84,7 +90,7 @@ Description-Content-Type: text/markdown
|
|
|
84
90
|
- A **accurate method** for detailed local analysis.
|
|
85
91
|
|
|
86
92
|
<p align="center">
|
|
87
|
-
<img src="https://
|
|
93
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/visibility_comparison_methods.png" alt="visibility_comparison_methods" height="250">
|
|
88
94
|
<img src="https://github.com/user-attachments/assets/b5b0d4b3-a02f-4ade-8772-475703cd6435" alt="visibility-catchment-area" height="250">
|
|
89
95
|
</p>
|
|
90
96
|
|
|
@@ -93,7 +99,7 @@ Description-Content-Type: text/markdown
|
|
|
93
99
|
🔗 **[See detailed explanation in the Wiki](https://github.com/DDonnyy/ObjectNat/wiki/Noise-simulation)**
|
|
94
100
|
|
|
95
101
|
<p align="center">
|
|
96
|
-
|
|
102
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/noise_simulation_1point.png" alt="noise_simulation_1point" width="400">
|
|
97
103
|
</p>
|
|
98
104
|
|
|
99
105
|
|
|
@@ -104,7 +110,7 @@ Description-Content-Type: text/markdown
|
|
|
104
110
|
Additionally, the function can calculate the **relative ratio** of different service types within each cluster, enabling spatial analysis of service composition.
|
|
105
111
|
|
|
106
112
|
<p align="center">
|
|
107
|
-
<img src="https://
|
|
113
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/building_clusters.png" alt="building_clusters" width="400">
|
|
108
114
|
</p>
|
|
109
115
|
|
|
110
116
|
## City graphs
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
objectnat/__init__.py,sha256=OnDvrLPLEeYIE_9qOVYgMc-PkRzIqShtGxirguEXiRU,260
|
|
2
|
+
objectnat/_api.py,sha256=0R1nypAQUcbQ9YSLw_MUgUWoNl8c1zMZteV8wGzdkvc,711
|
|
3
|
+
objectnat/_config.py,sha256=fGPsMZqA8FVBBOINxRiTFkOOZsNLyablM5G0tdKeQB4,1306
|
|
4
|
+
objectnat/_version.py,sha256=viNrPDalxthAcGDGV3QUhm0tvEB8vd-Uh-27v9_MV7c,18
|
|
5
|
+
objectnat/methods/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
objectnat/methods/coverage_zones/__init__.py,sha256=3uTDC1xf3zgQRqSQR4URp__HjZ8eVUtjK8r3mGq-zuQ,161
|
|
7
|
+
objectnat/methods/coverage_zones/graph_coverage.py,sha256=iYI2huF9jLPPlISeh7R05ZaaWZ7s8ZBPGIk1WUeM-EI,4355
|
|
8
|
+
objectnat/methods/coverage_zones/radius_voronoi_coverage.py,sha256=H4_lvSc770TO6-xq6EzgWuUf00N5vYCMO3X6UJhZYAQ,1735
|
|
9
|
+
objectnat/methods/coverage_zones/stepped_coverage.py,sha256=dgNDHh2nG2zXcLHOIJZyZGVgcKnoxtu5iP1_aHdOYNM,5951
|
|
10
|
+
objectnat/methods/isochrones/__init__.py,sha256=bDfUZPbS3_PuTEB2QcRTYjvyJtUvjbDhAw6QJvD_ih4,90
|
|
11
|
+
objectnat/methods/isochrones/isochrone_utils.py,sha256=dJwvoGXUypwU2_oF-rdxNZm90gOi-RUp_0WM1C2HaPU,6870
|
|
12
|
+
objectnat/methods/isochrones/isochrones.py,sha256=TceEQ8xJ-KZR29fAZ1v9gLl42iN2vvDmoOIi1VkvQd4,12625
|
|
13
|
+
objectnat/methods/noise/__init__.py,sha256=Z5hdRiiv7tDgRAyrhwrTWkonI5dyLU3vfxHl-qdzw9A,233
|
|
14
|
+
objectnat/methods/noise/noise_exceptions.py,sha256=nTav5kp6RNpi0kxD9cMULTApOuvAu9wEiX28fkLAnOc,634
|
|
15
|
+
objectnat/methods/noise/noise_init_data.py,sha256=Vp-R_yH7CgYqZEtbGAdr1iiIbgauReniLQ_a2TcszhY,503
|
|
16
|
+
objectnat/methods/noise/noise_reduce.py,sha256=B85ifAN_mHiBKJso-cZiSkj7588w2sA-ugGvEal4CBw,6885
|
|
17
|
+
objectnat/methods/noise/noise_simulation.py,sha256=1hONZuYobyI9mVzkjVXost8XzEsZS8un2v7VTQobXQk,21680
|
|
18
|
+
objectnat/methods/noise/noise_simulation_simplified.py,sha256=_2wZ3nb-w-77aZiida0dOTGS4Yuq4PnRKmXi9xQEhYU,6135
|
|
19
|
+
objectnat/methods/point_clustering/__init__.py,sha256=pX2qDUCvs9LJI36mr65vbdRml6AE8hIYYxIJLdQZQxs,61
|
|
20
|
+
objectnat/methods/point_clustering/cluster_points_in_polygons.py,sha256=kwiZHY3TCUCE-nN5IdhCwDESWJvSCZUfrUJU3yC1csc,5042
|
|
21
|
+
objectnat/methods/provision/__init__.py,sha256=0Uy66n2xH0Y45JyhIYHEVfC2rig6bMYp6PV2KkNhbK8,80
|
|
22
|
+
objectnat/methods/provision/provision.py,sha256=g00mN6RGFZcLg0PIMgywnO3TWiFqx3sTYqTeG3Kl33s,4749
|
|
23
|
+
objectnat/methods/provision/provision_exceptions.py,sha256=lznEmlmZDzGIOtapZVqZDMutIi5eGbFuVCYeVa7VZWk,1687
|
|
24
|
+
objectnat/methods/provision/provision_model.py,sha256=_IKS3pe_i7gLcA56baB0g3mH77T9pPP_4FzjuK_FZ5Y,14529
|
|
25
|
+
objectnat/methods/utils/__init__.py,sha256=sGXy4KUOe5I0UYztnB4rIl2HNd-oqnqRYrBsiU-dpNY,52
|
|
26
|
+
objectnat/methods/utils/geom_utils.py,sha256=PdUjQDZ8drJ1ZFCFabDJnD6oFvgHeAQrsbeivuDOdcI,6385
|
|
27
|
+
objectnat/methods/utils/graph_utils.py,sha256=oIKG7XqVRkXE-QQaReXxClU8aHSKoAdwj_3g-ectUgo,12487
|
|
28
|
+
objectnat/methods/utils/math_utils.py,sha256=Vc8U15LtFOwgIt1YSOSKWYOIiW_1XLuMGOa6ejBpEUk,839
|
|
29
|
+
objectnat/methods/visibility/__init__.py,sha256=Mx1kaoV-yfQUxlMkgNF4AhjSweFEJMEx3NBis5OM3mA,161
|
|
30
|
+
objectnat/methods/visibility/visibility_analysis.py,sha256=J-GtggH12DoZaquZuDR7edDsXtCqxI3qKSRpT2ULPGs,21251
|
|
31
|
+
objectnat-1.2.0.dist-info/LICENSE.txt,sha256=yPEioMfTd7JAQgAU6J13inS1BSjwd82HFlRSoIb4My8,1498
|
|
32
|
+
objectnat-1.2.0.dist-info/METADATA,sha256=SerHhzYYX6o31ph9Yqm7MxxozcQ4Yxhcmzt9kKyvbI0,8881
|
|
33
|
+
objectnat-1.2.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
34
|
+
objectnat-1.2.0.dist-info/RECORD,,
|
objectnat-1.0.1.dist-info/RECORD
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
objectnat/__init__.py,sha256=OnDvrLPLEeYIE_9qOVYgMc-PkRzIqShtGxirguEXiRU,260
|
|
2
|
-
objectnat/_api.py,sha256=5TvsMjWjiR7kFIWnfRJxngWOrC_eKQ7Pt-TSMpjqzI0,595
|
|
3
|
-
objectnat/_config.py,sha256=fGPsMZqA8FVBBOINxRiTFkOOZsNLyablM5G0tdKeQB4,1306
|
|
4
|
-
objectnat/_version.py,sha256=9C8U_kMX0x98w3CUW1G2vm12pYBsE-TE72pXihbWYqU,18
|
|
5
|
-
objectnat/methods/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
objectnat/methods/coverage_zones/__init__.py,sha256=cdXjW041ysfIlr4sGQ_Xa2nok4tHfMJTEf8pem-0m-U,95
|
|
7
|
-
objectnat/methods/coverage_zones/graph_coverage.py,sha256=3nVZekDAmbz2ZzxsayX9KIgudlSZF3OnZ9g5kFmVt8o,4797
|
|
8
|
-
objectnat/methods/coverage_zones/radius_voronoi.py,sha256=H4_lvSc770TO6-xq6EzgWuUf00N5vYCMO3X6UJhZYAQ,1735
|
|
9
|
-
objectnat/methods/isochrones/__init__.py,sha256=bDfUZPbS3_PuTEB2QcRTYjvyJtUvjbDhAw6QJvD_ih4,90
|
|
10
|
-
objectnat/methods/isochrones/isochrone_utils.py,sha256=XdgjmGgeNfgXa3ERkkGEEzrzF5TTkrI6U8hxFAFHRkg,4861
|
|
11
|
-
objectnat/methods/isochrones/isochrones.py,sha256=GoaFGiLSsG-RRXA5jtij3zsrwxH5ThKDh8XexZ3rD3k,14046
|
|
12
|
-
objectnat/methods/noise/__init__.py,sha256=6JV586JdOqE0OUFUyxsrCoGuDxRuB-6ItSA5CRuFdsE,152
|
|
13
|
-
objectnat/methods/noise/noise_exceptions.py,sha256=nTav5kp6RNpi0kxD9cMULTApOuvAu9wEiX28fkLAnOc,634
|
|
14
|
-
objectnat/methods/noise/noise_init_data.py,sha256=Vp-R_yH7CgYqZEtbGAdr1iiIbgauReniLQ_a2TcszhY,503
|
|
15
|
-
objectnat/methods/noise/noise_reduce.py,sha256=B85ifAN_mHiBKJso-cZiSkj7588w2sA-ugGvEal4CBw,6885
|
|
16
|
-
objectnat/methods/noise/noise_sim.py,sha256=qb0C2CD-sIEcdUWO3xSwCGjuR94xPgU81FC94TeXHBo,20278
|
|
17
|
-
objectnat/methods/point_clustering/__init__.py,sha256=pX2qDUCvs9LJI36mr65vbdRml6AE8hIYYxIJLdQZQxs,61
|
|
18
|
-
objectnat/methods/point_clustering/cluster_points_in_polygons.py,sha256=kwiZHY3TCUCE-nN5IdhCwDESWJvSCZUfrUJU3yC1csc,5042
|
|
19
|
-
objectnat/methods/provision/__init__.py,sha256=0Uy66n2xH0Y45JyhIYHEVfC2rig6bMYp6PV2KkNhbK8,80
|
|
20
|
-
objectnat/methods/provision/provision.py,sha256=g00mN6RGFZcLg0PIMgywnO3TWiFqx3sTYqTeG3Kl33s,4749
|
|
21
|
-
objectnat/methods/provision/provision_exceptions.py,sha256=lznEmlmZDzGIOtapZVqZDMutIi5eGbFuVCYeVa7VZWk,1687
|
|
22
|
-
objectnat/methods/provision/provision_model.py,sha256=_IKS3pe_i7gLcA56baB0g3mH77T9pPP_4FzjuK_FZ5Y,14529
|
|
23
|
-
objectnat/methods/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
|
-
objectnat/methods/utils/geom_utils.py,sha256=6tt7gKztDYOTqaZ1sLO4vN2sNPV9qeFyKK1BbvwwD18,4582
|
|
25
|
-
objectnat/methods/utils/graph_utils.py,sha256=M983Hy1CXRIGg_q71tW0HVB8ZS4ZpDK5I2J3imyznUQ,4799
|
|
26
|
-
objectnat/methods/utils/math_utils.py,sha256=Vc8U15LtFOwgIt1YSOSKWYOIiW_1XLuMGOa6ejBpEUk,839
|
|
27
|
-
objectnat/methods/visibility/__init__.py,sha256=Mx1kaoV-yfQUxlMkgNF4AhjSweFEJMEx3NBis5OM3mA,161
|
|
28
|
-
objectnat/methods/visibility/visibility_analysis.py,sha256=fDIZ5S7oR-5ZEsjZPFexb7oFw-1X6uj0A4xuP3pX354,21183
|
|
29
|
-
objectnat-1.0.1.dist-info/LICENSE.txt,sha256=yPEioMfTd7JAQgAU6J13inS1BSjwd82HFlRSoIb4My8,1498
|
|
30
|
-
objectnat-1.0.1.dist-info/METADATA,sha256=2cgaqgB4Q4sXRbrBkVrzOKUIXEjIOejwS9f9kJIuxxE,8040
|
|
31
|
-
objectnat-1.0.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
32
|
-
objectnat-1.0.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|