ObjectNat 0.2.6__py3-none-any.whl → 1.0.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 +6 -8
- objectnat/_config.py +0 -24
- objectnat/_version.py +1 -1
- objectnat/methods/coverage_zones/__init__.py +2 -0
- objectnat/methods/coverage_zones/graph_coverage.py +118 -0
- objectnat/methods/coverage_zones/radius_voronoi.py +45 -0
- objectnat/methods/isochrones/__init__.py +1 -0
- objectnat/methods/isochrones/isochrone_utils.py +130 -0
- objectnat/methods/isochrones/isochrones.py +325 -0
- objectnat/methods/noise/__init__.py +3 -0
- objectnat/methods/noise/noise_exceptions.py +14 -0
- objectnat/methods/noise/noise_init_data.py +10 -0
- objectnat/methods/noise/noise_reduce.py +155 -0
- objectnat/methods/noise/noise_sim.py +423 -0
- objectnat/methods/point_clustering/__init__.py +1 -0
- objectnat/methods/{cluster_points_in_polygons.py → point_clustering/cluster_points_in_polygons.py} +22 -28
- objectnat/methods/provision/__init__.py +1 -0
- objectnat/methods/provision/provision.py +10 -7
- objectnat/methods/provision/provision_exceptions.py +4 -4
- objectnat/methods/provision/provision_model.py +21 -20
- objectnat/methods/utils/__init__.py +0 -0
- objectnat/methods/utils/geom_utils.py +130 -0
- objectnat/methods/utils/graph_utils.py +127 -0
- objectnat/methods/utils/math_utils.py +32 -0
- objectnat/methods/visibility/__init__.py +6 -0
- objectnat/methods/{visibility_analysis.py → visibility/visibility_analysis.py} +222 -243
- objectnat-1.0.0.dist-info/METADATA +143 -0
- objectnat-1.0.0.dist-info/RECORD +32 -0
- objectnat/methods/balanced_buildings.py +0 -69
- objectnat/methods/coverage_zones.py +0 -90
- objectnat/methods/isochrones.py +0 -143
- objectnat/methods/living_buildings_osm.py +0 -168
- objectnat-0.2.6.dist-info/METADATA +0 -113
- objectnat-0.2.6.dist-info/RECORD +0 -19
- {objectnat-0.2.6.dist-info → objectnat-1.0.0.dist-info}/LICENSE.txt +0 -0
- {objectnat-0.2.6.dist-info → objectnat-1.0.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ObjectNat
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: ObjectNat is an open-source library created for geospatial analysis created by IDU team
|
|
5
|
+
License: BSD-3-Clause
|
|
6
|
+
Author: DDonnyy
|
|
7
|
+
Author-email: 63115678+DDonnyy@users.noreply.github.com
|
|
8
|
+
Requires-Python: >=3.10,<3.13
|
|
9
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Requires-Dist: geopandas (>=1.0.1,<2.0.0)
|
|
15
|
+
Requires-Dist: networkx (>=3.4.2,<4.0.0)
|
|
16
|
+
Requires-Dist: numpy (>=2.1.3,<3.0.0)
|
|
17
|
+
Requires-Dist: pandarallel (>=1.6.5,<2.0.0)
|
|
18
|
+
Requires-Dist: pandas (>=2.2.0,<3.0.0)
|
|
19
|
+
Requires-Dist: pytest (>=8.3.5,<9.0.0)
|
|
20
|
+
Requires-Dist: scikit-learn (>=1.4.0,<2.0.0)
|
|
21
|
+
Requires-Dist: tqdm (>=4.66.2,<5.0.0)
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# ObjectNat
|
|
25
|
+
|
|
26
|
+
[](https://github.com/psf/black)
|
|
27
|
+
[](https://pypi.org/project/objectnat/)
|
|
28
|
+
[](https://github.com/DDonnyy/ObjecNat/actions/workflows/ci_pipeline.yml)
|
|
29
|
+
[](https://codecov.io/gh/DDonnyy/ObjectNat)
|
|
30
|
+
[](https://opensource.org/licenses/MIT)
|
|
31
|
+
|
|
32
|
+
- [РИДМИ (Russian)](README_ru.md)
|
|
33
|
+
<p align="center">
|
|
34
|
+
<img src="https://github.com/user-attachments/assets/ed0f226e-1728-4659-9e21-b4d499e703cd" alt="logo" width="400">
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
#### **ObjectNat** is an open-source library created for geospatial analysis created by **IDU team**
|
|
38
|
+
|
|
39
|
+
## Features and how to use
|
|
40
|
+
|
|
41
|
+
1. **[Isochrones and Transport Accessibility](./examples/isochrone_generator.ipynb)** — Isochrones represent areas reachable from a starting point within a given time limit along a transport network. This function enables analysis of transport accessibility using pedestrian, automobile, public transport graphs, or their combination.
|
|
42
|
+
|
|
43
|
+
The library offers multiple isochrone generation methods:
|
|
44
|
+
- **Baseline isochrones**: show a single area reachable within a specified time.
|
|
45
|
+
- **Stepped isochrones**: show accessibility ranges divided into time intervals (e.g., 5, 10, 15 minutes).
|
|
46
|
+
|
|
47
|
+
<p align="center">
|
|
48
|
+
<img src="https://github.com/user-attachments/assets/b1787430-63e1-4907-9198-a6171d546599" alt="isochrone_ways_15_min" width="300">
|
|
49
|
+
<img src="https://github.com/user-attachments/assets/64fce6bf-6509-490c-928c-dbd8daf9f570" alt="isochrone_radius_15_min" width="300">
|
|
50
|
+
</p>
|
|
51
|
+
<p align="center">
|
|
52
|
+
<img src="https://github.com/user-attachments/assets/ac9f8840-a867-4eb5-aec8-91a411d4e545" alt="stepped_isochrone_stepped_ways_15_min" width="300">
|
|
53
|
+
<img src="https://github.com/user-attachments/assets/b5429aa1-4625-44d1-982f-8bd4264148fb" alt="stepped_isochrone_stepped_radius_15_min" width="300">
|
|
54
|
+
<img src="https://github.com/user-attachments/assets/042c7362-70e1-45df-b2e1-02fc76bf638c" alt="stepped_isochrone_stepped_separate_15_min" width="300">
|
|
55
|
+
</p>
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
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.
|
|
59
|
+
|
|
60
|
+
<p align="center">
|
|
61
|
+
<img src="https://github.com/user-attachments/assets/fa8057d7-77aa-48a2-aa10-ea3e292a918d" alt="coverage_zones_time_10min" width="350">
|
|
62
|
+
<img src="https://github.com/user-attachments/assets/44362dde-c3b0-4321-9a0a-aa547f0f2e04" alt="coverage_zones_distance_600m" width="350">
|
|
63
|
+
</p>
|
|
64
|
+
|
|
65
|
+
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)
|
|
66
|
+
that have limited **capacity** and a defined **accessibility threshold** (in minutes or distance). The function models **demand-supply balance**, estimating how well services meet the needs of nearby buildings within the allowed time.
|
|
67
|
+
|
|
68
|
+
The library also supports:
|
|
69
|
+
- **Recalculation** of existing provision results using a new time threshold.
|
|
70
|
+
- **Clipping** of provision results to a custom analysis area (e.g., administrative boundaries).
|
|
71
|
+
|
|
72
|
+
<p align="center">
|
|
73
|
+
<img src="https://github.com/user-attachments/assets/ff1ed08d-9a35-4035-9e1f-9a7fdae5b0e0" alt="service_provision_initial" width="300">
|
|
74
|
+
<img src="https://github.com/user-attachments/assets/a0c0a6b0-f83f-4982-bfb3-4a476b2153ea" alt="service_provision_recalculated" width="300">
|
|
75
|
+
<img src="https://github.com/user-attachments/assets/f57dc1c6-21a0-458d-85f4-fe1b17c77695" alt="service_provision_clipped" width="300">
|
|
76
|
+
</p>
|
|
77
|
+
|
|
78
|
+
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.
|
|
79
|
+
This can be used to assess visual accessibility in urban environments.
|
|
80
|
+
The library also includes a **catchment area calculator** for large-scale visibility analysis based on a dense grid of observer points (recommended: ~1000 points spaced 10–20 meters apart).
|
|
81
|
+
Points can be generated using a road network and distributed along edges.
|
|
82
|
+
|
|
83
|
+
The module includes:
|
|
84
|
+
- A **fast approximate method** for large datasets.
|
|
85
|
+
- A **accurate method** for detailed local analysis.
|
|
86
|
+
|
|
87
|
+
<p align="center">
|
|
88
|
+
<img src="https://github.com/user-attachments/assets/aa139d29-07d4-4560-b835-9646c8802fe1" alt="visibility_comparison_methods" height="250">
|
|
89
|
+
<img src="https://github.com/user-attachments/assets/b5b0d4b3-a02f-4ade-8772-475703cd6435" alt="visibility-catchment-area" height="250">
|
|
90
|
+
</p>
|
|
91
|
+
|
|
92
|
+
5. **[Noise Simulation](./examples/noise_simulation.ipynb)** — Simulates noise propagation from a set of source points, taking into account **obstacles**, **vegetation**, and **environmental factors**.
|
|
93
|
+
|
|
94
|
+
🔗 **[See detailed explanation in the Wiki](https://github.com/DDonnyy/ObjectNat/wiki/Noise-simulation)**
|
|
95
|
+
|
|
96
|
+
<p align="center">
|
|
97
|
+
<img src="https://github.com/user-attachments/assets/b3a41962-6220-49c4-90d4-2e756f9706cf" alt="noise_simulation_test_result" width="400">
|
|
98
|
+
</p>
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
6. **[Point Clusterization](./examples/point_clusterization.ipynb)** — Function to generate **cluster polygons** from a set of input points based on:
|
|
102
|
+
- Minimum **distance** between points.
|
|
103
|
+
- Minimum **number of points** per cluster.
|
|
104
|
+
|
|
105
|
+
Additionally, the function can calculate the **relative ratio** of different service types within each cluster, enabling spatial analysis of service composition.
|
|
106
|
+
|
|
107
|
+
<p align="center">
|
|
108
|
+
<img src="https://github.com/user-attachments/assets/f86aac61-497a-4330-b4cf-68f4fc47fd34" alt="building_clusters" width="400">
|
|
109
|
+
</p>
|
|
110
|
+
|
|
111
|
+
## City graphs
|
|
112
|
+
|
|
113
|
+
To ensure optimal performance of ObjectNat's geospatial analysis functions, it's recommended to utilize urban graphs sourced from the [IduEdu](https://github.com/DDonnyy/IduEdu) library.
|
|
114
|
+
**IduEdu** is an open-source Python library designed for the creation and manipulation of complex city networks derived from OpenStreetMap data.
|
|
115
|
+
|
|
116
|
+
**IduEdu** can be installed with ``pip``:
|
|
117
|
+
```
|
|
118
|
+
pip install IduEdu
|
|
119
|
+
```
|
|
120
|
+
## Installation
|
|
121
|
+
|
|
122
|
+
**ObjectNat** can be installed with ``pip``:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
pip install ObjectNat
|
|
126
|
+
```
|
|
127
|
+
### Configuration changes
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from objectnat import config
|
|
131
|
+
|
|
132
|
+
config.change_logger_lvl('INFO') # To mute all debug msgs
|
|
133
|
+
config.set_enable_tqdm(False) # To mute all tqdm's progress bars
|
|
134
|
+
```
|
|
135
|
+
## Contacts
|
|
136
|
+
|
|
137
|
+
- [NCCR](https://actcognitive.org/) - National Center for Cognitive Research
|
|
138
|
+
- [IDU](https://idu.itmo.ru/) - Institute of Design and Urban Studies
|
|
139
|
+
- [Natalya Chichkova](https://t.me/nancy_nat) - project manager
|
|
140
|
+
- [Danila Oleynikov (Donny)](https://t.me/ddonny_dd) - lead software engineer
|
|
141
|
+
|
|
142
|
+
## Publications
|
|
143
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
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=lzGFsymf0DtA_1oAZcPbeQ557iY-1BRkekhfA2qaFh8,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=KubqeysYetj1dbWJ_WGrewfM_jWYmRFjj8AWz8aEc1I,4825
|
|
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.0.dist-info/LICENSE.txt,sha256=yPEioMfTd7JAQgAU6J13inS1BSjwd82HFlRSoIb4My8,1498
|
|
30
|
+
objectnat-1.0.0.dist-info/METADATA,sha256=ybm0Wwz002c0AWq9qJw6g6ZTveQB4yZJLekYC3kB70I,8079
|
|
31
|
+
objectnat-1.0.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
32
|
+
objectnat-1.0.0.dist-info/RECORD,,
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import geopandas as gpd
|
|
2
|
-
import population_restorator.balancer.houses as b_build
|
|
3
|
-
import population_restorator.balancer.territories as b_terr
|
|
4
|
-
|
|
5
|
-
from objectnat import config
|
|
6
|
-
|
|
7
|
-
logger = config.logger
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def get_balanced_buildings(
|
|
11
|
-
living_buildings: gpd.GeoDataFrame,
|
|
12
|
-
population: int,
|
|
13
|
-
territories: gpd.GeoDataFrame | None = None,
|
|
14
|
-
) -> gpd.GeoDataFrame:
|
|
15
|
-
"""
|
|
16
|
-
Balance city population into living buildings according to their spot area * `storeys_count`.
|
|
17
|
-
|
|
18
|
-
Args:
|
|
19
|
-
living_buildings (gpd.GeoDataFrame): (multi)polygons of buildings where people live. Must contain either
|
|
20
|
-
"storeys_count" or "living_area" attributes.
|
|
21
|
-
|
|
22
|
-
population (int): Total city population
|
|
23
|
-
|
|
24
|
-
territories (gpd.GeoDataFrame, optional): If more detailed population is known, it can be set via this parameter
|
|
25
|
-
in "population" field. In case territories are given, only buildings inside them will be populated.
|
|
26
|
-
|
|
27
|
-
Returns:
|
|
28
|
-
gpd.GeoDataFrame: The balanced living buildings.
|
|
29
|
-
|
|
30
|
-
Raises:
|
|
31
|
-
ValueError: If population is missing, or if living buildings are not set,
|
|
32
|
-
or if living buildings are missing the `storeys_count` attribute, or if buildings contain no valid value.
|
|
33
|
-
"""
|
|
34
|
-
|
|
35
|
-
if population is None:
|
|
36
|
-
raise ValueError("Population value is missing")
|
|
37
|
-
if living_buildings.shape[0] == 0:
|
|
38
|
-
raise ValueError("Living buildings are not set")
|
|
39
|
-
if "living_area" not in living_buildings.columns and "storeys_count" not in living_buildings.columns:
|
|
40
|
-
raise ValueError("Living buildings are missing `storeys_count` attribute")
|
|
41
|
-
logger.debug(f"Evacuating {population} residents into the provided building")
|
|
42
|
-
indexes = living_buildings.index
|
|
43
|
-
living_buildings = living_buildings.copy()
|
|
44
|
-
living_buildings.reset_index(drop=True, inplace=True)
|
|
45
|
-
if "living_area" not in living_buildings.columns:
|
|
46
|
-
living_buildings["living_area"] = living_buildings.area * living_buildings["storeys_count"]
|
|
47
|
-
living_buildings = living_buildings[living_buildings["living_area"].notna()]
|
|
48
|
-
if living_buildings.shape[0] == 0:
|
|
49
|
-
raise ValueError("Buildings contain no valid value")
|
|
50
|
-
if territories is None:
|
|
51
|
-
city_territory = b_terr.Territory("city", population, None, living_buildings)
|
|
52
|
-
else:
|
|
53
|
-
inner_territories = [
|
|
54
|
-
b_terr.Territory(
|
|
55
|
-
f"terr_{i}",
|
|
56
|
-
terr.get("population"),
|
|
57
|
-
None,
|
|
58
|
-
living_buildings[terr.contains(living_buildings.centroid)].copy(),
|
|
59
|
-
)
|
|
60
|
-
for i, (_, terr) in enumerate(territories.iterrows())
|
|
61
|
-
]
|
|
62
|
-
city_territory = b_terr.Territory("city", population, inner_territories, None)
|
|
63
|
-
|
|
64
|
-
b_terr.balance_territories(city_territory)
|
|
65
|
-
b_build.balance_houses(city_territory)
|
|
66
|
-
|
|
67
|
-
houses = city_territory.get_all_houses()
|
|
68
|
-
houses.set_index(indexes, drop=True, inplace=True)
|
|
69
|
-
return houses
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
from typing import Literal
|
|
2
|
-
|
|
3
|
-
import geopandas as gpd
|
|
4
|
-
import networkx as nx
|
|
5
|
-
|
|
6
|
-
from .isochrones import get_accessibility_isochrones
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def get_radius_zone_coverage(services: gpd.GeoDataFrame, radius: int) -> gpd.GeoDataFrame:
|
|
10
|
-
"""
|
|
11
|
-
Create a buffer zone with a defined radius around each service location.
|
|
12
|
-
|
|
13
|
-
Parameters
|
|
14
|
-
----------
|
|
15
|
-
services : gpd.GeoDataFrame
|
|
16
|
-
GeoDataFrame containing the service locations.
|
|
17
|
-
radius : int
|
|
18
|
-
The radius for the buffer in meters.
|
|
19
|
-
|
|
20
|
-
Returns
|
|
21
|
-
-------
|
|
22
|
-
gpd.GeoDataFrame
|
|
23
|
-
GeoDataFrame with the buffer zones around each service location.
|
|
24
|
-
|
|
25
|
-
Examples
|
|
26
|
-
--------
|
|
27
|
-
>>> import geopandas as gpd
|
|
28
|
-
>>> from shapely.geometry import Point
|
|
29
|
-
|
|
30
|
-
>>> # Create a sample GeoDataFrame for services
|
|
31
|
-
>>> services = gpd.read_file('services.geojson')
|
|
32
|
-
|
|
33
|
-
>>> # Define the radius
|
|
34
|
-
>>> radius = 50
|
|
35
|
-
|
|
36
|
-
>>> # Get radius zone coverage
|
|
37
|
-
>>> radius_zones = get_radius_zone_coverage(services, radius)
|
|
38
|
-
>>> print(radius_zones)
|
|
39
|
-
"""
|
|
40
|
-
services["geometry"] = services["geometry"].buffer(radius)
|
|
41
|
-
return services
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def get_isochrone_zone_coverage(
|
|
45
|
-
services: gpd.GeoDataFrame,
|
|
46
|
-
weight_type: Literal["time_min", "length_meter"],
|
|
47
|
-
weight_value: int,
|
|
48
|
-
city_graph: nx.Graph,
|
|
49
|
-
) -> tuple[gpd.GeoDataFrame, gpd.GeoDataFrame | None, gpd.GeoDataFrame | None]:
|
|
50
|
-
"""
|
|
51
|
-
Create isochrones for each service location based on travel time/distance.
|
|
52
|
-
|
|
53
|
-
Parameters
|
|
54
|
-
----------
|
|
55
|
-
services : gpd.GeoDataFrame
|
|
56
|
-
Layer containing the service locations.
|
|
57
|
-
weight_type : str
|
|
58
|
-
Type of weight used for calculating isochrones, either "time_min" or "length_meter".
|
|
59
|
-
weight_value : int
|
|
60
|
-
The value of the weight, representing time in minutes or distance in meters.
|
|
61
|
-
city_graph : nx.Graph
|
|
62
|
-
The graph representing the city's transportation network.
|
|
63
|
-
|
|
64
|
-
Returns
|
|
65
|
-
-------
|
|
66
|
-
tuple[gpd.GeoDataFrame, gpd.GeoDataFrame | None, gpd.GeoDataFrame | None]
|
|
67
|
-
The calculated isochrone zones, optionally including routes and stops.
|
|
68
|
-
|
|
69
|
-
Examples
|
|
70
|
-
--------
|
|
71
|
-
>>> import networkx as nx
|
|
72
|
-
>>> import geopandas as gpd
|
|
73
|
-
>>> from shapely.geometry import Point
|
|
74
|
-
>>> from iduedu import get_intermodal_graph
|
|
75
|
-
|
|
76
|
-
>>> # Create a sample city graph with get_intermodal_graph()
|
|
77
|
-
>>> graph = get_intermodal_graph(polygon=my_territory_polygon)
|
|
78
|
-
|
|
79
|
-
>>> # Create a sample GeoDataFrame for services
|
|
80
|
-
>>> services = gpd.read_file('services.geojson')
|
|
81
|
-
|
|
82
|
-
>>> # Define parameters
|
|
83
|
-
>>> weight_type = "time_min"
|
|
84
|
-
>>> weight_value = 10
|
|
85
|
-
|
|
86
|
-
>>> # Get isochrone zone coverage
|
|
87
|
-
>>> isochrones, pt_stops, pt_routes = get_isochrone_zone_coverage(services, weight_type, weight_value, city_graph)
|
|
88
|
-
"""
|
|
89
|
-
iso, routes, stops = get_accessibility_isochrones(services, weight_value, weight_type, city_graph)
|
|
90
|
-
return iso, routes, stops
|
objectnat/methods/isochrones.py
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
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 osmnx import graph_to_gdfs
|
|
8
|
-
from pyproj import CRS
|
|
9
|
-
from scipy.spatial import KDTree
|
|
10
|
-
from shapely import Point
|
|
11
|
-
from shapely.ops import unary_union
|
|
12
|
-
|
|
13
|
-
from objectnat import config
|
|
14
|
-
|
|
15
|
-
logger = config.logger
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def get_accessibility_isochrones(
|
|
19
|
-
points: gpd.GeoDataFrame,
|
|
20
|
-
weight_value: float,
|
|
21
|
-
weight_type: Literal["time_min", "length_meter"],
|
|
22
|
-
graph_nx: nx.Graph,
|
|
23
|
-
) -> tuple[gpd.GeoDataFrame, gpd.GeoDataFrame | None, gpd.GeoDataFrame | None]:
|
|
24
|
-
"""
|
|
25
|
-
Calculate accessibility isochrones from a gpd.GeoDataFrame based on the provided city graph.
|
|
26
|
-
|
|
27
|
-
Isochrones represent areas that can be reached from a given point within a specific time or distance,
|
|
28
|
-
using a graph that contains road and transport network data.
|
|
29
|
-
|
|
30
|
-
Parameters
|
|
31
|
-
----------
|
|
32
|
-
points : gpd.GeoDataFrame
|
|
33
|
-
A GeoDataFrame containing the geometry from which accessibility isochrones should be calculated.
|
|
34
|
-
The CRS of this GeoDataFrame must match the CRS of the provided graph.
|
|
35
|
-
weight_value : float
|
|
36
|
-
The maximum distance or time threshold for calculating isochrones.
|
|
37
|
-
weight_type : Literal["time_min", "length_meter"]
|
|
38
|
-
The type of weight to use for distance calculations. Either time in minutes ("time_min") or distance
|
|
39
|
-
in meters ("length_meter").
|
|
40
|
-
graph_nx : nx.Graph
|
|
41
|
-
A NetworkX graph representing the city network.
|
|
42
|
-
The graph must contain the appropriate CRS and, for time-based isochrones, a speed attribute.
|
|
43
|
-
|
|
44
|
-
Returns
|
|
45
|
-
-------
|
|
46
|
-
tuple[gpd.GeoDataFrame, gpd.GeoDataFrame | None, gpd.GeoDataFrame | None]
|
|
47
|
-
A tuple containing:
|
|
48
|
-
- isochrones :
|
|
49
|
-
GeoDataFrame with the calculated isochrone geometries.
|
|
50
|
-
- public transport stops (if applicable) :
|
|
51
|
-
GeoDataFrame with public transport stops within the isochrone, or None if not applicable.
|
|
52
|
-
- public transport routes (if applicable) :
|
|
53
|
-
GeoDataFrame with public transport routes within the isochrone, or None if not applicable.
|
|
54
|
-
|
|
55
|
-
Examples
|
|
56
|
-
--------
|
|
57
|
-
>>> from iduedu import get_intermodal_graph
|
|
58
|
-
>>> graph = get_intermodal_graph(polygon=my_territory_polygon)
|
|
59
|
-
>>> points = gpd.GeoDataFrame(geometry=[Point(30.33, 59.95)], crs=4326).to_crs(graph.graph['crs'])
|
|
60
|
-
>>> isochrones, pt_stops, pt_routes = get_accessibility_isochrones(points,weight_value=15, weight_type="time_min", graph_nx=my_graph)
|
|
61
|
-
|
|
62
|
-
"""
|
|
63
|
-
|
|
64
|
-
assert points.crs == CRS.from_epsg(
|
|
65
|
-
graph_nx.graph["crs"]
|
|
66
|
-
), f'CRS mismatch , points.crs = {points.crs.to_epsg()}, graph["crs"] = {graph_nx.graph["crs"]}'
|
|
67
|
-
|
|
68
|
-
nodes_with_data = list(graph_nx.nodes(data=True))
|
|
69
|
-
logger.info("Calculating isochrones distances...")
|
|
70
|
-
coordinates = np.array([(data["x"], data["y"]) for node, data in nodes_with_data])
|
|
71
|
-
tree = KDTree(coordinates)
|
|
72
|
-
|
|
73
|
-
target_coord = [(p.x, p.y) for p in points.representative_point()]
|
|
74
|
-
distances, indices = tree.query(target_coord)
|
|
75
|
-
|
|
76
|
-
nearest_nodes = [nodes_with_data[idx][0] for idx in indices]
|
|
77
|
-
del nodes_with_data
|
|
78
|
-
dist_nearest = pd.DataFrame(data=distances, index=nearest_nodes, columns=["dist"])
|
|
79
|
-
speed = 0
|
|
80
|
-
if graph_nx.graph["type"] in ["walk", "intermodal"] and weight_type == "time_min":
|
|
81
|
-
try:
|
|
82
|
-
speed = graph_nx.graph["walk_speed"]
|
|
83
|
-
except KeyError:
|
|
84
|
-
logger.warning("There is no walk_speed in graph, set to the default speed - 83.33 m/min")
|
|
85
|
-
speed = 83.33
|
|
86
|
-
dist_nearest = dist_nearest / speed
|
|
87
|
-
elif weight_type == "time_min":
|
|
88
|
-
speed = 20 * 1000 / 60
|
|
89
|
-
dist_nearest = dist_nearest / speed
|
|
90
|
-
|
|
91
|
-
if (dist_nearest > weight_value).all().all():
|
|
92
|
-
raise RuntimeError(
|
|
93
|
-
"The point(s) lie further from the graph than weight_value, it's impossible to "
|
|
94
|
-
"construct isochrones. Check the coordinates of the point(s)/their projection"
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
data = []
|
|
98
|
-
for source in nearest_nodes:
|
|
99
|
-
dist, path = nx.single_source_dijkstra(graph_nx, source, weight=weight_type, cutoff=weight_value)
|
|
100
|
-
for target_node, way in path.items():
|
|
101
|
-
source = way[0]
|
|
102
|
-
distance = dist.get(target_node, np.nan)
|
|
103
|
-
data.append((source, target_node, distance))
|
|
104
|
-
del dist
|
|
105
|
-
dist_matrix = pd.DataFrame(data, columns=["source", "destination", "distance"])
|
|
106
|
-
del data
|
|
107
|
-
dist_matrix = dist_matrix.pivot_table(index="source", columns="destination", values="distance", sort=False)
|
|
108
|
-
|
|
109
|
-
dist_matrix = dist_matrix.add(dist_nearest.dist, axis=0)
|
|
110
|
-
dist_matrix = dist_matrix.mask(dist_matrix >= weight_value, np.nan)
|
|
111
|
-
dist_matrix.dropna(how="all", inplace=True)
|
|
112
|
-
|
|
113
|
-
results = []
|
|
114
|
-
logger.info("Building isochrones geometry...")
|
|
115
|
-
for _, row in dist_matrix.iterrows():
|
|
116
|
-
geometry = []
|
|
117
|
-
for node_to, value in row.items():
|
|
118
|
-
if not pd.isna(value):
|
|
119
|
-
node = graph_nx.nodes[node_to]
|
|
120
|
-
point = Point(node["x"], node["y"])
|
|
121
|
-
geometry.append(
|
|
122
|
-
point.buffer(round((weight_value - value) * speed * 0.8, 2))
|
|
123
|
-
if weight_type == "time_min"
|
|
124
|
-
else point.buffer(round((weight_value - value) * 0.8, 2))
|
|
125
|
-
)
|
|
126
|
-
geometry = unary_union(geometry)
|
|
127
|
-
results.append(geometry)
|
|
128
|
-
|
|
129
|
-
isochrones = gpd.GeoDataFrame(data=points, geometry=results, crs=graph_nx.graph["crs"])
|
|
130
|
-
isochrones["weight_type"] = weight_type
|
|
131
|
-
isochrones["weight_value"] = weight_value
|
|
132
|
-
|
|
133
|
-
isochrones_subgraph = graph_nx.subgraph(dist_matrix.columns)
|
|
134
|
-
nodes = pd.DataFrame.from_dict(dict(isochrones_subgraph.nodes(data=True)), orient="index")
|
|
135
|
-
if "desc" in nodes.columns and "stop" in nodes["desc"].unique():
|
|
136
|
-
pt_nodes = nodes[nodes["desc"] == "stop"]
|
|
137
|
-
nodes, edges = graph_to_gdfs(isochrones_subgraph.subgraph(pt_nodes.index))
|
|
138
|
-
nodes.reset_index(drop=True, inplace=True)
|
|
139
|
-
nodes = nodes[["desc", "route", "geometry"]]
|
|
140
|
-
edges.reset_index(drop=True, inplace=True)
|
|
141
|
-
edges = edges[["type", "route", "geometry"]]
|
|
142
|
-
return isochrones, nodes, edges
|
|
143
|
-
return isochrones, None, None
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import geopandas as gpd
|
|
2
|
-
import numpy as np
|
|
3
|
-
import osmnx as ox
|
|
4
|
-
import pandas as pd
|
|
5
|
-
from iduedu import get_boundary
|
|
6
|
-
from shapely import MultiPolygon, Polygon
|
|
7
|
-
|
|
8
|
-
from objectnat import config
|
|
9
|
-
|
|
10
|
-
logger = config.logger
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def eval_is_living(row: gpd.GeoSeries):
|
|
14
|
-
"""
|
|
15
|
-
Determine if a building is used for residential purposes based on its attributes.
|
|
16
|
-
|
|
17
|
-
Parameters
|
|
18
|
-
----------
|
|
19
|
-
row : gpd.GeoSeries
|
|
20
|
-
A GeoSeries representing a row in a GeoDataFrame, containing building attributes.
|
|
21
|
-
|
|
22
|
-
Returns
|
|
23
|
-
-------
|
|
24
|
-
bool
|
|
25
|
-
A boolean indicating whether the building is used for residential purposes.
|
|
26
|
-
|
|
27
|
-
Examples
|
|
28
|
-
--------
|
|
29
|
-
>>> buildings = download_buildings(osm_territory_id=421007)
|
|
30
|
-
>>> buildings['is_living'] = buildings.apply(eval_is_living, axis=1)
|
|
31
|
-
"""
|
|
32
|
-
return row["building"] in (
|
|
33
|
-
"apartments",
|
|
34
|
-
"house",
|
|
35
|
-
"residential",
|
|
36
|
-
"detached",
|
|
37
|
-
"dormitory",
|
|
38
|
-
"semidetached_house",
|
|
39
|
-
"bungalow",
|
|
40
|
-
"cabin",
|
|
41
|
-
"farm",
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def eval_population(source: gpd.GeoDataFrame, population_column: str, area_per_person: float = 33):
|
|
46
|
-
"""
|
|
47
|
-
Estimate the population of buildings in a GeoDataFrame based on their attributes.
|
|
48
|
-
|
|
49
|
-
Parameters
|
|
50
|
-
----------
|
|
51
|
-
source : gpd.GeoDataFrame
|
|
52
|
-
A GeoDataFrame containing building geometries and attributes.
|
|
53
|
-
population_column : str
|
|
54
|
-
The name of the column where the estimated population will be stored.
|
|
55
|
-
area_per_person : float
|
|
56
|
-
The standart living space per person im m², (default is 33)
|
|
57
|
-
Returns
|
|
58
|
-
-------
|
|
59
|
-
gpd.GeoDataFrame
|
|
60
|
-
A GeoDataFrame with an added column for estimated population.
|
|
61
|
-
|
|
62
|
-
Raises
|
|
63
|
-
------
|
|
64
|
-
RuntimeError
|
|
65
|
-
If the 'building:levels' column is not present in the provided GeoDataFrame.
|
|
66
|
-
|
|
67
|
-
Examples
|
|
68
|
-
--------
|
|
69
|
-
>>> source = gpd.read_file('buildings.shp')
|
|
70
|
-
>>> source['is_living'] = source.apply(eval_is_living, axis=1)
|
|
71
|
-
>>> population_df = eval_population(source, 'approximate_pop')
|
|
72
|
-
"""
|
|
73
|
-
if "building:levels" not in source.columns:
|
|
74
|
-
raise RuntimeError("No 'building:levels' column in provided GeoDataFrame")
|
|
75
|
-
df = source.copy()
|
|
76
|
-
local_crs = source.estimate_utm_crs()
|
|
77
|
-
df["area"] = df.to_crs(local_crs).geometry.area.astype(float)
|
|
78
|
-
df["building:levels_is_real"] = df["building:levels"].apply(lambda x: False if pd.isna(x) else True)
|
|
79
|
-
df["building:levels"] = df["building:levels"].fillna(1)
|
|
80
|
-
df["building:levels"] = pd.to_numeric(df["building:levels"], errors="coerce")
|
|
81
|
-
df = df.dropna(subset=["building:levels"])
|
|
82
|
-
df["building:levels"] = df["building:levels"].astype(int)
|
|
83
|
-
df[population_column] = np.nan
|
|
84
|
-
df.loc[df["is_living"] == 1, population_column] = (
|
|
85
|
-
df[df["is_living"] == 1]
|
|
86
|
-
.apply(
|
|
87
|
-
lambda row: (
|
|
88
|
-
3
|
|
89
|
-
if ((row["area"] <= 400) & (row["building:levels"] <= 2))
|
|
90
|
-
else (row["building:levels"] * row["area"] * 0.8 / area_per_person)
|
|
91
|
-
),
|
|
92
|
-
axis=1,
|
|
93
|
-
)
|
|
94
|
-
.round(0)
|
|
95
|
-
.astype(int)
|
|
96
|
-
)
|
|
97
|
-
return df
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def download_buildings(
|
|
101
|
-
osm_territory_id: int | None = None,
|
|
102
|
-
osm_territory_name: str | None = None,
|
|
103
|
-
terr_polygon: Polygon | MultiPolygon | None = None,
|
|
104
|
-
is_living_column: str = "is_living",
|
|
105
|
-
population_column: str = "approximate_pop",
|
|
106
|
-
area_per_person: float = 33,
|
|
107
|
-
) -> gpd.GeoDataFrame | None:
|
|
108
|
-
"""
|
|
109
|
-
Download building geometries and evaluate 'is_living' and 'population'
|
|
110
|
-
attributes for a specified territory from OpenStreetMap.
|
|
111
|
-
|
|
112
|
-
Parameters
|
|
113
|
-
----------
|
|
114
|
-
osm_territory_id : int, optional
|
|
115
|
-
The OpenStreetMap ID of the territory to download buildings for.
|
|
116
|
-
osm_territory_name : str, optional
|
|
117
|
-
The name of the territory to download buildings for.
|
|
118
|
-
terr_polygon : Polygon or MultiPolygon, optional
|
|
119
|
-
A Polygon or MultiPolygon geometry defining the territory to download buildings for.
|
|
120
|
-
is_living_column : str, optional
|
|
121
|
-
The name of the column indicating whether a building is residential (default is "is_living").
|
|
122
|
-
population_column : str, optional
|
|
123
|
-
The name of the column for storing estimated population (default is "approximate_pop").
|
|
124
|
-
area_per_person : float
|
|
125
|
-
The standart living space per person im m², (default is 33)
|
|
126
|
-
Returns
|
|
127
|
-
-------
|
|
128
|
-
gpd.GeoDataFrame or None
|
|
129
|
-
A GeoDataFrame containing building geometries and attributes, or None if no buildings are found or an error occurs.
|
|
130
|
-
|
|
131
|
-
Examples
|
|
132
|
-
--------
|
|
133
|
-
>>> buildings_df = download_buildings(osm_territory_name="Saint-Petersburg, Russia")
|
|
134
|
-
>>> buildings_df.head()
|
|
135
|
-
"""
|
|
136
|
-
polygon = get_boundary(osm_territory_id, osm_territory_name, terr_polygon)
|
|
137
|
-
|
|
138
|
-
logger.debug("Downloading buildings from OpenStreetMap and counting population...")
|
|
139
|
-
buildings = ox.features_from_polygon(polygon, tags={"building": True})
|
|
140
|
-
if not buildings.empty:
|
|
141
|
-
buildings = buildings.loc[
|
|
142
|
-
(buildings["geometry"].geom_type == "Polygon") | (buildings["geometry"].geom_type == "MultiPolygon")
|
|
143
|
-
]
|
|
144
|
-
if buildings.empty:
|
|
145
|
-
logger.warning("There are no buildings in the specified territory. Output GeoDataFrame is empty.")
|
|
146
|
-
return buildings
|
|
147
|
-
|
|
148
|
-
buildings[is_living_column] = buildings.apply(eval_is_living, axis=1)
|
|
149
|
-
buildings = eval_population(buildings, population_column, area_per_person)
|
|
150
|
-
buildings.reset_index(drop=True, inplace=True)
|
|
151
|
-
logger.debug("Done!")
|
|
152
|
-
return buildings[
|
|
153
|
-
[
|
|
154
|
-
"building",
|
|
155
|
-
"addr:street",
|
|
156
|
-
"addr:housenumber",
|
|
157
|
-
"amenity",
|
|
158
|
-
"area",
|
|
159
|
-
"name",
|
|
160
|
-
"building:levels",
|
|
161
|
-
"leisure",
|
|
162
|
-
"design:year",
|
|
163
|
-
is_living_column,
|
|
164
|
-
"building:levels_is_real",
|
|
165
|
-
population_column,
|
|
166
|
-
"geometry",
|
|
167
|
-
]
|
|
168
|
-
]
|