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.

Files changed (36) hide show
  1. objectnat/_api.py +6 -8
  2. objectnat/_config.py +0 -24
  3. objectnat/_version.py +1 -1
  4. objectnat/methods/coverage_zones/__init__.py +2 -0
  5. objectnat/methods/coverage_zones/graph_coverage.py +118 -0
  6. objectnat/methods/coverage_zones/radius_voronoi.py +45 -0
  7. objectnat/methods/isochrones/__init__.py +1 -0
  8. objectnat/methods/isochrones/isochrone_utils.py +130 -0
  9. objectnat/methods/isochrones/isochrones.py +325 -0
  10. objectnat/methods/noise/__init__.py +3 -0
  11. objectnat/methods/noise/noise_exceptions.py +14 -0
  12. objectnat/methods/noise/noise_init_data.py +10 -0
  13. objectnat/methods/noise/noise_reduce.py +155 -0
  14. objectnat/methods/noise/noise_sim.py +423 -0
  15. objectnat/methods/point_clustering/__init__.py +1 -0
  16. objectnat/methods/{cluster_points_in_polygons.py → point_clustering/cluster_points_in_polygons.py} +22 -28
  17. objectnat/methods/provision/__init__.py +1 -0
  18. objectnat/methods/provision/provision.py +10 -7
  19. objectnat/methods/provision/provision_exceptions.py +4 -4
  20. objectnat/methods/provision/provision_model.py +21 -20
  21. objectnat/methods/utils/__init__.py +0 -0
  22. objectnat/methods/utils/geom_utils.py +130 -0
  23. objectnat/methods/utils/graph_utils.py +127 -0
  24. objectnat/methods/utils/math_utils.py +32 -0
  25. objectnat/methods/visibility/__init__.py +6 -0
  26. objectnat/methods/{visibility_analysis.py → visibility/visibility_analysis.py} +222 -243
  27. objectnat-1.0.0.dist-info/METADATA +143 -0
  28. objectnat-1.0.0.dist-info/RECORD +32 -0
  29. objectnat/methods/balanced_buildings.py +0 -69
  30. objectnat/methods/coverage_zones.py +0 -90
  31. objectnat/methods/isochrones.py +0 -143
  32. objectnat/methods/living_buildings_osm.py +0 -168
  33. objectnat-0.2.6.dist-info/METADATA +0 -113
  34. objectnat-0.2.6.dist-info/RECORD +0 -19
  35. {objectnat-0.2.6.dist-info → objectnat-1.0.0.dist-info}/LICENSE.txt +0 -0
  36. {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
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
27
+ [![PyPI version](https://img.shields.io/pypi/v/objectnat.svg)](https://pypi.org/project/objectnat/)
28
+ [![CI](https://github.com/DDonnyy/ObjectNat/actions/workflows/ci_pipeline.yml/badge.svg)](https://github.com/DDonnyy/ObjecNat/actions/workflows/ci_pipeline.yml)
29
+ [![codecov](https://codecov.io/gh/DDonnyy/ObjectNat/graph/badge.svg?token=K6JFSJ02GU)](https://codecov.io/gh/DDonnyy/ObjectNat)
30
+ [![License](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](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
@@ -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
- ]