ObjectNat 1.4.1__tar.gz → 2.0.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.
- objectnat-2.0.0/.gitignore +59 -0
- {objectnat-1.4.1 → objectnat-2.0.0}/PKG-INFO +32 -40
- {objectnat-1.4.1 → objectnat-2.0.0}/README.rst +21 -23
- objectnat-2.0.0/objectnat/_api.py +20 -0
- {objectnat-1.4.1/src → objectnat-2.0.0}/objectnat/_config.py +2 -2
- objectnat-2.0.0/objectnat/_version.py +1 -0
- objectnat-2.0.0/objectnat/methods/accessibility/__init__.py +3 -0
- objectnat-2.0.0/objectnat/methods/accessibility/_utils.py +285 -0
- objectnat-2.0.0/objectnat/methods/accessibility/coverage.py +337 -0
- objectnat-2.0.0/objectnat/methods/accessibility/isochrones.py +321 -0
- objectnat-1.4.1/src/objectnat/methods/coverage_zones/radius_voronoi_coverage.py → objectnat-2.0.0/objectnat/methods/accessibility/radius.py +3 -0
- {objectnat-1.4.1/src → objectnat-2.0.0}/objectnat/methods/noise/__init__.py +1 -1
- {objectnat-1.4.1/src → objectnat-2.0.0}/objectnat/methods/noise/noise_reduce.py +23 -0
- {objectnat-1.4.1/src → objectnat-2.0.0}/objectnat/methods/noise/noise_simulation.py +181 -46
- {objectnat-1.4.1/src → objectnat-2.0.0}/objectnat/methods/noise/noise_simulation_simplified.py +2 -2
- objectnat-2.0.0/objectnat/methods/provision/__init__.py +7 -0
- objectnat-2.0.0/objectnat/methods/provision/provision.py +95 -0
- objectnat-2.0.0/objectnat/methods/provision/provision_calculating.py +388 -0
- {objectnat-1.4.1/src → objectnat-2.0.0}/objectnat/methods/provision/provision_exceptions.py +8 -0
- objectnat-2.0.0/objectnat/methods/provision/provision_result.py +174 -0
- objectnat-2.0.0/objectnat/methods/utils/__init__.py +0 -0
- {objectnat-1.4.1/src → objectnat-2.0.0}/objectnat/methods/utils/geom_utils.py +11 -20
- {objectnat-1.4.1/src → objectnat-2.0.0}/objectnat/methods/visibility/visibility_analysis.py +2 -237
- objectnat-2.0.0/pyproject.toml +174 -0
- objectnat-1.4.1/pyproject.toml +0 -81
- objectnat-1.4.1/src/objectnat/_api.py +0 -9
- objectnat-1.4.1/src/objectnat/_version.py +0 -1
- objectnat-1.4.1/src/objectnat/methods/coverage_zones/__init__.py +0 -3
- objectnat-1.4.1/src/objectnat/methods/coverage_zones/graph_coverage.py +0 -105
- objectnat-1.4.1/src/objectnat/methods/coverage_zones/stepped_coverage.py +0 -136
- objectnat-1.4.1/src/objectnat/methods/isochrones/__init__.py +0 -1
- objectnat-1.4.1/src/objectnat/methods/isochrones/isochrone_utils.py +0 -167
- objectnat-1.4.1/src/objectnat/methods/isochrones/isochrones.py +0 -282
- objectnat-1.4.1/src/objectnat/methods/point_clustering/__init__.py +0 -1
- objectnat-1.4.1/src/objectnat/methods/point_clustering/cluster_points_in_polygons.py +0 -115
- objectnat-1.4.1/src/objectnat/methods/provision/__init__.py +0 -1
- objectnat-1.4.1/src/objectnat/methods/provision/provision.py +0 -213
- objectnat-1.4.1/src/objectnat/methods/provision/provision_model.py +0 -323
- objectnat-1.4.1/src/objectnat/methods/utils/__init__.py +0 -1
- objectnat-1.4.1/src/objectnat/methods/utils/graph_utils.py +0 -306
- objectnat-1.4.1/src/objectnat/methods/utils/math_utils.py +0 -32
- {objectnat-1.4.1 → objectnat-2.0.0}/LICENSE.txt +0 -0
- {objectnat-1.4.1/src → objectnat-2.0.0}/objectnat/__init__.py +0 -0
- {objectnat-1.4.1/src → objectnat-2.0.0}/objectnat/methods/__init__.py +0 -0
- {objectnat-1.4.1/src → objectnat-2.0.0}/objectnat/methods/noise/noise_init_data.py +0 -0
- {objectnat-1.4.1/src → objectnat-2.0.0}/objectnat/methods/visibility/__init__.py +0 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Project caches
|
|
2
|
+
.iduedu_cache/
|
|
3
|
+
cache/
|
|
4
|
+
__pycache__/
|
|
5
|
+
.pytest_cache/
|
|
6
|
+
.mypy_cache/
|
|
7
|
+
.ruff_cache/
|
|
8
|
+
|
|
9
|
+
# Python bytecode
|
|
10
|
+
*.py[cod]
|
|
11
|
+
*$py.class
|
|
12
|
+
|
|
13
|
+
# Build / dist artifacts
|
|
14
|
+
/dist/
|
|
15
|
+
/build/
|
|
16
|
+
/*.egg-info/
|
|
17
|
+
|
|
18
|
+
# Virtual envs
|
|
19
|
+
/.venv/
|
|
20
|
+
/.env
|
|
21
|
+
|
|
22
|
+
# IDE / tools
|
|
23
|
+
/.idea/
|
|
24
|
+
/.vscode/
|
|
25
|
+
*.swp
|
|
26
|
+
*.swo
|
|
27
|
+
|
|
28
|
+
# Coverage / test artifacts
|
|
29
|
+
.coverage
|
|
30
|
+
.coverage.*
|
|
31
|
+
htmlcov/
|
|
32
|
+
coverage.xml
|
|
33
|
+
|
|
34
|
+
# Sphinx docs
|
|
35
|
+
docs/_build/
|
|
36
|
+
docs/_autoapi/
|
|
37
|
+
docs/api/generated/
|
|
38
|
+
docs/_autosummary/
|
|
39
|
+
docs/_doctrees/
|
|
40
|
+
docs/api/generated
|
|
41
|
+
_build/
|
|
42
|
+
_autosummary/
|
|
43
|
+
|
|
44
|
+
# Local dev stuff
|
|
45
|
+
/dev
|
|
46
|
+
.claude
|
|
47
|
+
.codex
|
|
48
|
+
|
|
49
|
+
# OS junk
|
|
50
|
+
.DS_Store
|
|
51
|
+
Thumbs.db
|
|
52
|
+
|
|
53
|
+
# test artifacts (rendered plots + downloaded/cached OSM graph)
|
|
54
|
+
/tests/test_output/
|
|
55
|
+
/tests/test_cache/
|
|
56
|
+
|
|
57
|
+
# graphify knowledge graph (generated locally, not committed)
|
|
58
|
+
/graphify-out/
|
|
59
|
+
|
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ObjectNat
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: ObjectNat is an open-source library created for geospatial analysis created by IDU team
|
|
5
|
-
|
|
5
|
+
Author-email: DDonnyy <63115678+DDonnyy@users.noreply.github.com>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
6
7
|
License-File: LICENSE.txt
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Requires-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
Requires-Dist:
|
|
15
|
-
Requires-Dist: loguru (>=0.7.3,<0.8.0)
|
|
16
|
-
Requires-Dist: networkx (>=3.4.2,<4.0.0)
|
|
17
|
-
Requires-Dist: numpy (>=2.1.3,<3.0.0)
|
|
18
|
-
Requires-Dist: pandas (>=2.2.0,<3.0.0)
|
|
19
|
-
Requires-Dist: scikit-learn (>=1.4.0,<2.0.0)
|
|
20
|
-
Requires-Dist: tqdm (>=4.66.2,<5.0.0)
|
|
8
|
+
Requires-Python: <3.13,>=3.11
|
|
9
|
+
Requires-Dist: geopandas<2.0.0,>=1.1.0
|
|
10
|
+
Requires-Dist: iduedu<3.0.0,>=2.0.0
|
|
11
|
+
Requires-Dist: loguru<0.8.0,>=0.7.3
|
|
12
|
+
Requires-Dist: numpy<3.0.0,>=2.4.0
|
|
13
|
+
Requires-Dist: pandas<4.0.0,>=3.0.0
|
|
14
|
+
Requires-Dist: scipy<2.0.0,>=1.17.0
|
|
15
|
+
Requires-Dist: tqdm<5.0.0,>=4.66.2
|
|
21
16
|
Description-Content-Type: text/x-rst
|
|
22
17
|
|
|
23
18
|
ObjectNat
|
|
@@ -34,12 +29,12 @@ Object-oriented Network Analysis Tools
|
|
|
34
29
|
:target: https://pypi.org/project/objectnat/
|
|
35
30
|
:alt: PyPI version
|
|
36
31
|
|
|
37
|
-
.. |badge-ci| image:: https://github.com/IDUclub/ObjectNat/actions/workflows/
|
|
38
|
-
:target: https://github.com/IDUclub/ObjectNat/actions/workflows/
|
|
39
|
-
:alt:
|
|
32
|
+
.. |badge-ci| image:: https://github.com/IDUclub/ObjectNat/actions/workflows/quality.yml/badge.svg
|
|
33
|
+
:target: https://github.com/IDUclub/ObjectNat/actions/workflows/quality.yml
|
|
34
|
+
:alt: Tests and coverage
|
|
40
35
|
|
|
41
|
-
.. |badge-codecov| image:: https://codecov.io/gh/
|
|
42
|
-
:target: https://codecov.io/gh/
|
|
36
|
+
.. |badge-codecov| image:: https://codecov.io/gh/IDUclub/ObjectNat/graph/badge.svg?token=K6JFSJ02GU
|
|
37
|
+
:target: https://codecov.io/gh/IDUclub/ObjectNat
|
|
43
38
|
:alt: Coverage
|
|
44
39
|
|
|
45
40
|
.. |badge-license| image:: https://img.shields.io/badge/license-BSD--3--Clause-blue.svg
|
|
@@ -75,7 +70,7 @@ Each feature includes a **Jupyter Notebook example** and **full documentation**.
|
|
|
75
70
|
|
|
76
71
|
Isochrones represent areas reachable from an origin point within a specified time along a transport network.
|
|
77
72
|
This feature allows the analysis of transport accessibility using pedestrian, road,
|
|
78
|
-
public transport, or multimodal
|
|
73
|
+
public transport, or multimodal ``UrbanGraph`` objects prepared by IduEdu.
|
|
79
74
|
|
|
80
75
|
The library supports several methods for building isochrones:
|
|
81
76
|
|
|
@@ -102,6 +97,8 @@ Each feature includes a **Jupyter Notebook example** and **full documentation**.
|
|
|
102
97
|
and a defined **accessibility threshold** (in minutes or meters).
|
|
103
98
|
The function models the **balance between supply and demand**,
|
|
104
99
|
assessing how well services meet the needs of nearby buildings within an acceptable time.
|
|
100
|
+
ObjectNat 2.0 returns a structured ``ProvisionResult`` with a sparse flow matrix
|
|
101
|
+
and materialization helpers for buildings, services, and link geometries.
|
|
105
102
|
|
|
106
103
|
📘 `Example <https://iduclub.github.io/ObjectNat/methods/examples/provision.html>`__
|
|
107
104
|
🔗 `Documentation <https://iduclub.github.io/ObjectNat/methods/provision.html>`__
|
|
@@ -110,9 +107,8 @@ Each feature includes a **Jupyter Notebook example** and **full documentation**.
|
|
|
110
107
|
|
|
111
108
|
A function for evaluating visibility from a given point or set of points to nearby buildings within a given radius.
|
|
112
109
|
It is used to assess visual accessibility in urban environments.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
Points can be generated along the transport network and distributed across its edges.
|
|
110
|
+
The unified ``get_visibility`` API supports accurate and simplified methods,
|
|
111
|
+
including parallel execution for batches of observer points.
|
|
116
112
|
|
|
117
113
|
📘 `Example <https://iduclub.github.io/ObjectNat/methods/examples/visibility.html>`__
|
|
118
114
|
🔗 `Documentation <https://iduclub.github.io/ObjectNat/methods/visibility.html>`__
|
|
@@ -126,19 +122,6 @@ Each feature includes a **Jupyter Notebook example** and **full documentation**.
|
|
|
126
122
|
🔗 `Documentation <https://iduclub.github.io/ObjectNat/methods/noise.html>`__
|
|
127
123
|
🧠 `Detailed theory <https://github.com/DDonnyy/ObjectNat/wiki/Noise-simulation>`__
|
|
128
124
|
|
|
129
|
-
6. **Point Clusterization**
|
|
130
|
-
|
|
131
|
-
A function for constructing **cluster polygons** based on a set of points using:
|
|
132
|
-
|
|
133
|
-
- Minimum **distance** between points.
|
|
134
|
-
- Minimum **number of points** in a cluster.
|
|
135
|
-
|
|
136
|
-
The function can also compute the **ratio of service types** in each cluster
|
|
137
|
-
for spatial analysis of service composition.
|
|
138
|
-
|
|
139
|
-
📘 `Example <https://iduclub.github.io/ObjectNat/methods/examples/clustering.html>`__
|
|
140
|
-
🔗 `Documentation <https://iduclub.github.io/ObjectNat/methods/clustering.html>`__
|
|
141
|
-
|
|
142
125
|
----
|
|
143
126
|
|
|
144
127
|
City Graphs via *IduEdu*
|
|
@@ -146,6 +129,8 @@ City Graphs via *IduEdu*
|
|
|
146
129
|
|
|
147
130
|
For optimal performance, **ObjectNat** is recommended to be used with graphs
|
|
148
131
|
created by the `IduEdu <https://github.com/IDUclub/IduEdu>`_ library.
|
|
132
|
+
Graph-based ObjectNat methods consume ``iduedu.UrbanGraph`` directly; ObjectNat
|
|
133
|
+
does not build NetworkX graphs internally.
|
|
149
134
|
|
|
150
135
|
**IduEdu** is an open-source Python library designed for building and processing
|
|
151
136
|
complex urban networks based on OpenStreetMap data.
|
|
@@ -160,7 +145,7 @@ Example usage::
|
|
|
160
145
|
from iduedu import get_4326_boundary, get_intermodal_graph
|
|
161
146
|
|
|
162
147
|
poly = get_4326_boundary(osm_id=1114252)
|
|
163
|
-
|
|
148
|
+
urban_graph = get_intermodal_graph(territory=poly, clip_by_territory=True)
|
|
164
149
|
|
|
165
150
|
----
|
|
166
151
|
|
|
@@ -183,6 +168,14 @@ You can adjust logging and progress bar output using the config module::
|
|
|
183
168
|
config.change_logger_lvl("INFO") # mute debug logs
|
|
184
169
|
config.set_enable_tqdm(False) # disable tqdm progress bars
|
|
185
170
|
|
|
171
|
+
Migration to ObjectNat 2.0
|
|
172
|
+
--------------------------
|
|
173
|
+
|
|
174
|
+
ObjectNat 2.0 replaces NetworkX-based graph inputs with ``iduedu.UrbanGraph``,
|
|
175
|
+
renames accessibility functions, and returns ``ProvisionResult`` from service
|
|
176
|
+
provision. See the migration guide in the documentation:
|
|
177
|
+
`Migration 1.x to 2.0 <https://iduclub.github.io/ObjectNat/migration_1_to_2.html>`__.
|
|
178
|
+
|
|
186
179
|
----
|
|
187
180
|
|
|
188
181
|
Contacts
|
|
@@ -199,4 +192,3 @@ Publications
|
|
|
199
192
|
------------
|
|
200
193
|
|
|
201
194
|
Coming soon.
|
|
202
|
-
|
|
@@ -12,12 +12,12 @@ Object-oriented Network Analysis Tools
|
|
|
12
12
|
:target: https://pypi.org/project/objectnat/
|
|
13
13
|
:alt: PyPI version
|
|
14
14
|
|
|
15
|
-
.. |badge-ci| image:: https://github.com/IDUclub/ObjectNat/actions/workflows/
|
|
16
|
-
:target: https://github.com/IDUclub/ObjectNat/actions/workflows/
|
|
17
|
-
:alt:
|
|
15
|
+
.. |badge-ci| image:: https://github.com/IDUclub/ObjectNat/actions/workflows/quality.yml/badge.svg
|
|
16
|
+
:target: https://github.com/IDUclub/ObjectNat/actions/workflows/quality.yml
|
|
17
|
+
:alt: Tests and coverage
|
|
18
18
|
|
|
19
|
-
.. |badge-codecov| image:: https://codecov.io/gh/
|
|
20
|
-
:target: https://codecov.io/gh/
|
|
19
|
+
.. |badge-codecov| image:: https://codecov.io/gh/IDUclub/ObjectNat/graph/badge.svg?token=K6JFSJ02GU
|
|
20
|
+
:target: https://codecov.io/gh/IDUclub/ObjectNat
|
|
21
21
|
:alt: Coverage
|
|
22
22
|
|
|
23
23
|
.. |badge-license| image:: https://img.shields.io/badge/license-BSD--3--Clause-blue.svg
|
|
@@ -53,7 +53,7 @@ Each feature includes a **Jupyter Notebook example** and **full documentation**.
|
|
|
53
53
|
|
|
54
54
|
Isochrones represent areas reachable from an origin point within a specified time along a transport network.
|
|
55
55
|
This feature allows the analysis of transport accessibility using pedestrian, road,
|
|
56
|
-
public transport, or multimodal
|
|
56
|
+
public transport, or multimodal ``UrbanGraph`` objects prepared by IduEdu.
|
|
57
57
|
|
|
58
58
|
The library supports several methods for building isochrones:
|
|
59
59
|
|
|
@@ -80,6 +80,8 @@ Each feature includes a **Jupyter Notebook example** and **full documentation**.
|
|
|
80
80
|
and a defined **accessibility threshold** (in minutes or meters).
|
|
81
81
|
The function models the **balance between supply and demand**,
|
|
82
82
|
assessing how well services meet the needs of nearby buildings within an acceptable time.
|
|
83
|
+
ObjectNat 2.0 returns a structured ``ProvisionResult`` with a sparse flow matrix
|
|
84
|
+
and materialization helpers for buildings, services, and link geometries.
|
|
83
85
|
|
|
84
86
|
📘 `Example <https://iduclub.github.io/ObjectNat/methods/examples/provision.html>`__
|
|
85
87
|
🔗 `Documentation <https://iduclub.github.io/ObjectNat/methods/provision.html>`__
|
|
@@ -88,9 +90,8 @@ Each feature includes a **Jupyter Notebook example** and **full documentation**.
|
|
|
88
90
|
|
|
89
91
|
A function for evaluating visibility from a given point or set of points to nearby buildings within a given radius.
|
|
90
92
|
It is used to assess visual accessibility in urban environments.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
Points can be generated along the transport network and distributed across its edges.
|
|
93
|
+
The unified ``get_visibility`` API supports accurate and simplified methods,
|
|
94
|
+
including parallel execution for batches of observer points.
|
|
94
95
|
|
|
95
96
|
📘 `Example <https://iduclub.github.io/ObjectNat/methods/examples/visibility.html>`__
|
|
96
97
|
🔗 `Documentation <https://iduclub.github.io/ObjectNat/methods/visibility.html>`__
|
|
@@ -104,19 +105,6 @@ Each feature includes a **Jupyter Notebook example** and **full documentation**.
|
|
|
104
105
|
🔗 `Documentation <https://iduclub.github.io/ObjectNat/methods/noise.html>`__
|
|
105
106
|
🧠 `Detailed theory <https://github.com/DDonnyy/ObjectNat/wiki/Noise-simulation>`__
|
|
106
107
|
|
|
107
|
-
6. **Point Clusterization**
|
|
108
|
-
|
|
109
|
-
A function for constructing **cluster polygons** based on a set of points using:
|
|
110
|
-
|
|
111
|
-
- Minimum **distance** between points.
|
|
112
|
-
- Minimum **number of points** in a cluster.
|
|
113
|
-
|
|
114
|
-
The function can also compute the **ratio of service types** in each cluster
|
|
115
|
-
for spatial analysis of service composition.
|
|
116
|
-
|
|
117
|
-
📘 `Example <https://iduclub.github.io/ObjectNat/methods/examples/clustering.html>`__
|
|
118
|
-
🔗 `Documentation <https://iduclub.github.io/ObjectNat/methods/clustering.html>`__
|
|
119
|
-
|
|
120
108
|
----
|
|
121
109
|
|
|
122
110
|
City Graphs via *IduEdu*
|
|
@@ -124,6 +112,8 @@ City Graphs via *IduEdu*
|
|
|
124
112
|
|
|
125
113
|
For optimal performance, **ObjectNat** is recommended to be used with graphs
|
|
126
114
|
created by the `IduEdu <https://github.com/IDUclub/IduEdu>`_ library.
|
|
115
|
+
Graph-based ObjectNat methods consume ``iduedu.UrbanGraph`` directly; ObjectNat
|
|
116
|
+
does not build NetworkX graphs internally.
|
|
127
117
|
|
|
128
118
|
**IduEdu** is an open-source Python library designed for building and processing
|
|
129
119
|
complex urban networks based on OpenStreetMap data.
|
|
@@ -138,7 +128,7 @@ Example usage::
|
|
|
138
128
|
from iduedu import get_4326_boundary, get_intermodal_graph
|
|
139
129
|
|
|
140
130
|
poly = get_4326_boundary(osm_id=1114252)
|
|
141
|
-
|
|
131
|
+
urban_graph = get_intermodal_graph(territory=poly, clip_by_territory=True)
|
|
142
132
|
|
|
143
133
|
----
|
|
144
134
|
|
|
@@ -161,6 +151,14 @@ You can adjust logging and progress bar output using the config module::
|
|
|
161
151
|
config.change_logger_lvl("INFO") # mute debug logs
|
|
162
152
|
config.set_enable_tqdm(False) # disable tqdm progress bars
|
|
163
153
|
|
|
154
|
+
Migration to ObjectNat 2.0
|
|
155
|
+
--------------------------
|
|
156
|
+
|
|
157
|
+
ObjectNat 2.0 replaces NetworkX-based graph inputs with ``iduedu.UrbanGraph``,
|
|
158
|
+
renames accessibility functions, and returns ``ProvisionResult`` from service
|
|
159
|
+
provision. See the migration guide in the documentation:
|
|
160
|
+
`Migration 1.x to 2.0 <https://iduclub.github.io/ObjectNat/migration_1_to_2.html>`__.
|
|
161
|
+
|
|
164
162
|
----
|
|
165
163
|
|
|
166
164
|
Contacts
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# pylint: disable=unused-import,wildcard-import,unused-wildcard-import
|
|
2
|
+
|
|
3
|
+
from .methods.accessibility import (
|
|
4
|
+
get_graph_coverage,
|
|
5
|
+
get_graph_isochrones,
|
|
6
|
+
get_radius_coverage,
|
|
7
|
+
get_stepped_graph_coverage,
|
|
8
|
+
get_stepped_graph_isochrones,
|
|
9
|
+
)
|
|
10
|
+
from .methods.noise import calculate_simplified_noise_frame, simulate_noise
|
|
11
|
+
from .methods.provision import (
|
|
12
|
+
ProvisionResult,
|
|
13
|
+
clip_provision,
|
|
14
|
+
get_provision_buildings,
|
|
15
|
+
get_provision_links,
|
|
16
|
+
get_provision_services,
|
|
17
|
+
get_service_provision,
|
|
18
|
+
recalculate_links,
|
|
19
|
+
)
|
|
20
|
+
from .methods.visibility import get_visibility
|
|
@@ -31,7 +31,7 @@ class Config:
|
|
|
31
31
|
self.enable_tqdm_bar = enable_tqdm_bar
|
|
32
32
|
self.logger = logger
|
|
33
33
|
|
|
34
|
-
def change_logger_lvl(self, lvl: Literal["TRACE", "DEBUG", "INFO", "
|
|
34
|
+
def change_logger_lvl(self, lvl: Literal["TRACE", "DEBUG", "INFO", "WARNING", "ERROR"]):
|
|
35
35
|
self.logger.remove()
|
|
36
36
|
self.logger.add(sys.stderr, level=lvl)
|
|
37
37
|
|
|
@@ -40,4 +40,4 @@ class Config:
|
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
config = Config()
|
|
43
|
-
config.change_logger_lvl("
|
|
43
|
+
config.change_logger_lvl("WARNING")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = "2.0.0"
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
from typing import Any, Literal
|
|
2
|
+
|
|
3
|
+
import geopandas as gpd
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from iduedu import UrbanGraph
|
|
7
|
+
from shapely import convex_hull
|
|
8
|
+
from shapely.geometry import GeometryCollection, MultiPolygon, Polygon
|
|
9
|
+
from shapely.ops import polygonize, unary_union
|
|
10
|
+
|
|
11
|
+
from objectnat.methods.utils.geom_utils import polygons_to_multilinestring
|
|
12
|
+
|
|
13
|
+
SteppedGeometryType = Literal["radius", "ways", "separate"] | None
|
|
14
|
+
|
|
15
|
+
# Graph types whose "ways" geometry must be built from pedestrian edges only.
|
|
16
|
+
# On these graphs non-walk edges (transit legs) are straight lines between
|
|
17
|
+
# distant stops; buffering them would create spurious corridors.
|
|
18
|
+
_WALK_ONLY_WAYS_GRAPH_TYPES = {"intermodal", "walk"}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def road_edges_for_ways(urban_graph: UrbanGraph) -> gpd.GeoDataFrame:
|
|
22
|
+
"""Return the edge subset used to build ``geometry_type="ways"`` geometry.
|
|
23
|
+
|
|
24
|
+
For intermodal/walk graphs only pedestrian (``type == "walk"``) edges are
|
|
25
|
+
kept, so transit legs do not distort the resulting shape. Other graph types
|
|
26
|
+
(or graphs without a ``type`` column) use every edge.
|
|
27
|
+
"""
|
|
28
|
+
edges = urban_graph.edges_gdf
|
|
29
|
+
if urban_graph.type in _WALK_ONLY_WAYS_GRAPH_TYPES and "type" in edges.columns:
|
|
30
|
+
walk_edges = edges[edges["type"] == "walk"]
|
|
31
|
+
if not walk_edges.empty:
|
|
32
|
+
return walk_edges
|
|
33
|
+
return edges
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def build_voronoi_cells(
|
|
37
|
+
urban_graph: UrbanGraph,
|
|
38
|
+
reachable_graph_nodes_gdf: gpd.GeoDataFrame,
|
|
39
|
+
*,
|
|
40
|
+
value_column: str,
|
|
41
|
+
local_crs: Any,
|
|
42
|
+
clip_geom=None,
|
|
43
|
+
) -> gpd.GeoDataFrame:
|
|
44
|
+
reachable_node_index = pd.Index(reachable_graph_nodes_gdf.index)
|
|
45
|
+
if clip_geom is None:
|
|
46
|
+
edges = urban_graph.edges_gdf[["u", "v"]]
|
|
47
|
+
edges_near_reachable = edges[edges["u"].isin(reachable_node_index) | edges["v"].isin(reachable_node_index)]
|
|
48
|
+
neighbor_nodes = pd.Index(edges_near_reachable["u"]).append(pd.Index(edges_near_reachable["v"])).unique()
|
|
49
|
+
boundary_nodes = neighbor_nodes.difference(reachable_node_index)
|
|
50
|
+
voronoi_node_index = reachable_node_index.append(boundary_nodes).unique()
|
|
51
|
+
else:
|
|
52
|
+
clip_mask = urban_graph.nodes_gdf.geometry.intersects(clip_geom)
|
|
53
|
+
voronoi_node_index = urban_graph.nodes_gdf.index[clip_mask].append(reachable_node_index).unique()
|
|
54
|
+
voronoi_nodes = urban_graph.nodes_gdf.loc[voronoi_node_index, ["geometry"]]
|
|
55
|
+
voronois = gpd.GeoDataFrame(geometry=list(voronoi_nodes.geometry.voronoi_polygons()), crs=local_crs)
|
|
56
|
+
reachable_cell_index = reachable_graph_nodes_gdf.reset_index(names="node").sjoin(voronois, how="inner")
|
|
57
|
+
return gpd.GeoDataFrame(
|
|
58
|
+
reachable_cell_index[["node", value_column]].reset_index(drop=True),
|
|
59
|
+
geometry=voronois.geometry.iloc[reachable_cell_index["index_right"].to_numpy()].reset_index(drop=True),
|
|
60
|
+
crs=local_crs,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def edge_speed_m_per_min(urban_graph: UrbanGraph) -> float:
|
|
65
|
+
edge_speeds = urban_graph.edges_gdf["length_meter"] / urban_graph.edges_gdf["time_min"]
|
|
66
|
+
edge_speeds = edge_speeds.replace([np.inf, -np.inf], np.nan).dropna()
|
|
67
|
+
edge_speeds = edge_speeds[edge_speeds > 0]
|
|
68
|
+
if edge_speeds.empty:
|
|
69
|
+
raise ValueError("Cannot calculate speed_m_per_min from edge length_meter/time_min")
|
|
70
|
+
return float(edge_speeds.median())
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def build_radius_clip_geometry(
|
|
74
|
+
reachable_graph_nodes_gdf: gpd.GeoDataFrame,
|
|
75
|
+
*,
|
|
76
|
+
weight_value: float,
|
|
77
|
+
weight_type: str,
|
|
78
|
+
speed_m_per_min: float | None,
|
|
79
|
+
):
|
|
80
|
+
buffer_size = (float(weight_value) - reachable_graph_nodes_gdf["dist"].astype(float)).clip(lower=0.0)
|
|
81
|
+
|
|
82
|
+
if weight_type == "time_min":
|
|
83
|
+
if speed_m_per_min is None:
|
|
84
|
+
raise ValueError("speed_m_per_min is required for weight_type='time_min'")
|
|
85
|
+
buffer_size = buffer_size * float(speed_m_per_min)
|
|
86
|
+
return reachable_graph_nodes_gdf.geometry.buffer(buffer_size, resolution=4).union_all()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def remove_inner_geom(geom):
|
|
90
|
+
if geom.is_empty:
|
|
91
|
+
return geom
|
|
92
|
+
if geom.geom_type == "Polygon":
|
|
93
|
+
return Polygon(geom.exterior)
|
|
94
|
+
if geom.geom_type == "MultiPolygon":
|
|
95
|
+
return MultiPolygon([Polygon(polygon.exterior) for polygon in geom.geoms])
|
|
96
|
+
if geom.geom_type == "GeometryCollection":
|
|
97
|
+
polygon_geoms = [
|
|
98
|
+
remove_inner_geom(part) for part in geom.geoms if part.geom_type in {"Polygon", "MultiPolygon"}
|
|
99
|
+
]
|
|
100
|
+
if not polygon_geoms:
|
|
101
|
+
return GeometryCollection()
|
|
102
|
+
return unary_union(polygon_geoms)
|
|
103
|
+
return geom
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def build_ways_clip_geometry(
|
|
107
|
+
urban_graph: UrbanGraph,
|
|
108
|
+
reachable_graph_nodes_gdf: gpd.GeoDataFrame,
|
|
109
|
+
*,
|
|
110
|
+
weight_value: float,
|
|
111
|
+
weight_type: str,
|
|
112
|
+
speed_m_per_min: float | None,
|
|
113
|
+
road_buffer_size: float,
|
|
114
|
+
):
|
|
115
|
+
radius_geom = build_radius_clip_geometry(
|
|
116
|
+
reachable_graph_nodes_gdf,
|
|
117
|
+
weight_value=weight_value,
|
|
118
|
+
weight_type=weight_type,
|
|
119
|
+
speed_m_per_min=speed_m_per_min,
|
|
120
|
+
)
|
|
121
|
+
reachable_node_index = pd.Index(reachable_graph_nodes_gdf.index)
|
|
122
|
+
edges = road_edges_for_ways(urban_graph)
|
|
123
|
+
edge_mask = edges["u"].isin(reachable_node_index) | edges["v"].isin(reachable_node_index)
|
|
124
|
+
if not edge_mask.any():
|
|
125
|
+
return GeometryCollection()
|
|
126
|
+
|
|
127
|
+
roads = gpd.GeoDataFrame(
|
|
128
|
+
geometry=[edges.loc[edge_mask].geometry.buffer(float(road_buffer_size), resolution=1).union_all()],
|
|
129
|
+
crs=urban_graph.edges_gdf.crs,
|
|
130
|
+
)
|
|
131
|
+
clipped_roads = roads.clip(radius_geom, keep_geom_type=True).explode(ignore_index=True)
|
|
132
|
+
if clipped_roads.empty:
|
|
133
|
+
return GeometryCollection()
|
|
134
|
+
|
|
135
|
+
geom_to_keep = clipped_roads.sjoin(reachable_graph_nodes_gdf[["geometry"]], how="inner").index.unique()
|
|
136
|
+
if len(geom_to_keep) == 0:
|
|
137
|
+
return GeometryCollection()
|
|
138
|
+
return remove_inner_geom(clipped_roads.loc[geom_to_keep].union_all())
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def build_separated_dist_polygons(
|
|
142
|
+
points: gpd.GeoDataFrame,
|
|
143
|
+
*,
|
|
144
|
+
weight_value: float,
|
|
145
|
+
weight_type: str,
|
|
146
|
+
step: float,
|
|
147
|
+
speed_m_per_min: float | None = None,
|
|
148
|
+
buffer_ratio: float = 0.7,
|
|
149
|
+
) -> gpd.GeoDataFrame:
|
|
150
|
+
"""
|
|
151
|
+
Build independent per-step buffer polygons over reachable graph nodes.
|
|
152
|
+
|
|
153
|
+
``points`` must contain point geometry and a ``dist`` column in units of
|
|
154
|
+
``weight_type``. For ``time_min`` the buffer size is converted to meters via
|
|
155
|
+
``speed_m_per_min``.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
def empty_result() -> gpd.GeoDataFrame:
|
|
159
|
+
return gpd.GeoDataFrame({"dist": []}, geometry=[], crs=points.crs)
|
|
160
|
+
|
|
161
|
+
if points.empty:
|
|
162
|
+
return empty_result()
|
|
163
|
+
if "dist" not in points.columns:
|
|
164
|
+
raise KeyError("points must contain 'dist' column")
|
|
165
|
+
if weight_value < 0:
|
|
166
|
+
raise ValueError(f"weight_value must be >= 0, got {weight_value}")
|
|
167
|
+
if step <= 0:
|
|
168
|
+
raise ValueError(f"step must be > 0, got {step}")
|
|
169
|
+
if weight_type == "time_min" and (speed_m_per_min is None or speed_m_per_min <= 0):
|
|
170
|
+
raise ValueError("speed_m_per_min must be positive for weight_type='time_min'")
|
|
171
|
+
|
|
172
|
+
points = points.loc[points["dist"].notna(), ["dist", points.geometry.name]].copy()
|
|
173
|
+
if points.empty:
|
|
174
|
+
return empty_result()
|
|
175
|
+
|
|
176
|
+
dist_for_buffer = points["dist"].astype(float).clip(lower=0.1)
|
|
177
|
+
upper_step = np.minimum((np.floor(dist_for_buffer / float(step)) + 1.0) * float(step), float(weight_value))
|
|
178
|
+
buffer_size = np.maximum(upper_step - dist_for_buffer, 0.0) * float(buffer_ratio)
|
|
179
|
+
if weight_type == "time_min":
|
|
180
|
+
buffer_size = buffer_size * float(speed_m_per_min)
|
|
181
|
+
|
|
182
|
+
points["dist"] = np.minimum(
|
|
183
|
+
np.ceil(points["dist"].astype(float).to_numpy() / float(step)) * float(step),
|
|
184
|
+
float(weight_value),
|
|
185
|
+
)
|
|
186
|
+
points.geometry = points.geometry.buffer(buffer_size)
|
|
187
|
+
points = points[~points.geometry.is_empty]
|
|
188
|
+
if points.empty:
|
|
189
|
+
return empty_result()
|
|
190
|
+
|
|
191
|
+
dissolved = points.dissolve(by="dist", as_index=False)
|
|
192
|
+
lines = dissolved.geometry.apply(polygons_to_multilinestring).union_all()
|
|
193
|
+
polygons = gpd.GeoDataFrame(geometry=list(polygonize(lines)), crs=points.crs)
|
|
194
|
+
if polygons.empty:
|
|
195
|
+
return empty_result()
|
|
196
|
+
|
|
197
|
+
polygon_points = polygons.copy()
|
|
198
|
+
polygon_points.geometry = polygons.representative_point()
|
|
199
|
+
stepped = polygon_points.sjoin(dissolved[["dist", "geometry"]], predicate="within")
|
|
200
|
+
if stepped.empty:
|
|
201
|
+
return empty_result()
|
|
202
|
+
|
|
203
|
+
stepped_dist = stepped.groupby(level=0)["dist"].mean()
|
|
204
|
+
stepped_dist = np.minimum(np.floor(stepped_dist / float(step)) * float(step), float(weight_value))
|
|
205
|
+
stepped_polygons = gpd.GeoDataFrame(
|
|
206
|
+
{"dist": stepped_dist.to_numpy()},
|
|
207
|
+
geometry=polygons.geometry.loc[stepped_dist.index].to_numpy(),
|
|
208
|
+
crs=points.crs,
|
|
209
|
+
)
|
|
210
|
+
return stepped_polygons.dissolve(by="dist", as_index=False).explode(ignore_index=True)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def build_stepped_accessibility_geometry(
|
|
214
|
+
urban_graph: UrbanGraph,
|
|
215
|
+
reachable_graph_nodes_gdf: gpd.GeoDataFrame,
|
|
216
|
+
*,
|
|
217
|
+
geometry_type: SteppedGeometryType,
|
|
218
|
+
weight_value: float,
|
|
219
|
+
weight_type: str,
|
|
220
|
+
step: float,
|
|
221
|
+
local_crs: Any,
|
|
222
|
+
zone: gpd.GeoDataFrame | gpd.GeoSeries | None,
|
|
223
|
+
buffer_factor: float,
|
|
224
|
+
road_buffer_size: float,
|
|
225
|
+
) -> gpd.GeoDataFrame:
|
|
226
|
+
if reachable_graph_nodes_gdf.empty:
|
|
227
|
+
return gpd.GeoDataFrame()
|
|
228
|
+
|
|
229
|
+
speed_m_per_min = edge_speed_m_per_min(urban_graph) if weight_type == "time_min" else None
|
|
230
|
+
|
|
231
|
+
if geometry_type == "separate":
|
|
232
|
+
result = build_separated_dist_polygons(
|
|
233
|
+
reachable_graph_nodes_gdf,
|
|
234
|
+
weight_value=float(weight_value),
|
|
235
|
+
weight_type=weight_type,
|
|
236
|
+
step=float(step),
|
|
237
|
+
speed_m_per_min=speed_m_per_min,
|
|
238
|
+
buffer_ratio=buffer_factor,
|
|
239
|
+
)
|
|
240
|
+
if zone is not None and not result.empty:
|
|
241
|
+
result = result.clip(zone.to_crs(local_crs))
|
|
242
|
+
return result
|
|
243
|
+
|
|
244
|
+
if geometry_type is None:
|
|
245
|
+
if zone is None:
|
|
246
|
+
clip_geom = convex_hull(reachable_graph_nodes_gdf.union_all())
|
|
247
|
+
else:
|
|
248
|
+
clip_geom = zone.to_crs(local_crs).union_all()
|
|
249
|
+
elif geometry_type == "radius":
|
|
250
|
+
clip_geom = build_radius_clip_geometry(
|
|
251
|
+
reachable_graph_nodes_gdf,
|
|
252
|
+
weight_value=float(weight_value),
|
|
253
|
+
weight_type=weight_type,
|
|
254
|
+
speed_m_per_min=speed_m_per_min,
|
|
255
|
+
)
|
|
256
|
+
elif geometry_type == "ways":
|
|
257
|
+
clip_geom = build_ways_clip_geometry(
|
|
258
|
+
urban_graph,
|
|
259
|
+
reachable_graph_nodes_gdf,
|
|
260
|
+
weight_value=float(weight_value),
|
|
261
|
+
weight_type=weight_type,
|
|
262
|
+
speed_m_per_min=speed_m_per_min,
|
|
263
|
+
road_buffer_size=road_buffer_size,
|
|
264
|
+
)
|
|
265
|
+
else:
|
|
266
|
+
raise ValueError(f"Unsupported geometry_type={geometry_type!r}")
|
|
267
|
+
|
|
268
|
+
if geometry_type is not None and zone is not None:
|
|
269
|
+
clip_geom = clip_geom.intersection(zone.to_crs(local_crs).union_all())
|
|
270
|
+
|
|
271
|
+
nodes_for_voronoi = reachable_graph_nodes_gdf.copy()
|
|
272
|
+
nodes_for_voronoi["dist"] = np.minimum(
|
|
273
|
+
np.ceil(nodes_for_voronoi["dist"].astype(float).to_numpy() / float(step)) * float(step),
|
|
274
|
+
float(weight_value),
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
cells = build_voronoi_cells(
|
|
278
|
+
urban_graph,
|
|
279
|
+
nodes_for_voronoi,
|
|
280
|
+
value_column="dist",
|
|
281
|
+
local_crs=local_crs,
|
|
282
|
+
clip_geom=clip_geom if geometry_type is not None else None,
|
|
283
|
+
)
|
|
284
|
+
result = cells.dissolve(by="dist", as_index=False).drop(columns=["node"])
|
|
285
|
+
return result.clip(clip_geom)
|