ObjectNat 0.2.4__tar.gz → 0.2.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ObjectNat might be problematic. Click here for more details.

Files changed (22) hide show
  1. {objectnat-0.2.4 → objectnat-0.2.6}/PKG-INFO +4 -7
  2. {objectnat-0.2.4 → objectnat-0.2.6}/pyproject.toml +4 -7
  3. objectnat-0.2.6/src/objectnat/_version.py +1 -0
  4. {objectnat-0.2.4 → objectnat-0.2.6}/src/objectnat/methods/living_buildings_osm.py +2 -4
  5. {objectnat-0.2.4 → objectnat-0.2.6}/src/objectnat/methods/provision/provision.py +9 -5
  6. {objectnat-0.2.4 → objectnat-0.2.6}/src/objectnat/methods/provision/provision_model.py +98 -82
  7. objectnat-0.2.4/src/objectnat/_version.py +0 -1
  8. objectnat-0.2.4/src/objectnat/utils/__init__.py +0 -1
  9. objectnat-0.2.4/src/objectnat/utils/utils.py +0 -19
  10. {objectnat-0.2.4 → objectnat-0.2.6}/LICENSE.txt +0 -0
  11. {objectnat-0.2.4 → objectnat-0.2.6}/README.md +0 -0
  12. {objectnat-0.2.4 → objectnat-0.2.6}/src/objectnat/__init__.py +0 -0
  13. {objectnat-0.2.4 → objectnat-0.2.6}/src/objectnat/_api.py +0 -0
  14. {objectnat-0.2.4 → objectnat-0.2.6}/src/objectnat/_config.py +0 -0
  15. {objectnat-0.2.4 → objectnat-0.2.6}/src/objectnat/methods/__init__.py +0 -0
  16. {objectnat-0.2.4 → objectnat-0.2.6}/src/objectnat/methods/balanced_buildings.py +0 -0
  17. {objectnat-0.2.4 → objectnat-0.2.6}/src/objectnat/methods/cluster_points_in_polygons.py +0 -0
  18. {objectnat-0.2.4 → objectnat-0.2.6}/src/objectnat/methods/coverage_zones.py +0 -0
  19. {objectnat-0.2.4 → objectnat-0.2.6}/src/objectnat/methods/isochrones.py +0 -0
  20. {objectnat-0.2.4 → objectnat-0.2.6}/src/objectnat/methods/provision/__init__.py +0 -0
  21. {objectnat-0.2.4 → objectnat-0.2.6}/src/objectnat/methods/provision/provision_exceptions.py +0 -0
  22. {objectnat-0.2.4 → objectnat-0.2.6}/src/objectnat/methods/visibility_analysis.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ObjectNat
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: ObjectNat is an open-source library created for geospatial analysis created by IDU team
5
5
  License: BSD-3-Clause
6
6
  Author: DDonnyy
@@ -12,15 +12,12 @@ Classifier: Programming Language :: Python :: 3.10
12
12
  Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Requires-Dist: geopandas (>=0.14.3,<0.15.0)
15
- Requires-Dist: iduedu (>=0.2.0,<0.3.0)
16
- Requires-Dist: joblib (>=1.4.2,<2.0.0)
17
- Requires-Dist: jupyter (>=1.1.1,<2.0.0)
18
- Requires-Dist: networkx (>=3.2.1,<4.0.0)
19
- Requires-Dist: numpy (>=1.23.5,<2.0.0)
15
+ Requires-Dist: iduedu (>=0.2.2,<0.3.0)
16
+ Requires-Dist: networkx (>=3.3,<4.0)
17
+ Requires-Dist: numpy (>=1.26.4,<2.0.0)
20
18
  Requires-Dist: pandarallel (>=1.6.5,<2.0.0)
21
19
  Requires-Dist: pandas (>=2.2.0,<3.0.0)
22
20
  Requires-Dist: population-restorator (>=0.2.3,<0.3.0)
23
- Requires-Dist: pulp (>=2.8.0,<3.0.0)
24
21
  Requires-Dist: scikit-learn (>=1.4.0,<2.0.0)
25
22
  Requires-Dist: tqdm (>=4.66.2,<5.0.0)
26
23
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "ObjectNat"
3
- version = "0.2.4"
3
+ version = "0.2.6"
4
4
  description = "ObjectNat is an open-source library created for geospatial analysis created by IDU team"
5
5
  license = "BSD-3-Clause"
6
6
  authors = ["DDonnyy <63115678+DDonnyy@users.noreply.github.com>"]
@@ -10,18 +10,15 @@ packages = [{ include = "objectnat", from = "src" }]
10
10
 
11
11
  [tool.poetry.dependencies]
12
12
  python = ">=3.10,<3.13"
13
- numpy = "^1.23.5"
13
+ numpy = "^1.26.4"
14
14
  pandas = "^2.2.0"
15
15
  geopandas = "^0.14.3"
16
16
  tqdm = "^4.66.2"
17
17
  pandarallel = "^1.6.5"
18
- networkx = "^3.2.1"
19
- pulp = "^2.8.0"
18
+ networkx = "^3.3"
20
19
  population-restorator = "^0.2.3"
21
- iduedu = "^0.2.0"
22
- joblib = "^1.4.2"
20
+ iduedu = "^0.2.2"
23
21
  scikit-learn = "^1.4.0"
24
- jupyter = "^1.1.1"
25
22
 
26
23
 
27
24
  [tool.poetry.group.dev.dependencies]
@@ -0,0 +1 @@
1
+ VERSION = "0.2.6"
@@ -7,8 +7,6 @@ from shapely import MultiPolygon, Polygon
7
7
 
8
8
  from objectnat import config
9
9
 
10
- from ..utils import get_utm_crs_for_4326_gdf
11
-
12
10
  logger = config.logger
13
11
 
14
12
 
@@ -75,8 +73,8 @@ def eval_population(source: gpd.GeoDataFrame, population_column: str, area_per_p
75
73
  if "building:levels" not in source.columns:
76
74
  raise RuntimeError("No 'building:levels' column in provided GeoDataFrame")
77
75
  df = source.copy()
78
- local_utm_crs = get_utm_crs_for_4326_gdf(source.to_crs(4326))
79
- df["area"] = df.to_crs(local_utm_crs.to_epsg()).geometry.area.astype(float)
76
+ local_crs = source.estimate_utm_crs()
77
+ df["area"] = df.to_crs(local_crs).geometry.area.astype(float)
80
78
  df["building:levels_is_real"] = df["building:levels"].apply(lambda x: False if pd.isna(x) else True)
81
79
  df["building:levels"] = df["building:levels"].fillna(1)
82
80
  df["building:levels"] = pd.to_numeric(df["building:levels"], errors="coerce")
@@ -4,8 +4,12 @@ import geopandas as gpd
4
4
  import numpy as np
5
5
  import pandas as pd
6
6
 
7
+ from objectnat import config
8
+
7
9
  from .provision_model import Provision
8
10
 
11
+ logger = config.logger
12
+
9
13
 
10
14
  def get_service_provision(
11
15
  buildings: gpd.GeoDataFrame,
@@ -39,7 +43,7 @@ def get_service_provision(
39
43
  demanded_buildings=buildings,
40
44
  adjacency_matrix=adjacency_matrix,
41
45
  threshold=threshold,
42
- ).get_provisions()
46
+ ).run()
43
47
  return provision_buildings, provision_services, provision_links
44
48
 
45
49
 
@@ -69,12 +73,12 @@ def recalculate_links(
69
73
  services = services.copy()
70
74
  links = links.copy()
71
75
 
72
- max_dist = links["distance"].max()
73
- assert new_max_dist <= max_dist, "New distance exceeds max links distance"
74
-
75
76
  links_to_recalculate = links[links["distance"] > new_max_dist]
76
- links_to_keep = links[links["distance"] <= new_max_dist]
77
+ if len(links_to_recalculate) == 0:
78
+ logger.warning("To clip distance exceeds max links distance, returning full provision")
79
+ return buildings, services, links
77
80
 
81
+ links_to_keep = links[links["distance"] <= new_max_dist]
78
82
  free_demand = links_to_recalculate.groupby("building_index").agg({"demand": list, "distance": list})
79
83
  free_demand["distance"] = free_demand.apply(
80
84
  lambda x: sum((x1 * x2) for x1, x2 in zip(x.demand, x.distance)), axis=1
@@ -33,7 +33,7 @@ class Provision:
33
33
  column in 'services' or 'demand' column 'demanded_buildings' GeoDataFrame has no valid value.
34
34
  """
35
35
 
36
- _destination_matrix = None
36
+ destination_matrix = None
37
37
 
38
38
  def __init__(
39
39
  self,
@@ -42,12 +42,13 @@ class Provision:
42
42
  adjacency_matrix: pd.DataFrame,
43
43
  threshold: int,
44
44
  ):
45
- self.services = self.ensure_services(services)
46
- self.demanded_buildings = self.ensure_buildings(demanded_buildings)
47
- self.adjacency_matrix = self.delete_useless_matrix_rows_columns(adjacency_matrix, demanded_buildings, services)
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()
48
50
  self.threshold = threshold
49
51
  self.check_crs(self.demanded_buildings, self.services)
50
- print(config.pandarallel_use_file_system)
51
52
  pandarallel.initialize(progress_bar=False, verbose=0, use_memory_fs=config.pandarallel_use_file_system)
52
53
 
53
54
  @staticmethod
@@ -83,63 +84,16 @@ class Provision:
83
84
  columns = set(adjacency_matrix.columns.astype(int).tolist())
84
85
  dif = columns ^ service_indexes
85
86
  adjacency_matrix.drop(columns=(list(dif)), axis=0, inplace=True)
86
- return adjacency_matrix
87
+ return adjacency_matrix.transpose()
87
88
 
88
- def get_provisions(self) -> Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]:
89
- self._destination_matrix = pd.DataFrame(
90
- 0,
91
- index=self.adjacency_matrix.columns,
92
- columns=self.adjacency_matrix.index,
93
- )
94
- self.adjacency_matrix = self.adjacency_matrix.transpose()
95
- logger.debug(
96
- "Calculating provision from {} services to {} buildings.",
97
- len(self.services),
98
- len(self.demanded_buildings),
99
- )
100
- self.adjacency_matrix = self.adjacency_matrix.where(self.adjacency_matrix <= self.threshold * 3, np.inf)
101
-
102
- self._destination_matrix = self._provision_loop_gravity(
103
- self.demanded_buildings.copy(),
104
- self.services.copy(),
105
- self.adjacency_matrix.copy() + 1,
106
- self.threshold,
107
- self._destination_matrix.copy(),
108
- )
109
- _additional_options(
110
- self.demanded_buildings,
111
- self.services,
112
- self.adjacency_matrix,
113
- self._destination_matrix,
114
- self.threshold,
115
- )
116
-
117
- return (
118
- self.demanded_buildings,
119
- self.services,
120
- _calc_links(
121
- self._destination_matrix,
122
- self.services,
123
- self.demanded_buildings,
124
- self.adjacency_matrix,
125
- ),
126
- )
89
+ def run(self) -> Tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]:
127
90
 
128
- def _provision_loop_gravity(
129
- self,
130
- houses_table: gpd.GeoDataFrame,
131
- services_table: gpd.GeoDataFrame,
132
- distance_matrix: pd.DataFrame,
133
- selection_range,
134
- destination_matrix: pd.DataFrame,
135
- best_houses=0.9,
136
- ):
137
- def apply_function_based_on_size(df, func, axis, threshold=500):
91
+ def apply_function_based_on_size(df, func, axis, threshold=100):
138
92
  if len(df) > threshold:
139
93
  return df.parallel_apply(func, axis=axis)
140
94
  return df.apply(func, axis=axis)
141
95
 
142
- def _calculate_flows_y(loc):
96
+ def calculate_flows_y(loc):
143
97
  import numpy as np # pylint: disable=redefined-outer-name,reimported,import-outside-toplevel
144
98
  import pandas as pd # pylint: disable=redefined-outer-name,reimported,import-outside-toplevel
145
99
 
@@ -158,7 +112,7 @@ class Provision:
158
112
 
159
113
  return choice
160
114
 
161
- def _balance_flows_to_demands(loc):
115
+ def balance_flows_to_demands(loc):
162
116
  import numpy as np # pylint: disable=redefined-outer-name,reimported,import-outside-toplevel
163
117
  import pandas as pd # pylint: disable=redefined-outer-name,reimported,import-outside-toplevel
164
118
 
@@ -177,43 +131,104 @@ class Provision:
177
131
  return choice
178
132
  return loc
179
133
 
180
- temp_destination_matrix = apply_function_based_on_size(
181
- distance_matrix, lambda x: _calculate_flows_y(x[x <= selection_range]), 1
134
+ logger.debug(
135
+ f"Calculating provision from {len(self.services)} services to {len(self.demanded_buildings)} buildings."
182
136
  )
183
137
 
184
- temp_destination_matrix = temp_destination_matrix.fillna(0)
185
-
186
- temp_destination_matrix = apply_function_based_on_size(temp_destination_matrix, _balance_flows_to_demands, 0)
187
-
188
- temp_destination_matrix = temp_destination_matrix.fillna(0)
189
- destination_matrix = destination_matrix.add(temp_destination_matrix, fill_value=0)
190
-
191
- axis_1 = destination_matrix.sum(axis=1)
192
- axis_0 = destination_matrix.sum(axis=0)
193
-
194
- services_table["capacity_left"] = services_table["capacity"].subtract(axis_1, fill_value=0)
195
- houses_table["demand_left"] = houses_table["demand"].subtract(axis_0, fill_value=0)
138
+ distance_matrix = self.adjacency_matrix
139
+ destination_matrix = pd.DataFrame(
140
+ 0,
141
+ index=distance_matrix.index,
142
+ columns=distance_matrix.columns,
143
+ dtype=int,
144
+ )
145
+ distance_matrix = distance_matrix.where(distance_matrix <= self.threshold * 3, np.inf)
196
146
 
147
+ houses_table = self.demanded_buildings[["demand", "demand_left"]].copy()
148
+ services_table = self.services[["capacity", "capacity_left"]].copy()
197
149
  distance_matrix = distance_matrix.drop(
198
150
  index=services_table[services_table["capacity_left"] == 0].index.values,
199
151
  columns=houses_table[houses_table["demand_left"] == 0].index.values,
200
152
  errors="ignore",
201
153
  )
202
-
203
154
  distance_matrix = distance_matrix.loc[~(distance_matrix == np.inf).all(axis=1)]
204
155
  distance_matrix = distance_matrix.loc[:, ~(distance_matrix == np.inf).all(axis=0)]
205
156
 
206
- selection_range += selection_range
157
+ distance_matrix = distance_matrix + 1
158
+ selection_range = (self.threshold + 1) / 2
159
+ best_houses = 0.9
160
+ while len(distance_matrix.columns) > 0 and len(distance_matrix.index) > 0:
161
+ objects_n = sum(distance_matrix.shape)
162
+ logger.debug(
163
+ f"Matrix shape: {distance_matrix.shape},"
164
+ f" Total objects: {objects_n},"
165
+ f" Selection range: {selection_range},"
166
+ f" Best houses: {best_houses}"
167
+ )
168
+
169
+ temp_destination_matrix = apply_function_based_on_size(
170
+ distance_matrix, lambda x: calculate_flows_y(x[x <= selection_range]), 1
171
+ )
207
172
 
208
- if best_houses > 0.1:
209
- best_houses -= 0.1
173
+ temp_destination_matrix = temp_destination_matrix.fillna(0)
174
+ temp_destination_matrix = apply_function_based_on_size(temp_destination_matrix, balance_flows_to_demands, 0)
175
+ temp_destination_matrix = temp_destination_matrix.fillna(0)
176
+ temp_destination_matrix_aligned = temp_destination_matrix.reindex(
177
+ index=destination_matrix.index, columns=destination_matrix.columns, fill_value=0
178
+ )
179
+ del temp_destination_matrix
180
+ destination_matrix_np = destination_matrix.to_numpy()
181
+ temp_destination_matrix_np = temp_destination_matrix_aligned.to_numpy()
182
+ del temp_destination_matrix_aligned
183
+ destination_matrix = pd.DataFrame(
184
+ destination_matrix_np + temp_destination_matrix_np,
185
+ index=destination_matrix.index,
186
+ columns=destination_matrix.columns,
187
+ )
188
+ del destination_matrix_np, temp_destination_matrix_np
189
+ axis_1 = destination_matrix.sum(axis=1).astype(int)
190
+ axis_0 = destination_matrix.sum(axis=0).astype(int)
191
+
192
+ services_table["capacity_left"] = services_table["capacity"].subtract(axis_1, fill_value=0)
193
+ houses_table["demand_left"] = houses_table["demand"].subtract(axis_0, fill_value=0)
194
+ del axis_1, axis_0
195
+ distance_matrix = distance_matrix.drop(
196
+ index=services_table[services_table["capacity_left"] == 0].index.values,
197
+ columns=houses_table[houses_table["demand_left"] == 0].index.values,
198
+ errors="ignore",
199
+ )
200
+ distance_matrix = distance_matrix.loc[~(distance_matrix == np.inf).all(axis=1)]
201
+ distance_matrix = distance_matrix.loc[:, ~(distance_matrix == np.inf).all(axis=0)]
202
+
203
+ selection_range *= 1.5
210
204
  if best_houses <= 0.1:
211
205
  best_houses = 0
212
- if len(distance_matrix.columns) > 0 and len(distance_matrix.index) > 0:
213
- return self._provision_loop_gravity(
214
- houses_table, services_table, distance_matrix, selection_range, destination_matrix, best_houses
215
- )
216
- return destination_matrix
206
+ else:
207
+ objects_n_new = sum(distance_matrix.shape)
208
+ best_houses = objects_n_new / (objects_n / best_houses)
209
+
210
+ logger.debug("Done!")
211
+ del distance_matrix, houses_table, services_table
212
+ self.destination_matrix = destination_matrix
213
+
214
+ _additional_options(
215
+ self.demanded_buildings,
216
+ self.services,
217
+ self.adjacency_matrix,
218
+ self.destination_matrix,
219
+ self.threshold,
220
+ )
221
+
222
+ return (
223
+ self.demanded_buildings,
224
+ self.services,
225
+ _calc_links(
226
+ self.destination_matrix,
227
+ self.services,
228
+ self.demanded_buildings,
229
+ self.adjacency_matrix,
230
+ ),
231
+ )
217
232
 
218
233
 
219
234
  def _calc_links(
@@ -279,8 +294,7 @@ def _additional_options(
279
294
  buildings["supplyed_demands_without"] = 0
280
295
  services["carried_capacity_within"] = 0
281
296
  services["carried_capacity_without"] = 0
282
- for i in range(len(destination_matrix)):
283
- loc = destination_matrix.iloc[i]
297
+ for i, loc in destination_matrix.iterrows():
284
298
  distances_all = matrix.loc[loc.name]
285
299
  distances = distances_all[distances_all <= normative_distance]
286
300
  s = matrix.loc[loc.name] <= normative_distance
@@ -306,6 +320,7 @@ def _additional_options(
306
320
  services.at[loc.name, "carried_capacity_without"] = (
307
321
  services.at[loc.name, "carried_capacity_without"] + without.sum()
308
322
  )
323
+ buildings["min_dist"] = matrix.min(axis=0).replace(np.inf, None)
309
324
  buildings["avg_dist"] = (buildings["avg_dist"] / (buildings["demand"] - buildings["demand_left"])).astype(
310
325
  np.float32
311
326
  )
@@ -318,3 +333,4 @@ def _additional_options(
318
333
  buildings["supplyed_demands_without"] = buildings["supplyed_demands_without"].astype(np.uint16)
319
334
  services["carried_capacity_within"] = services["carried_capacity_within"].astype(np.uint16)
320
335
  services["carried_capacity_without"] = services["carried_capacity_without"].astype(np.uint16)
336
+ logger.debug("Done adding additional options")
@@ -1 +0,0 @@
1
- VERSION = "0.2.4"
@@ -1 +0,0 @@
1
- from .utils import get_utm_crs_for_4326_gdf
@@ -1,19 +0,0 @@
1
- import geopandas as gpd
2
- from pyproj import CRS
3
- from pyproj.aoi import AreaOfInterest
4
- from pyproj.database import query_utm_crs_info
5
-
6
-
7
- def get_utm_crs_for_4326_gdf(gdf: gpd.GeoDataFrame) -> CRS:
8
- assert gdf.crs == CRS.from_epsg(4326), "provided GeoDataFrame is not in EPSG 4326"
9
- minx, miny, maxx, maxy = gdf.total_bounds
10
- utm_crs_list = query_utm_crs_info(
11
- datum_name="WGS 84",
12
- area_of_interest=AreaOfInterest(
13
- west_lon_degree=minx,
14
- south_lat_degree=miny,
15
- east_lon_degree=maxx,
16
- north_lat_degree=maxy,
17
- ),
18
- )
19
- return CRS.from_epsg(utm_crs_list[0].code)
File without changes
File without changes