ObjectNat 1.0.1__tar.gz → 1.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ObjectNat might be problematic. Click here for more details.
- {objectnat-1.0.1 → objectnat-1.1.0}/PKG-INFO +22 -16
- {objectnat-1.0.1 → objectnat-1.1.0}/README.md +20 -15
- {objectnat-1.0.1 → objectnat-1.1.0}/pyproject.toml +3 -4
- {objectnat-1.0.1 → objectnat-1.1.0}/src/objectnat/_api.py +1 -1
- objectnat-1.1.0/src/objectnat/_version.py +1 -0
- objectnat-1.1.0/src/objectnat/methods/coverage_zones/__init__.py +3 -0
- {objectnat-1.0.1 → objectnat-1.1.0}/src/objectnat/methods/coverage_zones/graph_coverage.py +8 -18
- objectnat-1.1.0/src/objectnat/methods/coverage_zones/stepped_coverage.py +142 -0
- {objectnat-1.0.1 → objectnat-1.1.0}/src/objectnat/methods/isochrones/isochrone_utils.py +37 -0
- {objectnat-1.0.1 → objectnat-1.1.0}/src/objectnat/methods/isochrones/isochrones.py +3 -29
- {objectnat-1.0.1 → objectnat-1.1.0}/src/objectnat/methods/noise/noise_sim.py +95 -70
- {objectnat-1.0.1 → objectnat-1.1.0}/src/objectnat/methods/utils/graph_utils.py +79 -0
- objectnat-1.0.1/src/objectnat/_version.py +0 -1
- objectnat-1.0.1/src/objectnat/methods/coverage_zones/__init__.py +0 -2
- {objectnat-1.0.1 → objectnat-1.1.0}/LICENSE.txt +0 -0
- {objectnat-1.0.1 → objectnat-1.1.0}/src/objectnat/__init__.py +0 -0
- {objectnat-1.0.1 → objectnat-1.1.0}/src/objectnat/_config.py +0 -0
- {objectnat-1.0.1 → objectnat-1.1.0}/src/objectnat/methods/__init__.py +0 -0
- /objectnat-1.0.1/src/objectnat/methods/coverage_zones/radius_voronoi.py → /objectnat-1.1.0/src/objectnat/methods/coverage_zones/radius_voronoi_coverage.py +0 -0
- {objectnat-1.0.1 → objectnat-1.1.0}/src/objectnat/methods/isochrones/__init__.py +0 -0
- {objectnat-1.0.1 → objectnat-1.1.0}/src/objectnat/methods/noise/__init__.py +0 -0
- {objectnat-1.0.1 → objectnat-1.1.0}/src/objectnat/methods/noise/noise_exceptions.py +0 -0
- {objectnat-1.0.1 → objectnat-1.1.0}/src/objectnat/methods/noise/noise_init_data.py +0 -0
- {objectnat-1.0.1 → objectnat-1.1.0}/src/objectnat/methods/noise/noise_reduce.py +0 -0
- {objectnat-1.0.1 → objectnat-1.1.0}/src/objectnat/methods/point_clustering/__init__.py +0 -0
- {objectnat-1.0.1 → objectnat-1.1.0}/src/objectnat/methods/point_clustering/cluster_points_in_polygons.py +0 -0
- {objectnat-1.0.1 → objectnat-1.1.0}/src/objectnat/methods/provision/__init__.py +0 -0
- {objectnat-1.0.1 → objectnat-1.1.0}/src/objectnat/methods/provision/provision.py +0 -0
- {objectnat-1.0.1 → objectnat-1.1.0}/src/objectnat/methods/provision/provision_exceptions.py +0 -0
- {objectnat-1.0.1 → objectnat-1.1.0}/src/objectnat/methods/provision/provision_model.py +0 -0
- {objectnat-1.0.1 → objectnat-1.1.0}/src/objectnat/methods/utils/__init__.py +0 -0
- {objectnat-1.0.1 → objectnat-1.1.0}/src/objectnat/methods/utils/geom_utils.py +0 -0
- {objectnat-1.0.1 → objectnat-1.1.0}/src/objectnat/methods/utils/math_utils.py +0 -0
- {objectnat-1.0.1 → objectnat-1.1.0}/src/objectnat/methods/visibility/__init__.py +0 -0
- {objectnat-1.0.1 → objectnat-1.1.0}/src/objectnat/methods/visibility/visibility_analysis.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ObjectNat
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.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
|
|
@@ -22,21 +22,26 @@
|
|
|
22
22
|
- **Stepped isochrones**: show accessibility ranges divided into time intervals (e.g., 5, 10, 15 minutes).
|
|
23
23
|
|
|
24
24
|
<p align="center">
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/isochrone_ways_15_min.png" alt="isochrone_ways_15_min" width="300">
|
|
26
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/isochrone_radius_15_min.png" alt="isochrone_radius_15_min" width="300">
|
|
27
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/isochrone_3points_radius_8_min.png" alt="isochrone_3points_radius_8_min" width="300">
|
|
27
28
|
</p>
|
|
28
29
|
<p align="center">
|
|
29
|
-
<img src="https://
|
|
30
|
-
<img src="https://
|
|
31
|
-
<img src="https://
|
|
30
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/stepped_isochrone_ways_15_min.png" alt="stepped_isochrone_ways_15_min" width="300">
|
|
31
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/stepped_isochrone_radius_15_min.png" alt="stepped_isochrone_radius_15_min" width="300">
|
|
32
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/stepped_isochrone_separate_15_min.png" alt="stepped_isochrone_separate_15_min" width="300">
|
|
32
33
|
</p>
|
|
33
34
|
|
|
35
|
+
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.
|
|
34
36
|
|
|
35
|
-
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.
|
|
36
|
-
|
|
37
37
|
<p align="center">
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/coverage_zones_time_10min.png" alt="coverage_zones_time_10min" width="350">
|
|
39
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/coverage_zones_distance_600m.png" alt="coverage_zones_distance_600m" width="350">
|
|
40
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/coverage_zones_radius_distance_800m.png" alt="coverage_zones_distance_radius_voronoi" width="350">
|
|
41
|
+
</p>
|
|
42
|
+
<p align="center">
|
|
43
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/stepped_coverage_zones_separate.png" alt="stepped_coverage_zones_separate" width="350">
|
|
44
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/stepped_coverage_zones_voronoi.png" alt="stepped_coverage_zones_voronoi" width="350">
|
|
40
45
|
</p>
|
|
41
46
|
|
|
42
47
|
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)
|
|
@@ -47,9 +52,9 @@
|
|
|
47
52
|
- **Clipping** of provision results to a custom analysis area (e.g., administrative boundaries).
|
|
48
53
|
|
|
49
54
|
<p align="center">
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/service_provision_initial.png" alt="service_provision_initial" width="300">
|
|
56
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/service_provision_recalculated.png" alt="service_provision_recalculated" width="300">
|
|
57
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/service_provision_clipped.png" alt="service_provision_clipped" width="300">
|
|
53
58
|
</p>
|
|
54
59
|
|
|
55
60
|
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.
|
|
@@ -62,7 +67,7 @@
|
|
|
62
67
|
- A **accurate method** for detailed local analysis.
|
|
63
68
|
|
|
64
69
|
<p align="center">
|
|
65
|
-
<img src="https://
|
|
70
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/visibility_comparison_methods.png" alt="visibility_comparison_methods" height="250">
|
|
66
71
|
<img src="https://github.com/user-attachments/assets/b5b0d4b3-a02f-4ade-8772-475703cd6435" alt="visibility-catchment-area" height="250">
|
|
67
72
|
</p>
|
|
68
73
|
|
|
@@ -71,7 +76,7 @@
|
|
|
71
76
|
🔗 **[See detailed explanation in the Wiki](https://github.com/DDonnyy/ObjectNat/wiki/Noise-simulation)**
|
|
72
77
|
|
|
73
78
|
<p align="center">
|
|
74
|
-
|
|
79
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/noise_simulation_1point.png" alt="noise_simulation_1point" width="400">
|
|
75
80
|
</p>
|
|
76
81
|
|
|
77
82
|
|
|
@@ -82,7 +87,7 @@
|
|
|
82
87
|
Additionally, the function can calculate the **relative ratio** of different service types within each cluster, enabling spatial analysis of service composition.
|
|
83
88
|
|
|
84
89
|
<p align="center">
|
|
85
|
-
<img src="https://
|
|
90
|
+
<img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/building_clusters.png" alt="building_clusters" width="400">
|
|
86
91
|
</p>
|
|
87
92
|
|
|
88
93
|
## City graphs
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "ObjectNat"
|
|
3
|
-
version = "1.0
|
|
3
|
+
version = "1.1.0"
|
|
4
4
|
description = "ObjectNat is an open-source library created for geospatial analysis created by IDU team"
|
|
5
5
|
license = "BSD-3-Clause"
|
|
6
6
|
authors = ["DDonnyy <63115678+DDonnyy@users.noreply.github.com>"]
|
|
@@ -17,6 +17,7 @@ tqdm = "^4.66.2"
|
|
|
17
17
|
pandarallel = "^1.6.5"
|
|
18
18
|
networkx = "^3.4.2"
|
|
19
19
|
scikit-learn = "^1.4.0"
|
|
20
|
+
loguru = "^0.7.3"
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
[tool.poetry.group.dev.dependencies]
|
|
@@ -28,6 +29,7 @@ isort = "^5.13.2"
|
|
|
28
29
|
jupyter = "^1.0.0"
|
|
29
30
|
pytest = "^8.3.5"
|
|
30
31
|
pytest-cov = "^6.0.0"
|
|
32
|
+
pre-commit = "^4.2.0"
|
|
31
33
|
folium = "^0.19.5"
|
|
32
34
|
matplotlib = "^3.10.1"
|
|
33
35
|
mapclassify = "^2.8.1"
|
|
@@ -56,9 +58,6 @@ disable = [
|
|
|
56
58
|
"too-many-arguments",
|
|
57
59
|
"cyclic-import"
|
|
58
60
|
]
|
|
59
|
-
good-names = [
|
|
60
|
-
|
|
61
|
-
]
|
|
62
61
|
|
|
63
62
|
[tool.isort]
|
|
64
63
|
multi_line_output = 3
|
|
@@ -1,6 +1,6 @@
|
|
|
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
5
|
from .methods.noise import simulate_noise
|
|
6
6
|
from .methods.point_clustering import get_clusters_polygon
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = "1.1.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(
|
|
@@ -22,26 +22,33 @@ from objectnat.methods.visibility.visibility_analysis import get_visibility_accu
|
|
|
22
22
|
|
|
23
23
|
logger = config.logger
|
|
24
24
|
|
|
25
|
+
MAX_DB_VALUE = 194
|
|
26
|
+
|
|
25
27
|
|
|
26
28
|
def simulate_noise(
|
|
27
|
-
source_points: gpd.GeoDataFrame,
|
|
29
|
+
source_points: gpd.GeoDataFrame,
|
|
30
|
+
obstacles: gpd.GeoDataFrame,
|
|
31
|
+
source_noise_db: float = None,
|
|
32
|
+
geometric_mean_freq_hz: float = None,
|
|
33
|
+
**kwargs,
|
|
28
34
|
):
|
|
29
35
|
"""
|
|
30
36
|
Simulates noise propagation from a set of source points considering obstacles, trees, and environmental factors.
|
|
31
37
|
|
|
32
38
|
Args:
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
source_points (gpd.GeoDataFrame): A GeoDataFrame with one or more point geometries representing noise sources.
|
|
40
|
+
Optionally, it can include 'source_noise_db' and 'geometric_mean_freq_hz' columns for per-point simulation.
|
|
35
41
|
obstacles (gpd.GeoDataFrame): A GeoDataFrame representing obstacles in the environment. If a column with
|
|
36
42
|
sound absorption coefficients is present, its name should be provided in the `absorb_ratio_column` argument.
|
|
37
43
|
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
|
-
|
|
44
|
+
source_noise_db (float, optional): Default noise level (dB) to use if not specified per-point. Decibels are
|
|
45
|
+
logarithmic units used to measure sound intensity. A value of 20 dB represents a barely audible whisper,
|
|
46
|
+
while 140 dB is comparable to the noise of jet engines.
|
|
47
|
+
geometric_mean_freq_hz (float, optional): Default frequency (Hz) to use if not specified per-point.
|
|
48
|
+
This parameter influences the sound wave's propagation and scattering in the presence of trees.
|
|
49
|
+
Lower frequencies travel longer distances than higher frequencies.
|
|
50
|
+
It's recommended to use values between 63 Hz and 8000 Hz; values outside this range will be clamped to the
|
|
51
|
+
nearest boundary for the sound absorption coefficient calculation.
|
|
45
52
|
|
|
46
53
|
Optional kwargs:
|
|
47
54
|
absorb_ratio_column (str, optional): The name of the column in the `obstacles` GeoDataFrame that contains the
|
|
@@ -88,12 +95,40 @@ def simulate_noise(
|
|
|
88
95
|
reflection_n = kwargs.get("reflection_n", 3)
|
|
89
96
|
dead_area_r = kwargs.get("dead_area_r", 5)
|
|
90
97
|
|
|
98
|
+
# Validate optional columns or default values
|
|
99
|
+
use_column_db = False
|
|
100
|
+
if "source_noise_db" in source_points.columns:
|
|
101
|
+
if (source_points["source_noise_db"] > MAX_DB_VALUE).any():
|
|
102
|
+
raise ValueError(
|
|
103
|
+
f"One or more values in 'source_noise_db' column exceed the physical limit of {MAX_DB_VALUE} dB."
|
|
104
|
+
)
|
|
105
|
+
use_column_db = True
|
|
106
|
+
|
|
107
|
+
use_column_freq = "geometric_mean_freq_hz" in source_points.columns
|
|
108
|
+
|
|
109
|
+
if not use_column_db:
|
|
110
|
+
if source_noise_db is None:
|
|
111
|
+
raise ValueError(
|
|
112
|
+
"Either `source_noise_db` must be provided or the `source_points` must contain a 'source_noise_db' column."
|
|
113
|
+
)
|
|
114
|
+
if source_noise_db > MAX_DB_VALUE:
|
|
115
|
+
raise ValueError(
|
|
116
|
+
f"source_noise_db ({source_noise_db} dB) exceeds the physical limit of {MAX_DB_VALUE} dB in air."
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if not use_column_freq:
|
|
120
|
+
if geometric_mean_freq_hz is None:
|
|
121
|
+
raise ValueError(
|
|
122
|
+
"Either `geometric_mean_freq_hz` must be provided or the `source_points` must contain a 'geometric_mean_freq_hz' column."
|
|
123
|
+
)
|
|
124
|
+
if not use_column_db and not use_column_freq and len(source_points) > 1:
|
|
125
|
+
logger.warning(
|
|
126
|
+
"`source_noise_db` and `geometric_mean_freq_hz` will be used for all points. Per-point simulation parameters not found."
|
|
127
|
+
)
|
|
128
|
+
|
|
91
129
|
original_crs = source_points.crs
|
|
130
|
+
source_points = source_points.copy()
|
|
92
131
|
|
|
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
132
|
source_points = source_points.copy()
|
|
98
133
|
if len(obstacles) > 0:
|
|
99
134
|
obstacles = obstacles.copy()
|
|
@@ -118,63 +153,54 @@ def simulate_noise(
|
|
|
118
153
|
if absorb_ratio_column is None:
|
|
119
154
|
obstacles["absorb_ratio"] = standart_absorb_ratio
|
|
120
155
|
else:
|
|
121
|
-
obstacles["absorb_ratio"] = obstacles[absorb_ratio_column]
|
|
122
|
-
obstacles["absorb_ratio"] = obstacles["absorb_ratio"].fillna(standart_absorb_ratio)
|
|
156
|
+
obstacles["absorb_ratio"] = obstacles[absorb_ratio_column].fillna(standart_absorb_ratio)
|
|
123
157
|
obstacles = obstacles[["absorb_ratio", "geometry"]]
|
|
124
158
|
|
|
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
159
|
# creating initial task and simulating for each point
|
|
144
|
-
|
|
160
|
+
task_queue = multiprocessing.Queue()
|
|
161
|
+
dead_area_dict = {}
|
|
145
162
|
for ind, row in source_points.iterrows():
|
|
146
|
-
logger.info(f"Started simulation for point {ind+1} / {len(source_points)}")
|
|
147
163
|
source_point = row.geometry
|
|
148
|
-
|
|
164
|
+
local_db = row["source_noise_db"] if use_column_db else source_noise_db
|
|
165
|
+
local_freq = row["geometric_mean_freq_hz"] if use_column_freq else geometric_mean_freq_hz
|
|
166
|
+
div_ = (local_db - target_noise_db) % db_sim_step
|
|
167
|
+
if div_ != 0:
|
|
168
|
+
raise InvalidStepError(local_db, target_noise_db, db_sim_step, div_)
|
|
169
|
+
# calculating layer dist and db values
|
|
170
|
+
dist_db = [(0, local_db)]
|
|
171
|
+
cur_db = local_db - db_sim_step
|
|
172
|
+
while cur_db != target_noise_db - db_sim_step:
|
|
173
|
+
max_dist = dist_to_target_db(local_db, cur_db, local_freq, air_temperature)
|
|
174
|
+
dist_db.append((max_dist, cur_db))
|
|
175
|
+
cur_db -= db_sim_step
|
|
176
|
+
|
|
149
177
|
args = (source_point, obstacles, trees, 0, 0, dist_db)
|
|
150
178
|
kwargs = {
|
|
151
179
|
"reflection_n": reflection_n,
|
|
152
|
-
"geometric_mean_freq_hz":
|
|
180
|
+
"geometric_mean_freq_hz": local_freq,
|
|
153
181
|
"tree_res": tree_res,
|
|
154
182
|
"min_db": target_noise_db,
|
|
183
|
+
"simulation_ind": ind,
|
|
155
184
|
}
|
|
156
185
|
task_queue.put((_noise_from_point_task, args, kwargs))
|
|
186
|
+
dead_area_dict[ind] = source_point.buffer(dead_area_r, resolution=2)
|
|
157
187
|
|
|
158
|
-
|
|
159
|
-
task_queue, dead_area=source_point.buffer(dead_area_r, resolution=2), dead_area_r=dead_area_r
|
|
160
|
-
)
|
|
188
|
+
noise_gdf = _parallel_split_queue(task_queue, dead_area_dict=dead_area_dict, dead_area_r=dead_area_r)
|
|
161
189
|
|
|
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)
|
|
190
|
+
noise_gdf = gpd.GeoDataFrame(pd.concat(noise_gdf, ignore_index=True), crs=local_crs)
|
|
191
|
+
polygons = gpd.GeoDataFrame(
|
|
192
|
+
geometry=list(polygonize(noise_gdf.geometry.apply(polygons_to_multilinestring).union_all())), crs=local_crs
|
|
193
|
+
)
|
|
194
|
+
polygons_points = polygons.copy()
|
|
195
|
+
polygons_points.geometry = polygons.representative_point()
|
|
196
|
+
sim_result = polygons_points.sjoin(noise_gdf, predicate="within").reset_index()
|
|
197
|
+
sim_result = sim_result.groupby("index").agg({"noise_level": "max"})
|
|
198
|
+
sim_result["geometry"] = polygons
|
|
199
|
+
sim_result = (
|
|
200
|
+
gpd.GeoDataFrame(sim_result, geometry="geometry", crs=local_crs).dissolve(by="noise_level").reset_index()
|
|
201
|
+
)
|
|
176
202
|
|
|
177
|
-
return
|
|
203
|
+
return sim_result.to_crs(original_crs)
|
|
178
204
|
|
|
179
205
|
|
|
180
206
|
def _noise_from_point_task(task, **kwargs) -> tuple[gpd.GeoDataFrame, list[tuple] | None]: # pragma: no cover
|
|
@@ -383,34 +409,34 @@ def _noise_from_point_task(task, **kwargs) -> tuple[gpd.GeoDataFrame, list[tuple
|
|
|
383
409
|
return noise_from_point, new_tasks
|
|
384
410
|
|
|
385
411
|
|
|
386
|
-
def _parallel_split_queue(task_queue: multiprocessing.Queue,
|
|
412
|
+
def _parallel_split_queue(task_queue: multiprocessing.Queue, dead_area_dict: dict, dead_area_r: int):
|
|
387
413
|
results = []
|
|
388
414
|
total_tasks = task_queue.qsize()
|
|
389
415
|
|
|
390
416
|
with tqdm(total=total_tasks, desc="Simulating noise") as pbar:
|
|
391
417
|
with concurrent.futures.ProcessPoolExecutor() as executor:
|
|
418
|
+
# with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
392
419
|
future_to_task = {}
|
|
393
420
|
while True:
|
|
394
421
|
while not task_queue.empty() and len(future_to_task) < executor._max_workers:
|
|
395
422
|
func, task, kwargs = task_queue.get_nowait()
|
|
396
423
|
future = executor.submit(func, task, **kwargs)
|
|
397
|
-
future_to_task[future] =
|
|
398
|
-
|
|
424
|
+
future_to_task[future] = kwargs["simulation_ind"]
|
|
399
425
|
done, _ = concurrent.futures.wait(future_to_task.keys(), return_when=concurrent.futures.FIRST_COMPLETED)
|
|
400
|
-
|
|
401
426
|
for future in done:
|
|
402
|
-
future_to_task.pop(future)
|
|
427
|
+
simulation_ind = future_to_task.pop(future)
|
|
403
428
|
result, new_tasks = future.result()
|
|
404
429
|
if new_tasks:
|
|
405
430
|
new_tasks_n = 0
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
431
|
+
local_dead_area = dead_area_dict.get(simulation_ind)
|
|
432
|
+
new_dead_area_points = [local_dead_area]
|
|
433
|
+
for func, new_task, new_kwargs in new_tasks:
|
|
434
|
+
new_point = new_task[0]
|
|
435
|
+
if not local_dead_area.covers(new_point):
|
|
436
|
+
task_queue.put((func, new_task, new_kwargs))
|
|
437
|
+
new_dead_area_points.append(new_point.buffer(dead_area_r, resolution=2))
|
|
438
|
+
new_tasks_n += 1
|
|
439
|
+
dead_area_dict[simulation_ind] = unary_union(new_dead_area_points)
|
|
414
440
|
total_tasks += new_tasks_n
|
|
415
441
|
pbar.total = total_tasks
|
|
416
442
|
pbar.refresh()
|
|
@@ -419,5 +445,4 @@ def _parallel_split_queue(task_queue: multiprocessing.Queue, dead_area: Polygon,
|
|
|
419
445
|
time.sleep(0.01)
|
|
420
446
|
if not future_to_task and task_queue.empty():
|
|
421
447
|
break
|
|
422
|
-
|
|
423
448
|
return results
|
|
@@ -90,6 +90,28 @@ def graph_to_gdf(
|
|
|
90
90
|
|
|
91
91
|
|
|
92
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
|
+
"""
|
|
93
115
|
nodes_with_data = list(nx_graph.nodes(data=True))
|
|
94
116
|
try:
|
|
95
117
|
coordinates = np.array([(data["x"], data["y"]) for node, data in nodes_with_data])
|
|
@@ -103,6 +125,24 @@ def get_closest_nodes_from_gdf(gdf: gpd.GeoDataFrame, nx_graph: nx.Graph) -> tup
|
|
|
103
125
|
|
|
104
126
|
|
|
105
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
|
+
"""
|
|
106
146
|
graph = graph.copy()
|
|
107
147
|
|
|
108
148
|
weakly_connected_components = list(nx.weakly_connected_components(graph))
|
|
@@ -125,3 +165,42 @@ def remove_weakly_connected_nodes(graph: nx.DiGraph) -> nx.DiGraph:
|
|
|
125
165
|
graph.remove_nodes_from(nodes_to_del)
|
|
126
166
|
|
|
127
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 +0,0 @@
|
|
|
1
|
-
VERSION = "1.0.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|