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.

Files changed (33) hide show
  1. objectnat/_api.py +14 -14
  2. objectnat/_config.py +43 -47
  3. objectnat/_version.py +1 -1
  4. objectnat/methods/coverage_zones/__init__.py +3 -3
  5. objectnat/methods/coverage_zones/graph_coverage.py +11 -4
  6. objectnat/methods/coverage_zones/radius_voronoi_coverage.py +4 -2
  7. objectnat/methods/coverage_zones/stepped_coverage.py +20 -10
  8. objectnat/methods/isochrones/__init__.py +1 -1
  9. objectnat/methods/isochrones/isochrone_utils.py +167 -167
  10. objectnat/methods/isochrones/isochrones.py +31 -11
  11. objectnat/methods/noise/__init__.py +3 -3
  12. objectnat/methods/noise/noise_init_data.py +10 -10
  13. objectnat/methods/noise/noise_reduce.py +155 -155
  14. objectnat/methods/noise/noise_simulation.py +14 -13
  15. objectnat/methods/noise/noise_simulation_simplified.py +10 -9
  16. objectnat/methods/point_clustering/__init__.py +1 -1
  17. objectnat/methods/point_clustering/cluster_points_in_polygons.py +3 -3
  18. objectnat/methods/provision/__init__.py +1 -1
  19. objectnat/methods/provision/provision.py +112 -20
  20. objectnat/methods/provision/provision_exceptions.py +59 -59
  21. objectnat/methods/provision/provision_model.py +323 -348
  22. objectnat/methods/utils/__init__.py +1 -1
  23. objectnat/methods/utils/geom_utils.py +173 -173
  24. objectnat/methods/utils/graph_utils.py +5 -5
  25. objectnat/methods/utils/math_utils.py +32 -32
  26. objectnat/methods/visibility/__init__.py +6 -6
  27. objectnat/methods/visibility/visibility_analysis.py +9 -17
  28. objectnat-1.3.0.dist-info/METADATA +201 -0
  29. objectnat-1.3.0.dist-info/RECORD +33 -0
  30. {objectnat-1.2.2.dist-info → objectnat-1.3.0.dist-info}/WHEEL +1 -1
  31. objectnat-1.2.2.dist-info/METADATA +0 -116
  32. objectnat-1.2.2.dist-info/RECORD +0 -33
  33. {objectnat-1.2.2.dist-info/licenses → objectnat-1.3.0.dist-info}/LICENSE.txt +0 -0
@@ -1,348 +1,323 @@
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 pandarallel import pandarallel
8
- from shapely import LineString
9
-
10
- from objectnat import config
11
-
12
- from .provision_exceptions import CapacityKeyError, DemandKeyError
13
-
14
- logger = config.logger
15
-
16
-
17
- class Provision:
18
- """
19
- Represents the logic for city provision calculations using a gravity or linear model.
20
-
21
- Args:
22
- services (gpd.GeoDataFrame): GeoDataFrame representing the services available in the city.
23
- demanded_buildings (gpd.GeoDataFrame): GeoDataFrame representing the buildings with demands for services.
24
- adjacency_matrix (pd.DataFrame): DataFrame representing the adjacency matrix between buildings.
25
- threshold (int): Threshold value for the provision calculations.
26
-
27
- Returns:
28
- Provision: The CityProvision object.
29
-
30
- Raises: KeyError: If the 'demand' column is missing in the provided 'demanded_buildings' GeoDataFrame,
31
- or if the 'capacity' column is missing in the provided 'services' GeoDataFrame. ValueError: If the 'capacity'
32
- column in 'services' or 'demand' column 'demanded_buildings' GeoDataFrame has no valid value.
33
- """
34
-
35
- destination_matrix = None
36
-
37
- def __init__(
38
- self,
39
- services: gpd.GeoDataFrame,
40
- demanded_buildings: gpd.GeoDataFrame,
41
- adjacency_matrix: pd.DataFrame,
42
- threshold: int,
43
- pandarallel_init_kwargs: dict = None,
44
- ):
45
- self.services = self.ensure_services(services.copy())
46
- self.demanded_buildings = self.ensure_buildings(demanded_buildings.copy())
47
- self.adjacency_matrix = self.delete_useless_matrix_rows_columns(
48
- adjacency_matrix.copy(), demanded_buildings, services
49
- ).copy()
50
- self.threshold = threshold
51
- self.services.to_crs(self.demanded_buildings.crs, inplace=True)
52
-
53
- if pandarallel_init_kwargs is None:
54
- pandarallel_init_kwargs = {}
55
-
56
- pandarallel_init_kwargs["use_memory_fs"] = pandarallel_init_kwargs.get(
57
- "use_memory_fs", config.pandarallel_use_file_system
58
- )
59
- pandarallel_init_kwargs["progress_bar"] = pandarallel_init_kwargs.get("progress_bar", False)
60
- pandarallel_init_kwargs["verbose"] = pandarallel_init_kwargs.get("verbose", 0)
61
- pandarallel.initialize(**pandarallel_init_kwargs)
62
-
63
- @staticmethod
64
- def ensure_buildings(v: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
65
- if "demand" not in v.columns:
66
- raise DemandKeyError
67
- v["demand_left"] = v["demand"]
68
- return v
69
-
70
- @staticmethod
71
- def ensure_services(v: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
72
- if "capacity" not in v.columns:
73
- raise CapacityKeyError
74
- v["capacity_left"] = v["capacity"]
75
- return v
76
-
77
- @staticmethod
78
- def delete_useless_matrix_rows_columns(adjacency_matrix, demanded_buildings, services):
79
- adjacency_matrix.index = adjacency_matrix.index.astype(int)
80
-
81
- builds_indexes = set(demanded_buildings.index.astype(int).tolist())
82
- rows = set(adjacency_matrix.index.astype(int).tolist())
83
- dif = rows ^ builds_indexes
84
- adjacency_matrix.drop(index=(list(dif)), axis=0, inplace=True)
85
-
86
- service_indexes = set(services.index.astype(int).tolist())
87
- columns = set(adjacency_matrix.columns.astype(int).tolist())
88
- dif = columns ^ service_indexes
89
- adjacency_matrix.drop(columns=(list(dif)), axis=0, inplace=True)
90
- return adjacency_matrix.transpose()
91
-
92
- def run(self) -> Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]:
93
-
94
- def apply_function_based_on_size(df, func, axis, threshold=100):
95
- if len(df) > threshold:
96
- return df.parallel_apply(func, axis=axis)
97
- return df.apply(func, axis=axis)
98
-
99
- def calculate_flows_y(loc):
100
- import numpy as np # pylint: disable=redefined-outer-name,reimported,import-outside-toplevel
101
- import pandas as pd # pylint: disable=redefined-outer-name,reimported,import-outside-toplevel
102
-
103
- c = services_table.loc[loc.name]["capacity_left"]
104
- logger.debug(f"Capacity left: {c}")
105
- p = 1 / loc / loc
106
- p = p / p.sum()
107
- threshold = p.quantile(best_houses)
108
- p = p[p >= threshold]
109
- p = p / p.sum()
110
- if p.sum() == 0:
111
- return loc
112
- rng = np.random.default_rng(seed=0)
113
- r = pd.Series(0, p.index)
114
- choice = np.unique(rng.choice(p.index, int(c), p=p.values), return_counts=True)
115
- choice = r.add(pd.Series(choice[1], choice[0]), fill_value=0)
116
-
117
- return choice
118
-
119
- def balance_flows_to_demands(loc):
120
- import numpy as np # pylint: disable=redefined-outer-name,reimported,import-outside-toplevel
121
- import pandas as pd # pylint: disable=redefined-outer-name,reimported,import-outside-toplevel
122
-
123
- d = houses_table.loc[loc.name]["demand_left"]
124
- loc = loc[loc > 0]
125
- if loc.sum() > 0:
126
- p = loc / loc.sum()
127
- rng = np.random.default_rng(seed=0)
128
- r = pd.Series(0, p.index)
129
- choice = np.unique(rng.choice(p.index, int(d), p=p.values), return_counts=True)
130
- choice = r.add(pd.Series(choice[1], choice[0]), fill_value=0)
131
- choice = pd.Series(
132
- data=np.minimum(loc.sort_index().values, choice.sort_index().values),
133
- index=loc.sort_index().index,
134
- )
135
- return choice
136
- return loc
137
-
138
- logger.debug(
139
- f"Calculating provision from {len(self.services)} services to {len(self.demanded_buildings)} buildings."
140
- )
141
-
142
- distance_matrix = self.adjacency_matrix
143
- destination_matrix = pd.DataFrame(
144
- 0,
145
- index=distance_matrix.index,
146
- columns=distance_matrix.columns,
147
- dtype=int,
148
- )
149
- distance_matrix = distance_matrix.where(distance_matrix <= self.threshold * 3, np.inf)
150
-
151
- houses_table = self.demanded_buildings[["demand", "demand_left"]].copy()
152
- services_table = self.services[["capacity", "capacity_left"]].copy()
153
- distance_matrix = distance_matrix.drop(
154
- index=services_table[services_table["capacity_left"] == 0].index.values,
155
- columns=houses_table[houses_table["demand_left"] == 0].index.values,
156
- errors="ignore",
157
- )
158
- distance_matrix = distance_matrix.loc[~(distance_matrix == np.inf).all(axis=1)]
159
- distance_matrix = distance_matrix.loc[:, ~(distance_matrix == np.inf).all(axis=0)]
160
-
161
- distance_matrix = distance_matrix + 1
162
- selection_range = (self.threshold + 1) / 2
163
- best_houses = 0.9
164
- while len(distance_matrix.columns) > 0 and len(distance_matrix.index) > 0:
165
- objects_n = sum(distance_matrix.shape)
166
- logger.debug(
167
- f"Matrix shape: {distance_matrix.shape},"
168
- f" Total objects: {objects_n},"
169
- f" Selection range: {selection_range},"
170
- f" Best houses: {best_houses}"
171
- )
172
-
173
- temp_destination_matrix = apply_function_based_on_size(
174
- distance_matrix, lambda x: calculate_flows_y(x[x <= selection_range]), 1
175
- )
176
-
177
- temp_destination_matrix = temp_destination_matrix.fillna(0)
178
- temp_destination_matrix = apply_function_based_on_size(temp_destination_matrix, balance_flows_to_demands, 0)
179
- temp_destination_matrix = temp_destination_matrix.fillna(0)
180
- temp_destination_matrix_aligned = temp_destination_matrix.reindex(
181
- index=destination_matrix.index, columns=destination_matrix.columns, fill_value=0
182
- )
183
- del temp_destination_matrix
184
- destination_matrix_np = destination_matrix.to_numpy()
185
- temp_destination_matrix_np = temp_destination_matrix_aligned.to_numpy()
186
- del temp_destination_matrix_aligned
187
- destination_matrix = pd.DataFrame(
188
- destination_matrix_np + temp_destination_matrix_np,
189
- index=destination_matrix.index,
190
- columns=destination_matrix.columns,
191
- )
192
- del destination_matrix_np, temp_destination_matrix_np
193
- axis_1 = destination_matrix.sum(axis=1).astype(int)
194
- axis_0 = destination_matrix.sum(axis=0).astype(int)
195
-
196
- services_table["capacity_left"] = services_table["capacity"].subtract(axis_1, fill_value=0)
197
- houses_table["demand_left"] = houses_table["demand"].subtract(axis_0, fill_value=0)
198
- del axis_1, axis_0
199
- distance_matrix = distance_matrix.drop(
200
- index=services_table[services_table["capacity_left"] == 0].index.values,
201
- columns=houses_table[houses_table["demand_left"] == 0].index.values,
202
- errors="ignore",
203
- )
204
- distance_matrix = distance_matrix.loc[~(distance_matrix == np.inf).all(axis=1)]
205
- distance_matrix = distance_matrix.loc[:, ~(distance_matrix == np.inf).all(axis=0)]
206
-
207
- selection_range *= 1.5
208
- if best_houses <= 0.1:
209
- best_houses = 0
210
- else:
211
- objects_n_new = sum(distance_matrix.shape)
212
- best_houses = objects_n_new / (objects_n / best_houses)
213
-
214
- logger.debug("Done!")
215
- del distance_matrix, houses_table, services_table
216
- self.destination_matrix = destination_matrix
217
-
218
- _additional_options(
219
- self.demanded_buildings,
220
- self.services,
221
- self.adjacency_matrix,
222
- self.destination_matrix,
223
- self.threshold,
224
- )
225
-
226
- return (
227
- self.demanded_buildings,
228
- self.services,
229
- _calc_links(
230
- self.destination_matrix,
231
- self.services,
232
- self.demanded_buildings,
233
- self.adjacency_matrix,
234
- ),
235
- )
236
-
237
-
238
- def _calc_links(
239
- destination_matrix: pd.DataFrame,
240
- services: gpd.GeoDataFrame,
241
- buildings: gpd.GeoDataFrame,
242
- distance_matrix: pd.DataFrame,
243
- ):
244
- buildings_ = buildings.copy()
245
- services_ = services.copy()
246
- buildings_.geometry = buildings_.representative_point()
247
- services_.geometry = services_.representative_point()
248
-
249
- def subfunc(loc):
250
- try:
251
- return [
252
- {
253
- "building_index": int(k),
254
- "demand": int(v),
255
- "service_index": int(loc.name),
256
- }
257
- for k, v in loc.to_dict().items()
258
- ]
259
- except:
260
- return np.NaN
261
-
262
- def subfunc_geom(loc):
263
- return LineString(
264
- (
265
- buildings_.geometry[loc["building_index"]],
266
- services_.geometry[loc["service_index"]],
267
- )
268
- )
269
-
270
- flat_matrix = destination_matrix.transpose().apply(lambda x: subfunc(x[x > 0]), result_type="reduce")
271
-
272
- distribution_links = gpd.GeoDataFrame(data=[item for sublist in list(flat_matrix) for item in sublist])
273
- if distribution_links.empty:
274
- logger.warning(
275
- "Unable to create distribution links - no demand could be matched with service locations. "
276
- "This is likely because either: "
277
- "1) The demand column in buildings contains zero values, or "
278
- "2) The capacity column in services contains zero values, or "
279
- "3) There are no service locations within the maximum allowed distance"
280
- )
281
- return distribution_links
282
- distribution_links["distance"] = distribution_links.apply(
283
- lambda x: distance_matrix.loc[x["service_index"]][x["building_index"]],
284
- axis=1,
285
- result_type="reduce",
286
- )
287
-
288
- sel = distribution_links["building_index"].isin(buildings_.index.values) & distribution_links["service_index"].isin(
289
- services_.index.values
290
- )
291
- sel = distribution_links.loc[sel[sel].index.values]
292
- distribution_links = distribution_links.set_geometry(sel.apply(subfunc_geom, axis=1)).set_crs(buildings_.crs)
293
- distribution_links["distance"] = distribution_links["distance"].astype(float).round(2)
294
- return distribution_links
295
-
296
-
297
- def _additional_options(
298
- buildings,
299
- services,
300
- matrix,
301
- destination_matrix,
302
- normative_distance,
303
- ):
304
- buildings["avg_dist"] = 0
305
- buildings["supplied_demands_within"] = 0
306
- buildings["supplied_demands_without"] = 0
307
- services["carried_capacity_within"] = 0
308
- services["carried_capacity_without"] = 0
309
- for _, loc in destination_matrix.iterrows():
310
- distances_all = matrix.loc[loc.name]
311
- distances = distances_all[distances_all <= normative_distance]
312
- s = matrix.loc[loc.name] <= normative_distance
313
- within = loc[s]
314
- without = loc[~s]
315
- within = within[within > 0]
316
- without = without[without > 0]
317
- buildings["avg_dist"] = (
318
- buildings["avg_dist"]
319
- .add(distances.multiply(within, fill_value=0), fill_value=0)
320
- .add(distances_all.multiply(without, fill_value=0), fill_value=0)
321
- )
322
- buildings["demand_left"] = buildings["demand_left"].sub(within.add(without, fill_value=0), fill_value=0)
323
- buildings["supplied_demands_within"] = buildings["supplied_demands_within"].add(within, fill_value=0)
324
- buildings["supplied_demands_without"] = buildings["supplied_demands_without"].add(without, fill_value=0)
325
-
326
- services.at[loc.name, "capacity_left"] = (
327
- services.at[loc.name, "capacity_left"] - within.add(without, fill_value=0).sum()
328
- )
329
- services.at[loc.name, "carried_capacity_within"] = (
330
- services.at[loc.name, "carried_capacity_within"] + within.sum()
331
- )
332
- services.at[loc.name, "carried_capacity_without"] = (
333
- services.at[loc.name, "carried_capacity_without"] + without.sum()
334
- )
335
- buildings["min_dist"] = matrix.min(axis=0).replace(np.inf, None)
336
- buildings["avg_dist"] = (buildings["avg_dist"] / (buildings["demand"] - buildings["demand_left"])).astype(
337
- np.float32
338
- )
339
- buildings["avg_dist"] = buildings.apply(
340
- lambda x: np.nan if (x["demand"] == x["demand_left"]) else round(x["avg_dist"], 2), axis=1
341
- )
342
- buildings["provision_value"] = (buildings["supplied_demands_within"] / buildings["demand"]).astype(float).round(2)
343
- services["service_load"] = (services["capacity"] - services["capacity_left"]).astype(np.uint16)
344
- buildings["supplied_demands_within"] = buildings["supplied_demands_within"].astype(np.uint16)
345
- buildings["supplied_demands_without"] = buildings["supplied_demands_without"].astype(np.uint16)
346
- services["carried_capacity_within"] = services["carried_capacity_within"].astype(np.uint16)
347
- services["carried_capacity_without"] = services["carried_capacity_without"].astype(np.uint16)
348
- logger.debug("Done adding additional options")
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
+
16
+ class Provision:
17
+ """
18
+ Represents the logic for city provision calculations using a gravity or linear model.
19
+
20
+ Args:
21
+ services (gpd.GeoDataFrame): GeoDataFrame representing the services available in the city.
22
+ demanded_buildings (gpd.GeoDataFrame): GeoDataFrame representing the buildings with demands for services.
23
+ adjacency_matrix (pd.DataFrame): DataFrame representing the adjacency matrix between buildings.
24
+ threshold (int): Threshold value for the provision calculations.
25
+
26
+ Returns:
27
+ Provision: The CityProvision object.
28
+
29
+ Raises: KeyError: If the 'demand' column is missing in the provided 'demanded_buildings' GeoDataFrame,
30
+ or if the 'capacity' column is missing in the provided 'services' GeoDataFrame. ValueError: If the 'capacity'
31
+ column in 'services' or 'demand' column 'demanded_buildings' GeoDataFrame has no valid value.
32
+ """
33
+
34
+ destination_matrix = None
35
+
36
+ def __init__(
37
+ self,
38
+ services: gpd.GeoDataFrame,
39
+ demanded_buildings: gpd.GeoDataFrame,
40
+ adjacency_matrix: pd.DataFrame,
41
+ threshold: int,
42
+ ):
43
+ self.services = self.ensure_services(services.copy())
44
+ self.demanded_buildings = self.ensure_buildings(demanded_buildings.copy())
45
+ self.adjacency_matrix = self.delete_useless_matrix_rows_columns(
46
+ adjacency_matrix.copy(), demanded_buildings, services
47
+ ).copy()
48
+ self.threshold = threshold
49
+ self.services.to_crs(self.demanded_buildings.crs, inplace=True)
50
+
51
+ @staticmethod
52
+ def ensure_buildings(v: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
53
+ if "demand" not in v.columns:
54
+ raise DemandKeyError
55
+ v["demand_left"] = v["demand"]
56
+ return v
57
+
58
+ @staticmethod
59
+ def ensure_services(v: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
60
+ if "capacity" not in v.columns:
61
+ raise CapacityKeyError
62
+ v["capacity_left"] = v["capacity"]
63
+ return v
64
+
65
+ @staticmethod
66
+ def delete_useless_matrix_rows_columns(adjacency_matrix, demanded_buildings, services):
67
+ adjacency_matrix.index = adjacency_matrix.index.astype(int)
68
+
69
+ builds_indexes = set(demanded_buildings.index.astype(int).tolist())
70
+ rows = set(adjacency_matrix.index.astype(int).tolist())
71
+ dif = rows ^ builds_indexes
72
+ adjacency_matrix.drop(index=(list(dif)), axis=0, inplace=True)
73
+
74
+ service_indexes = set(services.index.astype(int).tolist())
75
+ columns = set(adjacency_matrix.columns.astype(int).tolist())
76
+ dif = columns ^ service_indexes
77
+ adjacency_matrix.drop(columns=(list(dif)), axis=0, inplace=True)
78
+ return adjacency_matrix.transpose()
79
+
80
+ def run(self) -> Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]:
81
+
82
+ def calculate_flows_y(loc):
83
+ c = services_table.loc[loc.name]["capacity_left"]
84
+ p = 1 / loc / loc
85
+ p = p / p.sum()
86
+ threshold = p.quantile(best_houses)
87
+ p = p[p >= threshold]
88
+ p = p / p.sum()
89
+ if p.sum() == 0:
90
+ return loc
91
+ rng = np.random.default_rng(seed=0)
92
+ r = pd.Series(0, p.index)
93
+ choice = np.unique(rng.choice(p.index, int(c), p=p.values), return_counts=True)
94
+ choice = r.add(pd.Series(choice[1], choice[0]), fill_value=0)
95
+
96
+ return choice
97
+
98
+ def balance_flows_to_demands(loc):
99
+ d = houses_table.loc[loc.name]["demand_left"]
100
+ loc = loc[loc > 0]
101
+ if loc.sum() > 0:
102
+ p = loc / loc.sum()
103
+ rng = np.random.default_rng(seed=0)
104
+ r = pd.Series(0, p.index)
105
+ choice = np.unique(rng.choice(p.index, int(d), p=p.values), return_counts=True)
106
+ choice = r.add(pd.Series(choice[1], choice[0]), fill_value=0)
107
+ choice = pd.Series(
108
+ data=np.minimum(loc.sort_index().values, choice.sort_index().values),
109
+ index=loc.sort_index().index,
110
+ )
111
+ return choice
112
+ return loc
113
+
114
+ logger.debug(
115
+ f"Calculating provision from {len(self.services)} services to {len(self.demanded_buildings)} buildings."
116
+ )
117
+
118
+ distance_matrix = self.adjacency_matrix
119
+ destination_matrix = pd.DataFrame(
120
+ 0,
121
+ index=distance_matrix.index,
122
+ columns=distance_matrix.columns,
123
+ dtype=int,
124
+ )
125
+ distance_matrix = distance_matrix.where(distance_matrix <= self.threshold * 3, np.inf)
126
+
127
+ houses_table = self.demanded_buildings[["demand", "demand_left"]].copy()
128
+ services_table = self.services[["capacity", "capacity_left"]].copy()
129
+ distance_matrix = distance_matrix.drop(
130
+ index=services_table[services_table["capacity_left"] == 0].index.values,
131
+ columns=houses_table[houses_table["demand_left"] == 0].index.values,
132
+ errors="ignore",
133
+ )
134
+ distance_matrix = distance_matrix.loc[~(distance_matrix == np.inf).all(axis=1)]
135
+ distance_matrix = distance_matrix.loc[:, ~(distance_matrix == np.inf).all(axis=0)]
136
+
137
+ distance_matrix = distance_matrix + 1
138
+ selection_range = (self.threshold + 1) / 2
139
+ best_houses = 0.9
140
+ while len(distance_matrix.columns) > 0 and len(distance_matrix.index) > 0:
141
+ objects_n = sum(distance_matrix.shape)
142
+ logger.debug(
143
+ f"Matrix shape: {distance_matrix.shape},"
144
+ f" Total objects: {objects_n},"
145
+ f" Selection range: {selection_range},"
146
+ f" Best houses: {best_houses}"
147
+ )
148
+
149
+ temp_destination_matrix = distance_matrix.apply(
150
+ lambda x: calculate_flows_y(x[x <= selection_range]), axis=1
151
+ )
152
+ temp_destination_matrix = temp_destination_matrix.fillna(0)
153
+ temp_destination_matrix = temp_destination_matrix.apply(balance_flows_to_demands, axis=0)
154
+ temp_destination_matrix = temp_destination_matrix.fillna(0)
155
+ temp_destination_matrix_aligned = temp_destination_matrix.reindex(
156
+ index=destination_matrix.index, columns=destination_matrix.columns, fill_value=0
157
+ )
158
+ del temp_destination_matrix
159
+ destination_matrix_np = destination_matrix.to_numpy()
160
+ temp_destination_matrix_np = temp_destination_matrix_aligned.to_numpy()
161
+ del temp_destination_matrix_aligned
162
+ destination_matrix = pd.DataFrame(
163
+ destination_matrix_np + temp_destination_matrix_np,
164
+ index=destination_matrix.index,
165
+ columns=destination_matrix.columns,
166
+ )
167
+ del destination_matrix_np, temp_destination_matrix_np
168
+ axis_1 = destination_matrix.sum(axis=1).astype(int)
169
+ axis_0 = destination_matrix.sum(axis=0).astype(int)
170
+
171
+ services_table["capacity_left"] = services_table["capacity"].subtract(axis_1, fill_value=0)
172
+ houses_table["demand_left"] = houses_table["demand"].subtract(axis_0, fill_value=0)
173
+ del axis_1, axis_0
174
+ distance_matrix = distance_matrix.drop(
175
+ index=services_table[services_table["capacity_left"] == 0].index.values,
176
+ columns=houses_table[houses_table["demand_left"] == 0].index.values,
177
+ errors="ignore",
178
+ )
179
+ distance_matrix = distance_matrix.loc[~(distance_matrix == np.inf).all(axis=1)]
180
+ distance_matrix = distance_matrix.loc[:, ~(distance_matrix == np.inf).all(axis=0)]
181
+
182
+ selection_range *= 1.5
183
+ if best_houses <= 0.1:
184
+ best_houses = 0
185
+ else:
186
+ objects_n_new = sum(distance_matrix.shape)
187
+ best_houses = objects_n_new / (objects_n / best_houses)
188
+
189
+ logger.debug("Done!")
190
+ del distance_matrix, houses_table, services_table
191
+ self.destination_matrix = destination_matrix
192
+
193
+ _additional_options(
194
+ self.demanded_buildings,
195
+ self.services,
196
+ self.adjacency_matrix,
197
+ self.destination_matrix,
198
+ self.threshold,
199
+ )
200
+
201
+ return (
202
+ self.demanded_buildings,
203
+ self.services,
204
+ _calc_links(
205
+ self.destination_matrix,
206
+ self.services,
207
+ self.demanded_buildings,
208
+ self.adjacency_matrix,
209
+ ),
210
+ )
211
+
212
+
213
+ def _calc_links(
214
+ destination_matrix: pd.DataFrame,
215
+ services: gpd.GeoDataFrame,
216
+ buildings: gpd.GeoDataFrame,
217
+ distance_matrix: pd.DataFrame,
218
+ ):
219
+ buildings_ = buildings.copy()
220
+ services_ = services.copy()
221
+ buildings_.geometry = buildings_.representative_point()
222
+ services_.geometry = services_.representative_point()
223
+
224
+ def subfunc(loc):
225
+ try:
226
+ return [
227
+ {
228
+ "building_index": int(k),
229
+ "demand": int(v),
230
+ "service_index": int(loc.name),
231
+ }
232
+ for k, v in loc.to_dict().items()
233
+ ]
234
+ except:
235
+ return np.NaN
236
+
237
+ def subfunc_geom(loc):
238
+ return LineString(
239
+ (
240
+ buildings_.geometry[loc["building_index"]],
241
+ services_.geometry[loc["service_index"]],
242
+ )
243
+ )
244
+
245
+ flat_matrix = destination_matrix.transpose().apply(lambda x: subfunc(x[x > 0]), result_type="reduce")
246
+
247
+ distribution_links = gpd.GeoDataFrame(data=[item for sublist in list(flat_matrix) for item in sublist])
248
+ if distribution_links.empty:
249
+ logger.warning(
250
+ "Unable to create distribution links - no demand could be matched with service locations. "
251
+ "This is likely because either: "
252
+ "1) The demand column in buildings contains zero values, or "
253
+ "2) The capacity column in services contains zero values, or "
254
+ "3) There are no service locations within the maximum allowed distance"
255
+ )
256
+ return distribution_links
257
+ distribution_links["distance"] = distribution_links.apply(
258
+ lambda x: distance_matrix.loc[x["service_index"]][x["building_index"]],
259
+ axis=1,
260
+ result_type="reduce",
261
+ )
262
+
263
+ sel = distribution_links["building_index"].isin(buildings_.index.values) & distribution_links["service_index"].isin(
264
+ services_.index.values
265
+ )
266
+ sel = distribution_links.loc[sel[sel].index.values]
267
+ distribution_links = distribution_links.set_geometry(sel.apply(subfunc_geom, axis=1)).set_crs(buildings_.crs)
268
+ distribution_links["distance"] = distribution_links["distance"].astype(float).round(2)
269
+ return distribution_links
270
+
271
+
272
+ def _additional_options(
273
+ buildings,
274
+ services,
275
+ matrix,
276
+ destination_matrix,
277
+ normative_distance,
278
+ ):
279
+ buildings["avg_dist"] = 0
280
+ buildings["supplied_demands_within"] = 0
281
+ buildings["supplied_demands_without"] = 0
282
+ services["carried_capacity_within"] = 0
283
+ services["carried_capacity_without"] = 0
284
+ for _, loc in destination_matrix.iterrows():
285
+ distances_all = matrix.loc[loc.name]
286
+ distances = distances_all[distances_all <= normative_distance]
287
+ s = matrix.loc[loc.name] <= normative_distance
288
+ within = loc[s]
289
+ without = loc[~s]
290
+ within = within[within > 0]
291
+ without = without[without > 0]
292
+ buildings["avg_dist"] = (
293
+ buildings["avg_dist"]
294
+ .add(distances.multiply(within, fill_value=0), fill_value=0)
295
+ .add(distances_all.multiply(without, fill_value=0), fill_value=0)
296
+ )
297
+ buildings["demand_left"] = buildings["demand_left"].sub(within.add(without, fill_value=0), fill_value=0)
298
+ buildings["supplied_demands_within"] = buildings["supplied_demands_within"].add(within, fill_value=0)
299
+ buildings["supplied_demands_without"] = buildings["supplied_demands_without"].add(without, fill_value=0)
300
+
301
+ services.at[loc.name, "capacity_left"] = (
302
+ services.at[loc.name, "capacity_left"] - within.add(without, fill_value=0).sum()
303
+ )
304
+ services.at[loc.name, "carried_capacity_within"] = (
305
+ services.at[loc.name, "carried_capacity_within"] + within.sum()
306
+ )
307
+ services.at[loc.name, "carried_capacity_without"] = (
308
+ services.at[loc.name, "carried_capacity_without"] + without.sum()
309
+ )
310
+ buildings["min_dist"] = matrix.min(axis=0).replace(np.inf, None)
311
+ buildings["avg_dist"] = (buildings["avg_dist"] / (buildings["demand"] - buildings["demand_left"])).astype(
312
+ np.float32
313
+ )
314
+ buildings["avg_dist"] = buildings.apply(
315
+ lambda x: np.nan if (x["demand"] == x["demand_left"]) else round(x["avg_dist"], 2), axis=1
316
+ )
317
+ buildings["provision_value"] = (buildings["supplied_demands_within"] / buildings["demand"]).astype(float).round(2)
318
+ services["service_load"] = (services["capacity"] - services["capacity_left"]).astype(np.uint16)
319
+ buildings["supplied_demands_within"] = buildings["supplied_demands_within"].astype(np.uint16)
320
+ buildings["supplied_demands_without"] = buildings["supplied_demands_without"].astype(np.uint16)
321
+ services["carried_capacity_within"] = services["carried_capacity_within"].astype(np.uint16)
322
+ services["carried_capacity_without"] = services["carried_capacity_without"].astype(np.uint16)
323
+ logger.debug("Done adding additional options")