ObjectNat 0.1.5__py3-none-any.whl → 0.2.1__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/__init__.py +11 -17
- objectnat/_api.py +15 -0
- objectnat/_config.py +67 -0
- objectnat/_version.py +1 -0
- objectnat/methods/balanced_buildings.py +4 -1
- objectnat/methods/cluster_points_in_polygons.py +4 -1
- objectnat/methods/coverage_zones.py +6 -21
- objectnat/methods/isochrones.py +121 -47
- objectnat/methods/living_buildings_osm.py +24 -94
- objectnat/methods/provision/__init__.py +0 -0
- objectnat/methods/provision/city_provision.py +325 -0
- objectnat/methods/provision/provision.py +90 -0
- objectnat/methods/provision/provision_exceptions.py +59 -0
- objectnat/methods/visibility_analysis.py +4 -1
- {objectnat-0.1.5.dist-info → objectnat-0.2.1.dist-info}/METADATA +35 -21
- objectnat-0.2.1.dist-info/RECORD +21 -0
- {objectnat-0.1.5.dist-info → objectnat-0.2.1.dist-info}/WHEEL +1 -1
- objectnat/methods/adjacency_matrix.py +0 -39
- objectnat/methods/demands.py +0 -43
- objectnat/methods/osm_graph.py +0 -23
- objectnat/methods/provision.py +0 -135
- objectnat-0.1.5.dist-info/RECORD +0 -18
- {objectnat-0.1.5.dist-info → objectnat-0.2.1.dist-info}/LICENSE.txt +0 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
# pylint: disable=singleton-comparison
|
|
2
|
+
from typing import Tuple
|
|
3
|
+
|
|
4
|
+
import geopandas as gpd
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from shapely import LineString
|
|
8
|
+
|
|
9
|
+
from objectnat import config
|
|
10
|
+
|
|
11
|
+
from .provision_exceptions import CapacityKeyError, DemandKeyError
|
|
12
|
+
|
|
13
|
+
logger = config.logger
|
|
14
|
+
|
|
15
|
+
from pandarallel import pandarallel
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CityProvision:
|
|
19
|
+
"""
|
|
20
|
+
Represents the logic for city provision calculations using a gravity or linear model.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
services (InstanceOf[gpd.GeoDataFrame]): GeoDataFrame representing the services available in the city.
|
|
24
|
+
demanded_buildings (InstanceOf[gpd.GeoDataFrame]): GeoDataFrame representing the buildings with demands for services.
|
|
25
|
+
adjacency_matrix (InstanceOf[pd.DataFrame]): DataFrame representing the adjacency matrix between buildings.
|
|
26
|
+
threshold (int): Threshold value for the provision calculations.
|
|
27
|
+
calculation_type (str, optional): Type of calculation ("gravity" or "linear"). Defaults to "gravity".
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
CityProvision: The CityProvision object.
|
|
31
|
+
|
|
32
|
+
Raises: KeyError: If the 'demand' column is missing in the provided 'demanded_buildings' GeoDataFrame,
|
|
33
|
+
or if the 'capacity' column is missing in the provided 'services' GeoDataFrame. ValueError: If the 'capacity'
|
|
34
|
+
column in 'services' or 'demand' column 'demanded_buildings' GeoDataFrame has no valid value.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
_destination_matrix = None
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
services: gpd.GeoDataFrame,
|
|
42
|
+
demanded_buildings: gpd.GeoDataFrame,
|
|
43
|
+
adjacency_matrix: pd.DataFrame,
|
|
44
|
+
threshold: int,
|
|
45
|
+
):
|
|
46
|
+
self.services = self.ensure_services(services)
|
|
47
|
+
self.demanded_buildings = self.ensure_buildings(demanded_buildings)
|
|
48
|
+
self.adjacency_matrix = self.delete_useless_matrix_rows(adjacency_matrix.copy(), demanded_buildings, services)
|
|
49
|
+
self.threshold = threshold
|
|
50
|
+
self.check_crs(self.demanded_buildings, self.services)
|
|
51
|
+
pandarallel.initialize(progress_bar=False, verbose=0)
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def ensure_buildings(v: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
|
|
55
|
+
if "demand" not in v.columns:
|
|
56
|
+
raise DemandKeyError
|
|
57
|
+
v = v.copy()
|
|
58
|
+
v["demand_left"] = v["demand"]
|
|
59
|
+
return v
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def ensure_services(v: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
|
|
63
|
+
if "capacity" not in v.columns:
|
|
64
|
+
raise CapacityKeyError
|
|
65
|
+
v = v.copy()
|
|
66
|
+
v["capacity_left"] = v["capacity"]
|
|
67
|
+
return v
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def check_crs(demanded_buildings, services):
|
|
71
|
+
assert (
|
|
72
|
+
demanded_buildings.crs == services.crs
|
|
73
|
+
), f"\nThe CRS in the provided geodataframes are different.\nBuildings CRS:{demanded_buildings.crs}\nServices CRS:{services.crs} \n"
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def delete_useless_matrix_rows(adjacency_matrix, demanded_buildings, services):
|
|
77
|
+
adjacency_matrix.index = adjacency_matrix.index.astype(int)
|
|
78
|
+
|
|
79
|
+
builds_indexes = set(demanded_buildings.index.astype(int).tolist())
|
|
80
|
+
rows = set(adjacency_matrix.index.astype(int).tolist())
|
|
81
|
+
dif = rows ^ builds_indexes
|
|
82
|
+
adjacency_matrix.drop(index=(list(dif)), axis=0, inplace=True)
|
|
83
|
+
|
|
84
|
+
service_indexes = set(services.index.astype(int).tolist())
|
|
85
|
+
columns = set(adjacency_matrix.columns.astype(int).tolist())
|
|
86
|
+
dif = columns ^ service_indexes
|
|
87
|
+
adjacency_matrix.drop(columns=(list(dif)), axis=0, inplace=True)
|
|
88
|
+
return adjacency_matrix
|
|
89
|
+
|
|
90
|
+
def get_provisions(self) -> Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]:
|
|
91
|
+
self._destination_matrix = pd.DataFrame(
|
|
92
|
+
0,
|
|
93
|
+
index=self.adjacency_matrix.columns,
|
|
94
|
+
columns=self.adjacency_matrix.index,
|
|
95
|
+
)
|
|
96
|
+
self.adjacency_matrix = self.adjacency_matrix.transpose()
|
|
97
|
+
logger.debug(
|
|
98
|
+
"Calculating provision from {} services to {} buildings.",
|
|
99
|
+
len(self.services),
|
|
100
|
+
len(self.demanded_buildings),
|
|
101
|
+
)
|
|
102
|
+
self.adjacency_matrix = self.adjacency_matrix.where(self.adjacency_matrix <= self.threshold * 3, np.inf)
|
|
103
|
+
|
|
104
|
+
self._destination_matrix = self._provision_loop_gravity(
|
|
105
|
+
self.demanded_buildings.copy(),
|
|
106
|
+
self.services.copy(),
|
|
107
|
+
self.adjacency_matrix.copy() + 1,
|
|
108
|
+
self.threshold,
|
|
109
|
+
self._destination_matrix.copy(),
|
|
110
|
+
)
|
|
111
|
+
_additional_options(
|
|
112
|
+
self.demanded_buildings,
|
|
113
|
+
self.services,
|
|
114
|
+
self.adjacency_matrix,
|
|
115
|
+
self._destination_matrix,
|
|
116
|
+
self.threshold,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# self.demanded_buildings['provision_value'] = self.demanded_buildings['provision_value'].fillna(0)
|
|
120
|
+
# self.services = self.services.fillna(0)
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
self.demanded_buildings,
|
|
124
|
+
self.services,
|
|
125
|
+
_calc_links(
|
|
126
|
+
self._destination_matrix,
|
|
127
|
+
self.services,
|
|
128
|
+
self.demanded_buildings,
|
|
129
|
+
self.adjacency_matrix,
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def _provision_loop_gravity(
|
|
134
|
+
self,
|
|
135
|
+
houses_table: gpd.GeoDataFrame,
|
|
136
|
+
services_table: gpd.GeoDataFrame,
|
|
137
|
+
distance_matrix: pd.DataFrame,
|
|
138
|
+
selection_range,
|
|
139
|
+
destination_matrix: pd.DataFrame,
|
|
140
|
+
best_houses=0.9,
|
|
141
|
+
):
|
|
142
|
+
def apply_function_based_on_size(df, func, axis, threshold=500):
|
|
143
|
+
if len(df) > threshold:
|
|
144
|
+
return df.parallel_apply(func, axis=axis)
|
|
145
|
+
else:
|
|
146
|
+
return df.apply(func, axis=axis)
|
|
147
|
+
|
|
148
|
+
def _calculate_flows_y(loc):
|
|
149
|
+
import numpy as np
|
|
150
|
+
import pandas as pd
|
|
151
|
+
|
|
152
|
+
c = services_table.loc[loc.name]["capacity_left"]
|
|
153
|
+
p = 1 / loc / loc
|
|
154
|
+
p = p / p.sum()
|
|
155
|
+
threshold = p.quantile(best_houses)
|
|
156
|
+
p = p[p >= threshold]
|
|
157
|
+
p = p / p.sum()
|
|
158
|
+
if p.sum() == 0:
|
|
159
|
+
return loc
|
|
160
|
+
rng = np.random.default_rng(seed=0)
|
|
161
|
+
r = pd.Series(0, p.index)
|
|
162
|
+
choice = np.unique(rng.choice(p.index, int(c), p=p.values), return_counts=True)
|
|
163
|
+
choice = r.add(pd.Series(choice[1], choice[0]), fill_value=0)
|
|
164
|
+
|
|
165
|
+
return choice
|
|
166
|
+
|
|
167
|
+
def _balance_flows_to_demands(loc):
|
|
168
|
+
import numpy as np
|
|
169
|
+
import pandas as pd
|
|
170
|
+
|
|
171
|
+
d = houses_table.loc[loc.name]["demand_left"]
|
|
172
|
+
loc = loc[loc > 0]
|
|
173
|
+
if loc.sum() > 0:
|
|
174
|
+
p = loc / loc.sum()
|
|
175
|
+
rng = np.random.default_rng(seed=0)
|
|
176
|
+
r = pd.Series(0, p.index)
|
|
177
|
+
choice = np.unique(rng.choice(p.index, int(d), p=p.values), return_counts=True)
|
|
178
|
+
choice = r.add(pd.Series(choice[1], choice[0]), fill_value=0)
|
|
179
|
+
choice = pd.Series(
|
|
180
|
+
data=np.minimum(loc.sort_index().values, choice.sort_index().values),
|
|
181
|
+
index=loc.sort_index().index,
|
|
182
|
+
)
|
|
183
|
+
return choice
|
|
184
|
+
return loc
|
|
185
|
+
|
|
186
|
+
temp_destination_matrix = apply_function_based_on_size(
|
|
187
|
+
distance_matrix, lambda x: _calculate_flows_y(x[x <= selection_range]), 1
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
temp_destination_matrix = temp_destination_matrix.fillna(0)
|
|
191
|
+
|
|
192
|
+
temp_destination_matrix = apply_function_based_on_size(temp_destination_matrix, _balance_flows_to_demands, 0)
|
|
193
|
+
|
|
194
|
+
temp_destination_matrix = temp_destination_matrix.fillna(0)
|
|
195
|
+
destination_matrix = destination_matrix.add(temp_destination_matrix, fill_value=0)
|
|
196
|
+
|
|
197
|
+
axis_1 = destination_matrix.sum(axis=1)
|
|
198
|
+
axis_0 = destination_matrix.sum(axis=0)
|
|
199
|
+
|
|
200
|
+
services_table["capacity_left"] = services_table["capacity"].subtract(axis_1, fill_value=0)
|
|
201
|
+
houses_table["demand_left"] = houses_table["demand"].subtract(axis_0, fill_value=0)
|
|
202
|
+
|
|
203
|
+
distance_matrix = distance_matrix.drop(
|
|
204
|
+
index=services_table[services_table["capacity_left"] == 0].index.values,
|
|
205
|
+
columns=houses_table[houses_table["demand_left"] == 0].index.values,
|
|
206
|
+
errors="ignore",
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
distance_matrix = distance_matrix.loc[~(distance_matrix == np.inf).all(axis=1)]
|
|
210
|
+
distance_matrix = distance_matrix.loc[:, ~(distance_matrix == np.inf).all(axis=0)]
|
|
211
|
+
|
|
212
|
+
selection_range += selection_range
|
|
213
|
+
|
|
214
|
+
if best_houses > 0.1:
|
|
215
|
+
best_houses -= 0.1
|
|
216
|
+
if best_houses <= 0.1:
|
|
217
|
+
best_houses = 0
|
|
218
|
+
if len(distance_matrix.columns) > 0 and len(distance_matrix.index) > 0:
|
|
219
|
+
return self._provision_loop_gravity(
|
|
220
|
+
houses_table, services_table, distance_matrix, selection_range, destination_matrix, best_houses
|
|
221
|
+
)
|
|
222
|
+
return destination_matrix
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _calc_links(
|
|
226
|
+
destination_matrix: pd.DataFrame,
|
|
227
|
+
services: gpd.GeoDataFrame,
|
|
228
|
+
buildings: gpd.GeoDataFrame,
|
|
229
|
+
distance_matrix: pd.DataFrame,
|
|
230
|
+
):
|
|
231
|
+
def subfunc(loc):
|
|
232
|
+
try:
|
|
233
|
+
return [
|
|
234
|
+
{
|
|
235
|
+
"building_index": int(k),
|
|
236
|
+
"demand": int(v),
|
|
237
|
+
"service_index": int(loc.name),
|
|
238
|
+
}
|
|
239
|
+
for k, v in loc.to_dict().items()
|
|
240
|
+
]
|
|
241
|
+
except:
|
|
242
|
+
return np.NaN
|
|
243
|
+
|
|
244
|
+
def subfunc_geom(loc):
|
|
245
|
+
return LineString(
|
|
246
|
+
(
|
|
247
|
+
buildings_["geometry"][loc["building_index"]],
|
|
248
|
+
services_["geometry"][loc["service_index"]],
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
buildings_ = buildings.copy()
|
|
253
|
+
services_ = services.copy()
|
|
254
|
+
buildings_.geometry = buildings_.representative_point()
|
|
255
|
+
services_.geometry = services_.representative_point()
|
|
256
|
+
flat_matrix = destination_matrix.transpose().apply(lambda x: subfunc(x[x > 0]), result_type="reduce")
|
|
257
|
+
|
|
258
|
+
distribution_links = gpd.GeoDataFrame(data=[item for sublist in list(flat_matrix) for item in sublist])
|
|
259
|
+
|
|
260
|
+
distribution_links["distance"] = distribution_links.apply(
|
|
261
|
+
lambda x: distance_matrix.loc[x["service_index"]][x["building_index"]],
|
|
262
|
+
axis=1,
|
|
263
|
+
result_type="reduce",
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
sel = distribution_links["building_index"].isin(buildings_.index.values) & distribution_links["service_index"].isin(
|
|
267
|
+
services_.index.values
|
|
268
|
+
)
|
|
269
|
+
sel = distribution_links.loc[sel[sel].index.values]
|
|
270
|
+
distribution_links = distribution_links.set_geometry(sel.apply(subfunc_geom, axis=1)).set_crs(buildings_.crs)
|
|
271
|
+
distribution_links["distance"] = distribution_links["distance"].astype(float).round(2)
|
|
272
|
+
return distribution_links
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _additional_options(
|
|
276
|
+
buildings,
|
|
277
|
+
services,
|
|
278
|
+
matrix,
|
|
279
|
+
destination_matrix,
|
|
280
|
+
normative_distance,
|
|
281
|
+
):
|
|
282
|
+
buildings["avg_dist"] = 0
|
|
283
|
+
buildings["supplyed_demands_within"] = 0
|
|
284
|
+
buildings["supplyed_demands_without"] = 0
|
|
285
|
+
services["carried_capacity_within"] = 0
|
|
286
|
+
services["carried_capacity_without"] = 0
|
|
287
|
+
for i in range(len(destination_matrix)):
|
|
288
|
+
loc = destination_matrix.iloc[i]
|
|
289
|
+
distances_all = matrix.loc[loc.name]
|
|
290
|
+
distances = distances_all[distances_all <= normative_distance]
|
|
291
|
+
s = matrix.loc[loc.name] <= normative_distance
|
|
292
|
+
within = loc[s]
|
|
293
|
+
without = loc[~s]
|
|
294
|
+
within = within[within > 0]
|
|
295
|
+
without = without[without > 0]
|
|
296
|
+
buildings["avg_dist"] = (
|
|
297
|
+
buildings["avg_dist"]
|
|
298
|
+
.add(distances.multiply(within, fill_value=0), fill_value=0)
|
|
299
|
+
.add(distances_all.multiply(without, fill_value=0), fill_value=0)
|
|
300
|
+
)
|
|
301
|
+
buildings["demand_left"] = buildings["demand_left"].sub(within.add(without, fill_value=0), fill_value=0)
|
|
302
|
+
buildings["supplyed_demands_within"] = buildings["supplyed_demands_within"].add(within, fill_value=0)
|
|
303
|
+
buildings["supplyed_demands_without"] = buildings["supplyed_demands_without"].add(without, fill_value=0)
|
|
304
|
+
|
|
305
|
+
services.at[loc.name, "capacity_left"] = (
|
|
306
|
+
services.at[loc.name, "capacity_left"] - within.add(without, fill_value=0).sum()
|
|
307
|
+
)
|
|
308
|
+
services.at[loc.name, "carried_capacity_within"] = (
|
|
309
|
+
services.at[loc.name, "carried_capacity_within"] + within.sum()
|
|
310
|
+
)
|
|
311
|
+
services.at[loc.name, "carried_capacity_without"] = (
|
|
312
|
+
services.at[loc.name, "carried_capacity_without"] + without.sum()
|
|
313
|
+
)
|
|
314
|
+
buildings["avg_dist"] = (buildings["avg_dist"] / (buildings["demand"] - buildings["demand_left"])).astype(
|
|
315
|
+
np.float32
|
|
316
|
+
)
|
|
317
|
+
buildings["avg_dist"] = buildings.apply(
|
|
318
|
+
lambda x: np.nan if (x["demand"] == x["demand_left"]) else round(x["avg_dist"], 2), axis=1
|
|
319
|
+
)
|
|
320
|
+
buildings["provison_value"] = (buildings["supplyed_demands_within"] / buildings["demand"]).astype(float).round(2)
|
|
321
|
+
services["service_load"] = (services["capacity"] - services["capacity_left"]).astype(np.uint16)
|
|
322
|
+
buildings["supplyed_demands_within"] = buildings["supplyed_demands_within"].astype(np.uint16)
|
|
323
|
+
buildings["supplyed_demands_without"] = buildings["supplyed_demands_without"].astype(np.uint16)
|
|
324
|
+
services["carried_capacity_within"] = services["carried_capacity_within"].astype(np.uint16)
|
|
325
|
+
services["carried_capacity_without"] = services["carried_capacity_without"].astype(np.uint16)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from typing import Tuple
|
|
2
|
+
|
|
3
|
+
import geopandas as gpd
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from .city_provision import CityProvision
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_service_provision(
|
|
11
|
+
buildings: gpd.GeoDataFrame,
|
|
12
|
+
adjacency_matrix: pd.DataFrame,
|
|
13
|
+
services: gpd.GeoDataFrame,
|
|
14
|
+
threshold: int,
|
|
15
|
+
) -> Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]:
|
|
16
|
+
"""Calculate load from buildings with demands on the given services using the distances matrix between them.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
services (gpd.GeoDataFrame): GeoDataFrame of services
|
|
20
|
+
adjacency_matrix (pd.DataFrame): DataFrame representing the adjacency matrix
|
|
21
|
+
buildings (gpd.GeoDataFrame): GeoDataFrame of demanded buildings
|
|
22
|
+
threshold (int): Threshold value
|
|
23
|
+
Returns:
|
|
24
|
+
Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]: Tuple of GeoDataFrames representing provision
|
|
25
|
+
buildings, provision services, and provision links
|
|
26
|
+
"""
|
|
27
|
+
provision_buildings, provision_services, provision_links = CityProvision(
|
|
28
|
+
services=services,
|
|
29
|
+
demanded_buildings=buildings,
|
|
30
|
+
adjacency_matrix=adjacency_matrix,
|
|
31
|
+
threshold=threshold,
|
|
32
|
+
).get_provisions()
|
|
33
|
+
return provision_buildings, provision_services, provision_links
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def clip_provision(
|
|
37
|
+
buildings: gpd.GeoDataFrame, services: gpd.GeoDataFrame, links: gpd.GeoDataFrame, selection_zone: gpd.GeoDataFrame
|
|
38
|
+
) -> Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]:
|
|
39
|
+
|
|
40
|
+
assert (
|
|
41
|
+
selection_zone.crs == buildings.crs == services.crs == links.crs
|
|
42
|
+
), f"CRS mismatch: buildings_crs:{buildings.crs}, links_crs:{links.crs} , services_crs:{services.crs}, selection_zone_crs:{selection_zone.crs}"
|
|
43
|
+
|
|
44
|
+
s = buildings.intersects(selection_zone.unary_union)
|
|
45
|
+
buildings = buildings.loc[s[s].index]
|
|
46
|
+
links = links[links["building_index"].isin(buildings.index.tolist())]
|
|
47
|
+
services_to_keep = set(links["service_index"].tolist())
|
|
48
|
+
services.drop(list(set(services.index.tolist()) - services_to_keep), inplace=True)
|
|
49
|
+
return buildings, services, links
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def recalculate_links(
|
|
53
|
+
buildings: gpd.GeoDataFrame, services: gpd.GeoDataFrame, links: gpd.GeoDataFrame, new_max_dist: float
|
|
54
|
+
) -> tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]:
|
|
55
|
+
buildings = buildings.copy()
|
|
56
|
+
services = services.copy()
|
|
57
|
+
links = links.copy()
|
|
58
|
+
|
|
59
|
+
max_dist = links["distance"].max()
|
|
60
|
+
assert new_max_dist <= max_dist, "New distance exceeds max links distance"
|
|
61
|
+
|
|
62
|
+
links_to_recalculate = links[links["distance"] > new_max_dist]
|
|
63
|
+
links_to_keep = links[links["distance"] <= new_max_dist]
|
|
64
|
+
|
|
65
|
+
free_demand = links_to_recalculate.groupby("building_index").agg({"demand": list, "distance": list})
|
|
66
|
+
free_demand["distance"] = free_demand.apply(
|
|
67
|
+
lambda x: sum((x1 * x2) for x1, x2 in zip(x.demand, x.distance)), axis=1
|
|
68
|
+
)
|
|
69
|
+
free_demand["demand"] = free_demand["demand"].apply(sum)
|
|
70
|
+
free_demand = free_demand.reindex(buildings.index, fill_value=0)
|
|
71
|
+
new_sum_time = (buildings["supplyed_demands_within"] + buildings["supplyed_demands_without"]) * buildings[
|
|
72
|
+
"avg_dist"
|
|
73
|
+
] - free_demand["distance"]
|
|
74
|
+
|
|
75
|
+
buildings["demand_left"] = buildings["demand_left"] + free_demand["demand"]
|
|
76
|
+
buildings["supplyed_demands_without"] = buildings["supplyed_demands_without"] - free_demand["demand"]
|
|
77
|
+
buildings["avg_dist"] = new_sum_time / (
|
|
78
|
+
buildings["supplyed_demands_without"] + buildings["supplyed_demands_within"]
|
|
79
|
+
)
|
|
80
|
+
buildings["avg_dist"] = buildings.apply(
|
|
81
|
+
lambda x: np.nan if (x["demand"] == x["demand_left"]) else round(x["avg_dist"], 2), axis=1
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
free_capacity = links_to_recalculate.groupby("service_index").agg({"demand": "sum"})
|
|
85
|
+
free_capacity = free_capacity.reindex(services.index, fill_value=0)
|
|
86
|
+
services["capacity_left"] = services["capacity_left"] + free_capacity["demand"]
|
|
87
|
+
services["carried_capacity_without"] = services["carried_capacity_without"] - free_capacity["demand"]
|
|
88
|
+
services["service_load"] = services["service_load"] - free_capacity["demand"]
|
|
89
|
+
|
|
90
|
+
return buildings, services, links_to_keep
|
|
@@ -0,0 +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 "CapacityKeyError, {0} ".format(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 "CapacityValueError, {0} ".format(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 "DemandKeyError, {0} ".format(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 "DemandValueError, {0} ".format(self.message)
|
|
59
|
+
return "Column 'demand' in 'demanded_buildings' GeoDataFrame has no valid value."
|
|
@@ -4,12 +4,15 @@ from multiprocessing import cpu_count
|
|
|
4
4
|
import geopandas as gpd
|
|
5
5
|
import numpy as np
|
|
6
6
|
import pandas as pd
|
|
7
|
-
from loguru import logger
|
|
8
7
|
from pandarallel import pandarallel
|
|
9
8
|
from shapely import LineString, MultiPolygon, Point, Polygon
|
|
10
9
|
from shapely.ops import polygonize, unary_union
|
|
11
10
|
from tqdm.contrib.concurrent import process_map
|
|
12
11
|
|
|
12
|
+
from objectnat import config
|
|
13
|
+
|
|
14
|
+
logger = config.logger
|
|
15
|
+
|
|
13
16
|
|
|
14
17
|
def get_visibility_accurate(point_from: Point, obstacles: gpd.GeoDataFrame, view_distance) -> Polygon:
|
|
15
18
|
"""
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ObjectNat
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: ObjectNat is an open-source library created for geospatial analysis created by IDU team
|
|
5
5
|
License: BSD-3-Clause
|
|
6
|
-
Author:
|
|
6
|
+
Author: DDonnyy
|
|
7
7
|
Author-email: 63115678+DDonnyy@users.noreply.github.com
|
|
8
|
-
Requires-Python: >=3.10,<
|
|
8
|
+
Requires-Python: >=3.10,<3.13
|
|
9
9
|
Classifier: License :: OSI Approved :: BSD License
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.10
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
-
Requires-Dist: dongraphio (>=0.3.13,<0.4.0)
|
|
15
14
|
Requires-Dist: geopandas (>=0.14.3,<0.15.0)
|
|
15
|
+
Requires-Dist: iduedu (>=0.1.4,<0.2.0)
|
|
16
16
|
Requires-Dist: joblib (>=1.4.2,<2.0.0)
|
|
17
17
|
Requires-Dist: networkit (>=11.0,<12.0)
|
|
18
18
|
Requires-Dist: networkx (>=3.2.1,<4.0.0)
|
|
@@ -20,7 +20,7 @@ Requires-Dist: numpy (>=1.23.5,<2.0.0)
|
|
|
20
20
|
Requires-Dist: pandarallel (>=1.6.5,<2.0.0)
|
|
21
21
|
Requires-Dist: pandas (>=2.2.0,<3.0.0)
|
|
22
22
|
Requires-Dist: population-restorator (>=0.2.3,<0.3.0)
|
|
23
|
-
Requires-Dist:
|
|
23
|
+
Requires-Dist: pulp (>=2.8.0,<3.0.0)
|
|
24
24
|
Requires-Dist: scikit-learn (>=1.4.0,<2.0.0)
|
|
25
25
|
Requires-Dist: tqdm (>=4.66.2,<5.0.0)
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
@@ -28,59 +28,64 @@ Description-Content-Type: text/markdown
|
|
|
28
28
|
# ObjectNat - Meta Library
|
|
29
29
|
|
|
30
30
|
[](https://github.com/psf/black)
|
|
31
|
+
[](https://pypi.org/project/objectnat/)
|
|
31
32
|
|
|
33
|
+
- [РИДМИ (Russian)](README_ru.md)
|
|
32
34
|
<p align="center">
|
|
33
|
-
<img src="https://
|
|
35
|
+
<img src="https://github.com/user-attachments/assets/d3878cce-8eba-4f96-8458-9a798d436120" alt="logo" width="400">
|
|
34
36
|
</p>
|
|
35
37
|
|
|
36
38
|
#### **ObjectNat** is an open-source library created for geospatial analysis created by **IDU team**
|
|
37
39
|
|
|
38
40
|
## ObjectNat Components
|
|
39
41
|
|
|
40
|
-
- [
|
|
41
|
-
- [provisio](https://github.com/DDonnyy/provisio) : `provisio` provides main provisio fuctions
|
|
42
|
+
- [IduEdu](https://github.com/DDonnyy/IduEdu) : `IduEdu` provides graph functions
|
|
42
43
|
- [population-restorator](https://github.com/kanootoko/population-restorator) : `restorator` provides city resettlement
|
|
43
44
|
|
|
44
45
|
## Features and how to use
|
|
45
46
|
|
|
46
|
-
1. **[City graph from OSM](
|
|
47
|
-
transport graph from OpenStreetMap (OSM) and creating Intermodal graph.
|
|
47
|
+
1. **[City graph from OSM (IduEdu)](https://github.com/DDonnyy/IduEdu/blob/main/examples/get_any_graph.ipynb)** - Functions to assemble a road, pedestrian,
|
|
48
|
+
and public transport graph from OpenStreetMap (OSM) and creating Intermodal graph.
|
|
48
49
|
|
|
49
|
-
<img src="https://
|
|
50
|
+
<img src="https://github.com/user-attachments/assets/8dc98da9-8462-415e-8cc8-bdfca788e206" alt="IntermodalGraph" height="250">
|
|
50
51
|
|
|
51
52
|
2. **[Adjacency matrix](./examples/calculate_adjacency_matrix.ipynb)** - Calculate adjacency matrix based on the provided
|
|
52
53
|
graph and edge weight type (time or distance). The intermodal graph can be obtained using the previous example.
|
|
54
|
+
|
|
53
55
|
3. **[Isochrones,transport accessibility](./examples/isochrone_generator.ipynb)** - Function for generating isochrones to
|
|
54
56
|
analyze transportation accessibility from specified starting coordinates. Isochrones can be constructed based on
|
|
55
57
|
pedestrian, automobile, or public transport graphs, or a combination thereof.
|
|
56
58
|
|
|
57
|
-
<img src="https://
|
|
59
|
+
<img src="https://github.com/user-attachments/assets/37f308a5-db56-497d-b080-4edef3584fe5" alt="isochrones" height="250">
|
|
58
60
|
|
|
59
61
|
4. **[Population restoration](./examples/restore_population.ipynb)** - Function for resettling population into the provided
|
|
60
62
|
layer of residential buildings. This function distributes people among dwellings based on the total city population
|
|
61
63
|
and the living area of each house.
|
|
62
64
|
5. **[Service provision](./examples/calculate_provision.ipynb)** - Function for calculating the provision of residential
|
|
63
|
-
buildings and population with services.
|
|
64
|
-
|
|
65
|
+
buildings and population with services.
|
|
66
|
+
|
|
67
|
+
<img src="https://github.com/user-attachments/assets/5f2b3c55-9a02-4d70-80f4-503b77023eda" alt="ProvisionSchools" height="250">
|
|
65
68
|
|
|
66
|
-
<img src="https://i.ibb.co/CW7Xj5F/Burger-Provision5min.webp" alt="Burger-Provision5min" height="250">
|
|
67
|
-
|
|
68
69
|
6. **[Visibility analysis](./examples/visibility_analysis.ipynb)** - Function to get a quick estimate of visibility from a
|
|
69
70
|
given point(s) to buildings within a given distance. Also, there is a visibility catchment area calculator for a
|
|
70
71
|
large
|
|
71
72
|
urban area. This function is designed to work with at least 1000 points spaced 10-20 meters apart for optimal
|
|
72
73
|
results. Points can be generated using a road graph and random point distribution along edges.
|
|
73
74
|
|
|
74
|
-
<img src="https://
|
|
75
|
-
|
|
76
|
-
<img src="https://i.ibb.co/zNRzXc5/visibility-catchment-area.webp" alt="visibility-catchment-area" height="250">
|
|
75
|
+
<img src="https://github.com/user-attachments/assets/2927ac86-01e8-4b0e-9ea8-72ad81c13cf5" alt="visibility-from-point" height="250">
|
|
77
76
|
|
|
77
|
+
<img src="https://github.com/user-attachments/assets/b5b0d4b3-a02f-4ade-8772-475703cd6435" alt="visibility-catchment-area" height="250">
|
|
78
|
+
|
|
78
79
|
7. **[Point clusterization](./examples/point_clusterization.ipynb)** - Function to generate cluster polygons for given
|
|
79
80
|
points based on a specified minimum distance and minimum points per cluster. Optionally, calculate the relative ratio
|
|
80
81
|
between types of services within the clusters.
|
|
81
82
|
|
|
82
|
-
<img src="https://
|
|
83
|
-
|
|
83
|
+
<img src="https://github.com/user-attachments/assets/2a9ad722-87d2-4954-9612-5ac3765aa824" alt="service-clusterization" height="250">
|
|
84
|
+
|
|
85
|
+
8. **[Living buildings from OSM](./examples/download_buildings_from_osm.ipynb)** - This function downloads building geometries from OpenStreetMap (OSM) for a specified territory and assigns attributes to each building. Specifically, it determines whether a building is residential (`is_living` attribute) and estimates the approximate number of inhabitants (`approximate_pop` attribute).
|
|
86
|
+
|
|
87
|
+
<img src="https://github.com/user-attachments/assets/d60dcd85-1a2e-4342-aae4-561aeda18858" alt="Living buildings" height="250">
|
|
88
|
+
|
|
84
89
|
## Installation
|
|
85
90
|
|
|
86
91
|
**ObjectNat** can be installed with ``pip``:
|
|
@@ -88,7 +93,16 @@ Description-Content-Type: text/markdown
|
|
|
88
93
|
```
|
|
89
94
|
pip install ObjectNat
|
|
90
95
|
```
|
|
96
|
+
### Configuration changes
|
|
91
97
|
|
|
98
|
+
```python
|
|
99
|
+
from objectnat import config
|
|
100
|
+
|
|
101
|
+
config.set_timeout(10) # Timeout for overpass queries
|
|
102
|
+
config.change_logger_lvl('INFO') # To mute all debug msgs
|
|
103
|
+
config.set_enable_tqdm(False) # To mute all tqdm's progress bars
|
|
104
|
+
config.set_overpass_url('http://your.overpass-api.de/interpreter/URL')
|
|
105
|
+
```
|
|
92
106
|
## Contacts
|
|
93
107
|
|
|
94
108
|
- [NCCR](https://actcognitive.org/) - National
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
objectnat/__init__.py,sha256=OnDvrLPLEeYIE_9qOVYgMc-PkRzIqShtGxirguEXiRU,260
|
|
2
|
+
objectnat/_api.py,sha256=oiEO2P-tv6AMDdNoT8d0BWMmgeUJa4bhzGDTU2BWTXI,704
|
|
3
|
+
objectnat/_config.py,sha256=t4nv83Tj4fwYjdzwUh0bA8b_12DqL-GlEVfKaG_hccg,2107
|
|
4
|
+
objectnat/_version.py,sha256=Vdi6OffDRorPQeWjvXo2MPbidl7CNworxvziT78bjl0,18
|
|
5
|
+
objectnat/methods/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
objectnat/methods/balanced_buildings.py,sha256=hLT2QgmGWpROtnL8SJQIujeP6q9ou15yIdHpv66CfMs,2892
|
|
7
|
+
objectnat/methods/cluster_points_in_polygons.py,sha256=2oHK-_CauEz8dZX6r-UGEopkqdUENLGaJPpMTuVV_o8,4678
|
|
8
|
+
objectnat/methods/coverage_zones.py,sha256=yMeK1DjneMAxxKv9busEKdAsP25xiJMcPCixlJCDI4s,2835
|
|
9
|
+
objectnat/methods/isochrones.py,sha256=CeNTVpUnlITaacamB5mJQjnbphXckC1FJ0L1EThswhU,6111
|
|
10
|
+
objectnat/methods/living_buildings_osm.py,sha256=pHyeDSKhs4j05Wr3Z_QBxLfLbiZpbwdj_SXz7qQ7V2M,6041
|
|
11
|
+
objectnat/methods/provision/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
objectnat/methods/provision/city_provision.py,sha256=71Vg2tZie_C2paeFtbf8-eeQ8H7P3dECHo108bVEm0Q,13026
|
|
13
|
+
objectnat/methods/provision/provision.py,sha256=BrwfKcGoRwNcLL9fZ6mU9m6kMuGnAIgxGHfXdgHA39o,4057
|
|
14
|
+
objectnat/methods/provision/provision_exceptions.py,sha256=-TK4A-vacUuzlPJGSt2YyawRwKDLCZFlAbuIvIf1FnY,1723
|
|
15
|
+
objectnat/methods/visibility_analysis.py,sha256=__S01m4YcIZbUcr6Umzvr4NpaCsajXxKNcfJm3zquVY,20690
|
|
16
|
+
objectnat/utils/__init__.py,sha256=w8R5V_Ws_GUt4hLwpudMgjXvocG4vCxWSzVw_jTReQ4,44
|
|
17
|
+
objectnat/utils/utils.py,sha256=_vbCW-XTHwZOR3yNlzf_vgNwbYwonhGlduSznGufEgs,638
|
|
18
|
+
objectnat-0.2.1.dist-info/LICENSE.txt,sha256=yPEioMfTd7JAQgAU6J13inS1BSjwd82HFlRSoIb4My8,1498
|
|
19
|
+
objectnat-0.2.1.dist-info/METADATA,sha256=ajah19veqGq4DwU3l9UNIyBbE04MRb9_VL6TD_vRHEI,5923
|
|
20
|
+
objectnat-0.2.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
21
|
+
objectnat-0.2.1.dist-info/RECORD,,
|