ObjectNat 1.1.0__py3-none-any.whl → 1.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.

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