ObjectNat 1.2.2__py3-none-any.whl → 1.3.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 +14 -14
- objectnat/_config.py +43 -47
- objectnat/_version.py +1 -1
- objectnat/methods/coverage_zones/__init__.py +3 -3
- objectnat/methods/coverage_zones/graph_coverage.py +11 -4
- objectnat/methods/coverage_zones/radius_voronoi_coverage.py +4 -2
- objectnat/methods/coverage_zones/stepped_coverage.py +20 -10
- objectnat/methods/isochrones/__init__.py +1 -1
- objectnat/methods/isochrones/isochrone_utils.py +167 -167
- objectnat/methods/isochrones/isochrones.py +31 -11
- objectnat/methods/noise/__init__.py +3 -3
- objectnat/methods/noise/noise_init_data.py +10 -10
- objectnat/methods/noise/noise_reduce.py +155 -155
- objectnat/methods/noise/noise_simulation.py +14 -13
- objectnat/methods/noise/noise_simulation_simplified.py +10 -9
- objectnat/methods/point_clustering/__init__.py +1 -1
- objectnat/methods/point_clustering/cluster_points_in_polygons.py +3 -3
- objectnat/methods/provision/__init__.py +1 -1
- objectnat/methods/provision/provision.py +112 -20
- objectnat/methods/provision/provision_exceptions.py +59 -59
- objectnat/methods/provision/provision_model.py +323 -348
- objectnat/methods/utils/__init__.py +1 -1
- objectnat/methods/utils/geom_utils.py +173 -173
- objectnat/methods/utils/graph_utils.py +5 -5
- objectnat/methods/utils/math_utils.py +32 -32
- objectnat/methods/visibility/__init__.py +6 -6
- objectnat/methods/visibility/visibility_analysis.py +9 -17
- objectnat-1.3.0.dist-info/METADATA +201 -0
- objectnat-1.3.0.dist-info/RECORD +33 -0
- {objectnat-1.2.2.dist-info → objectnat-1.3.0.dist-info}/WHEEL +1 -1
- objectnat-1.2.2.dist-info/METADATA +0 -116
- objectnat-1.2.2.dist-info/RECORD +0 -33
- {objectnat-1.2.2.dist-info/licenses → objectnat-1.3.0.dist-info}/LICENSE.txt +0 -0
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from typing import Tuple
|
|
2
|
-
|
|
3
1
|
import geopandas as gpd
|
|
4
2
|
import numpy as np
|
|
5
3
|
import pandas as pd
|
|
@@ -18,29 +16,53 @@ def get_service_provision(
|
|
|
18
16
|
threshold: int,
|
|
19
17
|
buildings_demand_column: str = "demand",
|
|
20
18
|
services_capacity_column: str = "capacity",
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
) -> tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]:
|
|
20
|
+
"""
|
|
21
|
+
Compute service provision between demand locations (buildings) and service facilities.
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
The function implements a **gravity-based allocation model**: service capacity is
|
|
24
|
+
distributed across nearby demand points with weights that **decay with the square
|
|
25
|
+
of distance (or generalized cost)**. Closer buildings receive proportionally
|
|
26
|
+
higher shares of the available capacity.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
30
29
|
buildings (gpd.GeoDataFrame):
|
|
31
|
-
GeoDataFrame of
|
|
30
|
+
GeoDataFrame of **demand locations** (e.g., residential buildings).
|
|
31
|
+
Must include a numeric column with demand values
|
|
32
|
+
(see ``buildings_demand_column``).
|
|
33
|
+
adjacency_matrix (pd.DataFrame):
|
|
34
|
+
A rectangular DataFrame representing **OD (origin–destination) costs**
|
|
35
|
+
between ``buildings`` (rows) and ``services`` (columns).
|
|
36
|
+
Units must match ``threshold`` (e.g., minutes or meters).
|
|
37
|
+
Missing or infinite values (``NaN`` or ``inf``) are treated as **unreachable**.
|
|
38
|
+
The row index must match ``buildings.index`` and column index must
|
|
39
|
+
match ``services.index``.
|
|
40
|
+
services (gpd.GeoDataFrame):
|
|
41
|
+
GeoDataFrame of **service facilities** (e.g., schools, clinics).
|
|
42
|
+
Must include a numeric column with service capacity
|
|
43
|
+
(see ``services_capacity_column``).
|
|
32
44
|
threshold (int):
|
|
33
|
-
|
|
45
|
+
Maximum allowed cost value for assignment.
|
|
46
|
+
Any OD entry **greater than this threshold** is considered unreachable.
|
|
47
|
+
Units are the same as in ``adjacency_matrix``.
|
|
34
48
|
buildings_demand_column (str):
|
|
35
|
-
|
|
49
|
+
Column name of building demand values. Default is ``"demand"``.
|
|
36
50
|
services_capacity_column (str):
|
|
37
|
-
|
|
38
|
-
pandarallel_init_kwargs (dict):
|
|
39
|
-
Dictionary of keyword arguments to pass to pandarallel
|
|
51
|
+
Column name of service capacity values. Default is ``"capacity"``.
|
|
40
52
|
|
|
41
53
|
Returns:
|
|
42
|
-
|
|
43
|
-
|
|
54
|
+
Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]:
|
|
55
|
+
A tuple of three GeoDataFrames:
|
|
56
|
+
|
|
57
|
+
- **buildings**: input buildings with updated provision metrics.
|
|
58
|
+
- **services**: input services with updated load and capacity metrics.
|
|
59
|
+
- **links**: building–service links within the threshold, containing
|
|
60
|
+
allocated demand shares and distances/costs based on the gravity model.
|
|
61
|
+
|
|
62
|
+
Notes:
|
|
63
|
+
- The model is **gravity-based**, with cost weights decaying by the **square of distance**.
|
|
64
|
+
- Unreachable OD pairs (``NaN`` or ``inf``) are ignored.
|
|
65
|
+
- The function does not perform routing; it expects a precomputed OD matrix.
|
|
44
66
|
"""
|
|
45
67
|
buildings = buildings.copy()
|
|
46
68
|
services = services.copy()
|
|
@@ -53,15 +75,42 @@ def get_service_provision(
|
|
|
53
75
|
demanded_buildings=buildings,
|
|
54
76
|
adjacency_matrix=adjacency_matrix,
|
|
55
77
|
threshold=threshold,
|
|
56
|
-
pandarallel_init_kwargs=pandarallel_init_kwargs,
|
|
57
78
|
).run()
|
|
58
79
|
return provision_buildings, provision_services, provision_links
|
|
59
80
|
|
|
60
81
|
|
|
61
82
|
def clip_provision(
|
|
62
83
|
buildings: gpd.GeoDataFrame, services: gpd.GeoDataFrame, links: gpd.GeoDataFrame, selection_zone: gpd.GeoDataFrame
|
|
63
|
-
) ->
|
|
84
|
+
) -> tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]:
|
|
85
|
+
"""
|
|
86
|
+
Clip service provision results to a specific geographic boundary.
|
|
87
|
+
|
|
88
|
+
Keeps only:
|
|
89
|
+
* buildings that intersect the ``selection_zone``;
|
|
90
|
+
* links that connect to the kept buildings;
|
|
91
|
+
* services referenced by those links.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
buildings:
|
|
95
|
+
GeoDataFrame of buildings **after** :func:`get_service_provision`.
|
|
96
|
+
services:
|
|
97
|
+
GeoDataFrame of services **after** :func:`get_service_provision`.
|
|
98
|
+
links:
|
|
99
|
+
GeoDataFrame of building–service links from
|
|
100
|
+
:func:`get_service_provision`. Must include indices or columns
|
|
101
|
+
to match buildings and services.
|
|
102
|
+
selection_zone:
|
|
103
|
+
GeoDataFrame (polygon or multipolygon) defining the clipping area.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]:
|
|
107
|
+
The filtered subsets of buildings, services, and links
|
|
108
|
+
that fall inside the specified zone.
|
|
64
109
|
|
|
110
|
+
Notes:
|
|
111
|
+
- The function performs **spatial filtering only**.
|
|
112
|
+
It does **not** recompute or redistribute demand/supply.
|
|
113
|
+
"""
|
|
65
114
|
assert selection_zone.crs == buildings.crs == services.crs == links.crs, (
|
|
66
115
|
f"CRS mismatch: buildings_crs:{buildings.crs}, "
|
|
67
116
|
f"links_crs:{links.crs} , "
|
|
@@ -83,6 +132,49 @@ def clip_provision(
|
|
|
83
132
|
def recalculate_links(
|
|
84
133
|
buildings: gpd.GeoDataFrame, services: gpd.GeoDataFrame, links: gpd.GeoDataFrame, new_max_dist: float
|
|
85
134
|
) -> tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]:
|
|
135
|
+
"""
|
|
136
|
+
Recalculate provision aggregates after tightening the accessibility threshold.
|
|
137
|
+
|
|
138
|
+
Removes all links whose cost (distance or time) exceeds ``new_max_dist``, then
|
|
139
|
+
updates demand and capacity aggregates accordingly. This is done **without
|
|
140
|
+
redistributing** removed demand to alternative services.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
buildings:
|
|
144
|
+
GeoDataFrame of buildings after :func:`get_service_provision`.
|
|
145
|
+
Expected to include provision-related fields such as demand, demand_left,
|
|
146
|
+
supplied demand, and average distance/cost.
|
|
147
|
+
|
|
148
|
+
services:
|
|
149
|
+
GeoDataFrame of services after :func:`get_service_provision`, with
|
|
150
|
+
fields describing remaining capacity and service load.
|
|
151
|
+
|
|
152
|
+
links:
|
|
153
|
+
GeoDataFrame of building–service links containing at least:
|
|
154
|
+
|
|
155
|
+
- ``building_index``
|
|
156
|
+
- ``service_index``
|
|
157
|
+
- ``distance`` (or time cost, in the same units as ``new_max_dist``)
|
|
158
|
+
- ``demand`` (assigned portion)
|
|
159
|
+
|
|
160
|
+
new_max_dist:
|
|
161
|
+
New maximum allowed cost value (same units as OD/threshold).
|
|
162
|
+
Links with cost **greater than** this value are removed.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]:
|
|
166
|
+
- **buildings**: updated aggregate demand metrics and recalculated
|
|
167
|
+
average cost.
|
|
168
|
+
- **services**: updated load and capacity fields after freeing excess capacity.
|
|
169
|
+
- **links**: subset of links that remain within the new threshold.
|
|
170
|
+
|
|
171
|
+
Notes:
|
|
172
|
+
- If no links exceed ``new_max_dist``, the function logs a warning
|
|
173
|
+
and returns the original inputs unchanged.
|
|
174
|
+
- Average cost values are recomputed based on remaining links.
|
|
175
|
+
If a building has no remaining assigned demand, ``avg_dist`` becomes ``NaN``.
|
|
176
|
+
- Removed demand is **not reallocated** to other services.
|
|
177
|
+
"""
|
|
86
178
|
buildings = buildings.copy()
|
|
87
179
|
services = services.copy()
|
|
88
180
|
links = links.copy()
|
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
class CapacityKeyError(KeyError):
|
|
2
|
-
def __init__(self, *args):
|
|
3
|
-
if args:
|
|
4
|
-
self.message = args[0]
|
|
5
|
-
else:
|
|
6
|
-
self.message = None
|
|
7
|
-
|
|
8
|
-
def __str__(self):
|
|
9
|
-
if self.message:
|
|
10
|
-
return f"CapacityKeyError, {self.message} "
|
|
11
|
-
|
|
12
|
-
return (
|
|
13
|
-
"Column 'capacity' was not found in provided 'services' GeoDataFrame. This attribute "
|
|
14
|
-
"corresponds to the total capacity for each service."
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class CapacityValueError(ValueError):
|
|
19
|
-
def __init__(self, *args):
|
|
20
|
-
if args:
|
|
21
|
-
self.message = args[0]
|
|
22
|
-
else:
|
|
23
|
-
self.message = None
|
|
24
|
-
|
|
25
|
-
def __str__(self):
|
|
26
|
-
if self.message:
|
|
27
|
-
return f"CapacityValueError, {self.message} "
|
|
28
|
-
|
|
29
|
-
return "Column 'capacity' in 'services' GeoDataFrame has no valid value."
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class DemandKeyError(KeyError):
|
|
33
|
-
def __init__(self, *args):
|
|
34
|
-
if args:
|
|
35
|
-
self.message = args[0]
|
|
36
|
-
else:
|
|
37
|
-
self.message = None
|
|
38
|
-
|
|
39
|
-
def __str__(self):
|
|
40
|
-
if self.message:
|
|
41
|
-
return f"DemandKeyError, {self.message} "
|
|
42
|
-
|
|
43
|
-
return (
|
|
44
|
-
"The column 'demand' was not found in the provided 'demanded_buildings' GeoDataFrame. "
|
|
45
|
-
"This attribute corresponds to the number of demands for the selected service in each building."
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
class DemandValueError(ValueError):
|
|
50
|
-
def __init__(self, *args):
|
|
51
|
-
if args:
|
|
52
|
-
self.message = args[0]
|
|
53
|
-
else:
|
|
54
|
-
self.message = None
|
|
55
|
-
|
|
56
|
-
def __str__(self):
|
|
57
|
-
if self.message:
|
|
58
|
-
return f"DemandValueError, {self.message} "
|
|
59
|
-
return "Column 'demand' in 'demanded_buildings' GeoDataFrame has no valid value."
|
|
1
|
+
class CapacityKeyError(KeyError):
|
|
2
|
+
def __init__(self, *args):
|
|
3
|
+
if args:
|
|
4
|
+
self.message = args[0]
|
|
5
|
+
else:
|
|
6
|
+
self.message = None
|
|
7
|
+
|
|
8
|
+
def __str__(self):
|
|
9
|
+
if self.message:
|
|
10
|
+
return f"CapacityKeyError, {self.message} "
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
"Column 'capacity' was not found in provided 'services' GeoDataFrame. This attribute "
|
|
14
|
+
"corresponds to the total capacity for each service."
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CapacityValueError(ValueError):
|
|
19
|
+
def __init__(self, *args):
|
|
20
|
+
if args:
|
|
21
|
+
self.message = args[0]
|
|
22
|
+
else:
|
|
23
|
+
self.message = None
|
|
24
|
+
|
|
25
|
+
def __str__(self):
|
|
26
|
+
if self.message:
|
|
27
|
+
return f"CapacityValueError, {self.message} "
|
|
28
|
+
|
|
29
|
+
return "Column 'capacity' in 'services' GeoDataFrame has no valid value."
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DemandKeyError(KeyError):
|
|
33
|
+
def __init__(self, *args):
|
|
34
|
+
if args:
|
|
35
|
+
self.message = args[0]
|
|
36
|
+
else:
|
|
37
|
+
self.message = None
|
|
38
|
+
|
|
39
|
+
def __str__(self):
|
|
40
|
+
if self.message:
|
|
41
|
+
return f"DemandKeyError, {self.message} "
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
"The column 'demand' was not found in the provided 'demanded_buildings' GeoDataFrame. "
|
|
45
|
+
"This attribute corresponds to the number of demands for the selected service in each building."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class DemandValueError(ValueError):
|
|
50
|
+
def __init__(self, *args):
|
|
51
|
+
if args:
|
|
52
|
+
self.message = args[0]
|
|
53
|
+
else:
|
|
54
|
+
self.message = None
|
|
55
|
+
|
|
56
|
+
def __str__(self):
|
|
57
|
+
if self.message:
|
|
58
|
+
return f"DemandValueError, {self.message} "
|
|
59
|
+
return "Column 'demand' in 'demanded_buildings' GeoDataFrame has no valid value."
|