ObjectNat 0.2.7__py3-none-any.whl → 1.0.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 (31) hide show
  1. objectnat/_api.py +5 -8
  2. objectnat/_config.py +0 -24
  3. objectnat/_version.py +1 -1
  4. objectnat/methods/coverage_zones/__init__.py +2 -0
  5. objectnat/methods/coverage_zones/graph_coverage.py +118 -0
  6. objectnat/methods/coverage_zones/radius_voronoi.py +45 -0
  7. objectnat/methods/isochrones/__init__.py +1 -0
  8. objectnat/methods/isochrones/isochrone_utils.py +130 -0
  9. objectnat/methods/isochrones/isochrones.py +325 -0
  10. objectnat/methods/noise/__init__.py +2 -2
  11. objectnat/methods/noise/noise_sim.py +14 -9
  12. objectnat/methods/point_clustering/__init__.py +1 -0
  13. objectnat/methods/{cluster_points_in_polygons.py → point_clustering/cluster_points_in_polygons.py} +22 -28
  14. objectnat/methods/provision/__init__.py +1 -0
  15. objectnat/methods/provision/provision.py +4 -4
  16. objectnat/methods/provision/provision_model.py +17 -18
  17. objectnat/methods/utils/geom_utils.py +54 -3
  18. objectnat/methods/utils/graph_utils.py +127 -0
  19. objectnat/methods/utils/math_utils.py +32 -0
  20. objectnat/methods/visibility/__init__.py +6 -0
  21. objectnat/methods/{visibility_analysis.py → visibility/visibility_analysis.py} +167 -208
  22. objectnat-1.0.1.dist-info/METADATA +142 -0
  23. objectnat-1.0.1.dist-info/RECORD +32 -0
  24. objectnat/methods/balanced_buildings.py +0 -69
  25. objectnat/methods/coverage_zones.py +0 -90
  26. objectnat/methods/isochrones.py +0 -143
  27. objectnat/methods/living_buildings_osm.py +0 -168
  28. objectnat-0.2.7.dist-info/METADATA +0 -118
  29. objectnat-0.2.7.dist-info/RECORD +0 -26
  30. {objectnat-0.2.7.dist-info → objectnat-1.0.1.dist-info}/LICENSE.txt +0 -0
  31. {objectnat-0.2.7.dist-info → objectnat-1.0.1.dist-info}/WHEEL +0 -0
@@ -6,30 +6,33 @@ import numpy as np
6
6
  import pandas as pd
7
7
  from pandarallel import pandarallel
8
8
  from shapely import LineString, MultiPolygon, Point, Polygon
9
- from shapely.ops import polygonize, unary_union
9
+ from shapely.ops import unary_union
10
10
  from tqdm.contrib.concurrent import process_map
11
11
 
12
12
  from objectnat import config
13
13
  from objectnat.methods.utils.geom_utils import (
14
+ combine_geometry,
14
15
  explode_linestring,
15
16
  get_point_from_a_thorough_b,
16
17
  point_side_of_line,
17
18
  polygons_to_multilinestring,
18
19
  )
20
+ from objectnat.methods.utils.math_utils import min_max_normalization
19
21
 
20
22
  logger = config.logger
21
23
 
22
24
 
23
25
  def get_visibility_accurate(
24
- point_from: Point, obstacles: gpd.GeoDataFrame, view_distance, return_max_view_dist=False
25
- ) -> Polygon | tuple[Polygon, float]:
26
+ point_from: Point | gpd.GeoDataFrame, obstacles: gpd.GeoDataFrame, view_distance, return_max_view_dist=False
27
+ ) -> Polygon | gpd.GeoDataFrame | tuple[Polygon | gpd.GeoDataFrame, float]:
26
28
  """
27
29
  Function to get accurate visibility from a given point to buildings within a given distance.
28
30
 
29
31
  Parameters
30
32
  ----------
31
- point_from : Point
32
- The point from which the line of sight is drawn.
33
+ point_from : Point | gpd.GeoDataFrame
34
+ The point or GeoDataFrame with 1 point from which the line of sight is drawn.
35
+ If Point is provided it should be in the same crs as obstacles
33
36
  obstacles : gpd.GeoDataFrame
34
37
  A GeoDataFrame containing the geometry of the obstacles.
35
38
  view_distance : float
@@ -39,20 +42,21 @@ def get_visibility_accurate(
39
42
 
40
43
  Returns
41
44
  -------
42
- Polygon | tuple[Polygon, float]
45
+ Polygon | gpd.GeoDataFrame | tuple[Polygon | gpd.GeoDataFrame, float]
43
46
  A polygon representing the area of visibility from the given point or polygon with max view distance.
47
+ if point_from was a GeoDataFrame, return GeoDataFrame with one feature, else Polygon.
44
48
 
45
49
  Notes
46
50
  -----
47
- If a quick result is important, consider using the `get_visibility_result()` function instead.
48
- However, please note that `get_visibility_result()` may provide less accurate results.
51
+ If a quick result is important, consider using the `get_visibility()` function instead.
52
+ However, please note that `get_visibility()` may provide less accurate results.
49
53
 
50
54
  Examples
51
55
  --------
52
- >>> point_from = Point(1, 1)
53
- >>> buildings = gpd.read_file('buildings.shp')
54
- >>> view_distance = 1000
55
- >>> visibility = get_visibility_accurate(point_from, obstacles, view_distance)
56
+ >>> from objectnat import get_visibility_accurate
57
+ >>> obstacles = gpd.read_parquet('examples_data/buildings.parquet')
58
+ >>> point_from = gpd.GeoDataFrame(geometry=[Point(30.2312112, 59.9482336)], crs=4326)
59
+ >>> result = get_visibility_accurate(point_from, obstacles, 500)
56
60
  """
57
61
 
58
62
  def find_furthest_point(point_from, view_polygon):
@@ -63,7 +67,27 @@ def get_visibility_accurate(
63
67
  raise e
64
68
  return res
65
69
 
66
- obstacles = obstacles.copy()
70
+ local_crs = None
71
+ original_crs = None
72
+ return_gdf = False
73
+ if isinstance(point_from, gpd.GeoDataFrame):
74
+ original_crs = point_from.crs
75
+ return_gdf = True
76
+ if len(obstacles) > 0:
77
+ local_crs = obstacles.estimate_utm_crs()
78
+ else:
79
+ local_crs = point_from.estimate_utm_crs()
80
+ obstacles = obstacles.to_crs(local_crs)
81
+ point_from = point_from.to_crs(local_crs)
82
+ if len(point_from) > 1:
83
+ logger.warning(
84
+ f"This method processes only single point. The GeoDataFrame contains {len(point_from)} points - "
85
+ "only the first geometry will be used for isochrone calculation. "
86
+ )
87
+ point_from = point_from.iloc[0].geometry
88
+ else:
89
+ obstacles = obstacles.copy()
90
+
67
91
  obstacles.reset_index(inplace=True, drop=True)
68
92
 
69
93
  point_buffer = point_from.buffer(view_distance, resolution=32)
@@ -130,30 +154,32 @@ def get_visibility_accurate(
130
154
  logger.debug("Done calculating!")
131
155
  res = point_buffer.difference(unary_union(polygons + obstacles_in_buffer.to_list()))
132
156
 
133
- if isinstance(res, Polygon):
134
- if return_max_view_dist:
135
- return res, find_furthest_point(point_from, res)
136
- return res
137
- res = list(res.geoms)
138
- polygon_containing_point = None
157
+ if isinstance(res, MultiPolygon):
158
+ res = list(res.geoms)
159
+ for polygon in res:
160
+ if polygon.intersects(point_from):
161
+ res = polygon
162
+ break
163
+
164
+ if return_gdf:
165
+ res = gpd.GeoDataFrame(geometry=[res], crs=local_crs).to_crs(original_crs)
139
166
 
140
- for polygon in res:
141
- if polygon.intersects(point_from):
142
- polygon_containing_point = polygon
143
- break
144
167
  if return_max_view_dist:
145
- return polygon_containing_point, find_furthest_point(point_from, polygon_containing_point)
146
- return polygon_containing_point
168
+ return res, find_furthest_point(point_from, res)
169
+ return res
147
170
 
148
171
 
149
- def get_visibility(point: Point, obstacles: gpd.GeoDataFrame, view_distance: float, resolution: int = 32) -> Polygon:
172
+ def get_visibility(
173
+ point_from: Point | gpd.GeoDataFrame, obstacles: gpd.GeoDataFrame, view_distance: float, resolution: int = 32
174
+ ) -> Polygon | gpd.GeoDataFrame:
150
175
  """
151
176
  Function to get a quick estimate of visibility from a given point to buildings within a given distance.
152
177
 
153
178
  Parameters
154
179
  ----------
155
- point : Point
156
- The point from which the line of sight is drawn.
180
+ point_from : Point | gpd.GeoDataFrame
181
+ The point or GeoDataFrame with 1 point from which the line of sight is drawn.
182
+ If Point is provided it should be in the same crs as obstacles
157
183
  obstacles : gpd.GeoDataFrame
158
184
  A GeoDataFrame containing the geometry of the buildings.
159
185
  view_distance : float
@@ -163,8 +189,9 @@ def get_visibility(point: Point, obstacles: gpd.GeoDataFrame, view_distance: flo
163
189
 
164
190
  Returns
165
191
  -------
166
- Polygon
167
- A polygon representing the estimated area of visibility from the given point.
192
+ Polygon | gpd.GeoDataFrame
193
+ A polygon representing the area of visibility from the given point.
194
+ if point_from was a GeoDataFrame, return GeoDataFrame with one feature, else Polygon.
168
195
 
169
196
  Notes
170
197
  -----
@@ -173,19 +200,36 @@ def get_visibility(point: Point, obstacles: gpd.GeoDataFrame, view_distance: flo
173
200
 
174
201
  Examples
175
202
  --------
176
- >>> point = Point(1, 1)
177
- >>> buildings = gpd.read_file('buildings.shp')
178
- >>> view_distance = 1000
179
- >>> visibility = get_visibility(point, obstacles, view_distance)
203
+ >>> from objectnat import get_visibility
204
+ >>> obstacles = gpd.read_parquet('examples_data/buildings.parquet')
205
+ >>> point_from = gpd.GeoDataFrame(geometry=[Point(30.2312112, 59.9482336)], crs=4326)
206
+ >>> result = get_visibility(point_from, obstacles, 500)
180
207
  """
181
-
182
- point_buffer = point.buffer(view_distance, resolution=resolution)
183
- s = obstacles.within(point_buffer)
208
+ return_gdf = False
209
+ if isinstance(point_from, gpd.GeoDataFrame):
210
+ original_crs = point_from.crs
211
+ return_gdf = True
212
+ if len(obstacles) > 0:
213
+ local_crs = obstacles.estimate_utm_crs()
214
+ else:
215
+ local_crs = point_from.estimate_utm_crs()
216
+ obstacles = obstacles.to_crs(local_crs)
217
+ point_from = point_from.to_crs(local_crs)
218
+ if len(point_from) > 1:
219
+ logger.warning(
220
+ f"This method processes only single point. The GeoDataFrame contains {len(point_from)} points - "
221
+ "only the first geometry will be used for isochrone calculation. "
222
+ )
223
+ point_from = point_from.iloc[0].geometry
224
+ else:
225
+ obstacles = obstacles.copy()
226
+ point_buffer = point_from.buffer(view_distance, resolution=resolution)
227
+ s = obstacles.intersects(point_buffer)
184
228
  buildings_in_buffer = obstacles.loc[s[s].index].reset_index(drop=True)
185
229
  buffer_exterior_ = list(point_buffer.exterior.coords)
186
- line_geometry = [LineString([point, ext]) for ext in buffer_exterior_]
230
+ line_geometry = [LineString([point_from, ext]) for ext in buffer_exterior_]
187
231
  buffer_lines_gdf = gpd.GeoDataFrame(geometry=line_geometry)
188
- united_buildings = buildings_in_buffer.unary_union
232
+ united_buildings = buildings_in_buffer.union_all()
189
233
  if united_buildings:
190
234
  splited_lines = buffer_lines_gdf["geometry"].apply(lambda x: x.difference(united_buildings))
191
235
  else:
@@ -199,158 +243,10 @@ def get_visibility(point: Point, obstacles: gpd.GeoDataFrame, view_distance: flo
199
243
  circuit = Polygon(splited_lines_list)
200
244
  if united_buildings:
201
245
  circuit = circuit.difference(united_buildings)
202
- return circuit
203
-
204
-
205
- def _multiprocess_get_vis(args):
206
- point, buildings, view_distance, sectors_n = args
207
- result = get_visibility_accurate(point, buildings, view_distance)
208
-
209
- if sectors_n is not None:
210
- sectors = []
211
-
212
- cx, cy = point.x, point.y
213
-
214
- angle_increment = 2 * math.pi / sectors_n
215
- view_distance = math.sqrt((view_distance**2) * (1 + (math.tan(angle_increment / 2) ** 2)))
216
- for i in range(sectors_n):
217
- angle1 = i * angle_increment
218
- angle2 = (i + 1) * angle_increment
219
-
220
- x1, y1 = cx + view_distance * math.cos(angle1), cy + view_distance * math.sin(angle1)
221
- x2, y2 = cx + view_distance * math.cos(angle2), cy + view_distance * math.sin(angle2)
222
-
223
- sector_triangle = Polygon([point, (x1, y1), (x2, y2)])
224
- sector = result.intersection(sector_triangle)
225
-
226
- if not sector.is_empty:
227
- sectors.append(sector)
228
- result = sectors
229
- return result
230
-
231
-
232
- def _polygons_to_linestring(geom):
233
- # pylint: disable-next=redefined-outer-name,reimported,import-outside-toplevel
234
- from shapely import LineString, MultiLineString, MultiPolygon
235
-
236
- def convert_polygon(polygon: Polygon):
237
- lines = []
238
- exterior = LineString(polygon.exterior.coords)
239
- lines.append(exterior)
240
- interior = [LineString(p.coords) for p in polygon.interiors]
241
- lines = lines + interior
242
- return lines
243
-
244
- def convert_multipolygon(polygon: MultiPolygon):
245
- return MultiLineString(sum([convert_polygon(p) for p in polygon.geoms], []))
246
-
247
- if geom.geom_type == "Polygon":
248
- return MultiLineString(convert_polygon(geom))
249
- if geom.geom_type == "MultiPolygon":
250
- return convert_multipolygon(geom)
251
- return geom
252
-
253
-
254
- def _combine_geometry(gdf: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
255
- """
256
- Combine geometry of intersecting layers into a single GeoDataFrame.
257
- Parameters
258
- ----------
259
- gdf: gpd.GeoDataFrame
260
- A GeoPandas GeoDataFrame
261
-
262
- Returns
263
- -------
264
- gpd.GeoDataFrame
265
- The combined GeoDataFrame with aggregated in lists columns.
266
-
267
- Examples
268
- --------
269
- >>> gdf = gpd.read_file('path_to_your_file.geojson')
270
- >>> result = _combine_geometry(gdf)
271
- """
272
-
273
- crs = gdf.crs
274
- polygons = polygonize(gdf["geometry"].apply(_polygons_to_linestring).unary_union)
275
- enclosures = gpd.GeoSeries(list(polygons), crs=crs)
276
- enclosures_points = gpd.GeoDataFrame(enclosures.representative_point(), columns=["geometry"], crs=crs)
277
- joined = gpd.sjoin(enclosures_points, gdf, how="inner", predicate="within").reset_index()
278
- cols = joined.columns.tolist()
279
- cols.remove("geometry")
280
- joined = joined.groupby("index").agg({column: list for column in cols})
281
- joined["geometry"] = enclosures
282
- joined = gpd.GeoDataFrame(joined, geometry="geometry", crs=crs)
283
- return joined
284
-
285
-
286
- def _min_max_normalization(data, new_min=0, new_max=1):
287
- """
288
- Min-max normalization for a given array of data.
289
-
290
- Parameters
291
- ----------
292
- data: numpy.ndarray
293
- Input data to be normalized.
294
- new_min: float, optional
295
- New minimum value for normalization. Defaults to 0.
296
- new_max: float, optional
297
- New maximum value for normalization. Defaults to 1.
298
-
299
- Returns
300
- -------
301
- numpy.ndarray
302
- Normalized data.
303
-
304
- Examples
305
- --------
306
- >>> import numpy as np
307
- >>> data = np.array([1, 2, 3, 4, 5])
308
- >>> normalized_data = min_max_normalization(data, new_min=0, new_max=1)
309
- """
310
-
311
- min_value = np.min(data)
312
- max_value = np.max(data)
313
- normalized_data = (data - min_value) / (max_value - min_value) * (new_max - new_min) + new_min
314
- return normalized_data
315
-
316
-
317
- def _process_group(group):
318
- geom = group
319
- combined_geometry = _combine_geometry(geom)
320
- combined_geometry.drop(columns=["index", "index_right"], inplace=True)
321
- combined_geometry["count_n"] = combined_geometry["ratio"].apply(len)
322
- combined_geometry["new_ratio"] = combined_geometry.apply(
323
- lambda x: np.power(np.prod(x.ratio), 1 / x.count_n) * x.count_n, axis=1
324
- )
325
-
326
- threshold = combined_geometry["new_ratio"].quantile(0.25)
327
- combined_geometry = combined_geometry[combined_geometry["new_ratio"] > threshold]
328
-
329
- combined_geometry["new_ratio_normalized"] = _min_max_normalization(
330
- combined_geometry["new_ratio"].values, new_min=1, new_max=10
331
- )
332
-
333
- combined_geometry["new_ratio_normalized"] = np.round(combined_geometry["new_ratio_normalized"]).astype(int)
334
246
 
335
- result_union = (
336
- combined_geometry.groupby("new_ratio_normalized")
337
- .agg({"geometry": lambda x: unary_union(MultiPolygon(list(x)).buffer(0))})
338
- .reset_index(drop=True)
339
- )
340
- result_union.set_geometry("geometry", inplace=True)
341
- result_union.set_crs(geom.crs, inplace=True)
342
-
343
- result_union = result_union.explode("geometry", index_parts=False).reset_index(drop=True)
344
-
345
- representative_points = combined_geometry.copy()
346
- representative_points["geometry"] = representative_points["geometry"].representative_point()
347
-
348
- joined = gpd.sjoin(result_union, representative_points, how="inner", predicate="contains").reset_index()
349
- joined = joined.groupby("index").agg({"geometry": "first", "new_ratio": lambda x: np.mean(list(x))})
350
-
351
- joined.set_geometry("geometry", inplace=True)
352
- joined.set_crs(geom.crs, inplace=True)
353
- return joined
247
+ if return_gdf:
248
+ circuit = gpd.GeoDataFrame(geometry=[circuit], crs=local_crs).to_crs(original_crs)
249
+ return circuit
354
250
 
355
251
 
356
252
  def get_visibilities_from_points(
@@ -385,23 +281,17 @@ def get_visibilities_from_points(
385
281
  -----
386
282
  This function uses `get_visibility_accurate()` in multiprocessing way.
387
283
 
388
- Examples
389
- --------
390
- >>> import geopandas as gpd
391
- >>> from shapely.geometry import Point, Polygon
392
- >>> points = gpd.GeoDataFrame({'geometry': [Point(0, 0), Point(1, 1)]}, crs='epsg:4326')
393
- >>> obstacles = gpd.GeoDataFrame({'geometry': [Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])]}, crs='epsg:4326')
394
- >>> view_distance = 100
395
-
396
- >>> visibilities = get_visibilities_from_points(points, obstacles, view_distance)
397
- >>> visibilities
398
284
  """
285
+ if points.crs != obstacles.crs:
286
+ raise ValueError(f"CRS mismatch, points crs:{points.crs} != obstacles crs:{obstacles.crs}")
287
+ if points.crs.is_geographic:
288
+ logger.warning("Points crs is geographic, it may produce invalid results")
399
289
  # remove points inside polygons
400
290
  joined = gpd.sjoin(points, obstacles, how="left", predicate="intersects")
401
291
  points = joined[joined.index_right.isnull()]
402
292
 
403
293
  # remove unused obstacles
404
- points_view = points.geometry.buffer(view_distance).unary_union
294
+ points_view = points.geometry.buffer(view_distance).union_all()
405
295
  s = obstacles.intersects(points_view)
406
296
  buildings_in_buffer = obstacles.loc[s[s].index].reset_index(drop=True)
407
297
 
@@ -417,6 +307,7 @@ def get_visibilities_from_points(
417
307
  "big amount of points",
418
308
  max_workers=max_workers,
419
309
  )
310
+
420
311
  # could return sectorized visions if sectors_n is set
421
312
  return all_visions
422
313
 
@@ -479,16 +370,18 @@ def calculate_visibility_catchment_area(
479
370
 
480
371
  pandarallel.initialize(progress_bar=True, verbose=0)
481
372
 
482
- assert points.crs == obstacles.crs
483
- crs = obstacles.crs
373
+ local_crs = obstacles.estimate_utm_crs()
374
+ obstacles = obstacles.to_crs(local_crs)
375
+ points = points.to_crs(local_crs)
376
+
484
377
  sectors_n = 12
485
378
  logger.info("Calculating Visibility Catchment Area from each point")
486
379
  all_visions_sectorized = get_visibilities_from_points(points, obstacles, view_distance, sectors_n, max_workers)
487
380
  all_visions_sectorized = gpd.GeoDataFrame(
488
- geometry=[item for sublist in all_visions_sectorized for item in sublist], crs=crs
381
+ geometry=[item for sublist in all_visions_sectorized for item in sublist], crs=local_crs
489
382
  )
490
383
  logger.info("Calculating non-vision part...")
491
- all_visions_unary = all_visions_sectorized.unary_union
384
+ all_visions_unary = all_visions_sectorized.union_all()
492
385
  convex = all_visions_unary.convex_hull
493
386
  dif = convex.difference(all_visions_unary)
494
387
 
@@ -496,7 +389,7 @@ def calculate_visibility_catchment_area(
496
389
 
497
390
  buf_area = (math.pi * view_distance**2) / sectors_n
498
391
  all_visions_sectorized["ratio"] = all_visions_sectorized.area / buf_area
499
- all_visions_sectorized["ratio"] = _min_max_normalization(
392
+ all_visions_sectorized["ratio"] = min_max_normalization(
500
393
  all_visions_sectorized["ratio"].values, new_min=1, new_max=10
501
394
  )
502
395
  groups = all_visions_sectorized.sample(frac=1).groupby(all_visions_sectorized.index // 6000)
@@ -511,7 +404,7 @@ def calculate_visibility_catchment_area(
511
404
  max_workers=max_workers,
512
405
  )
513
406
  logger.info("Calculating all groups intersection...")
514
- all_in = _combine_geometry(gpd.GeoDataFrame(data=pd.concat(groups_result), geometry="geometry", crs=crs))
407
+ all_in = combine_geometry(gpd.GeoDataFrame(data=pd.concat(groups_result), geometry="geometry", crs=local_crs))
515
408
 
516
409
  del groups_result
517
410
 
@@ -523,7 +416,7 @@ def calculate_visibility_catchment_area(
523
416
  all_in = all_in[all_in["factor"] > threshold]
524
417
 
525
418
  all_in["factor_normalized"] = np.round(
526
- _min_max_normalization(np.sqrt(all_in["factor"].values), new_min=1, new_max=5)
419
+ min_max_normalization(np.sqrt(all_in["factor"].values), new_min=1, new_max=5)
527
420
  ).astype(int)
528
421
  logger.info("Calculating normalized groups geometry...")
529
422
  all_in = all_in.groupby("factor_normalized").parallel_apply(unary_union_groups).reset_index()
@@ -550,3 +443,69 @@ def calculate_visibility_catchment_area(
550
443
  all_in = all_in.explode(index_parts=True)
551
444
  logger.info("Done!")
552
445
  return all_in
446
+
447
+
448
+ def _multiprocess_get_vis(args): # pragma: no cover
449
+ point, buildings, view_distance, sectors_n = args
450
+ result = get_visibility_accurate(point, buildings, view_distance)
451
+
452
+ if sectors_n is not None:
453
+ sectors = []
454
+
455
+ cx, cy = point.x, point.y
456
+
457
+ angle_increment = 2 * math.pi / sectors_n
458
+ view_distance = math.sqrt((view_distance**2) * (1 + (math.tan(angle_increment / 2) ** 2)))
459
+ for i in range(sectors_n):
460
+ angle1 = i * angle_increment
461
+ angle2 = (i + 1) * angle_increment
462
+
463
+ x1, y1 = cx + view_distance * math.cos(angle1), cy + view_distance * math.sin(angle1)
464
+ x2, y2 = cx + view_distance * math.cos(angle2), cy + view_distance * math.sin(angle2)
465
+
466
+ sector_triangle = Polygon([point, (x1, y1), (x2, y2)])
467
+ sector = result.intersection(sector_triangle)
468
+
469
+ if not sector.is_empty:
470
+ sectors.append(sector)
471
+ result = sectors
472
+ return result
473
+
474
+
475
+ def _process_group(group): # pragma: no cover
476
+ geom = group
477
+ combined_geometry = combine_geometry(geom)
478
+ combined_geometry.drop(columns=["index", "index_right"], inplace=True)
479
+ combined_geometry["count_n"] = combined_geometry["ratio"].apply(len)
480
+ combined_geometry["new_ratio"] = combined_geometry.apply(
481
+ lambda x: np.power(np.prod(x.ratio), 1 / x.count_n) * x.count_n, axis=1
482
+ )
483
+
484
+ threshold = combined_geometry["new_ratio"].quantile(0.25)
485
+ combined_geometry = combined_geometry[combined_geometry["new_ratio"] > threshold]
486
+
487
+ combined_geometry["new_ratio_normalized"] = min_max_normalization(
488
+ combined_geometry["new_ratio"].values, new_min=1, new_max=10
489
+ )
490
+
491
+ combined_geometry["new_ratio_normalized"] = np.round(combined_geometry["new_ratio_normalized"]).astype(int)
492
+
493
+ result_union = (
494
+ combined_geometry.groupby("new_ratio_normalized")
495
+ .agg({"geometry": lambda x: unary_union(MultiPolygon(list(x)).buffer(0))})
496
+ .reset_index(drop=True)
497
+ )
498
+ result_union.set_geometry("geometry", inplace=True)
499
+ result_union.set_crs(geom.crs, inplace=True)
500
+
501
+ result_union = result_union.explode("geometry", index_parts=False).reset_index(drop=True)
502
+
503
+ representative_points = combined_geometry.copy()
504
+ representative_points["geometry"] = representative_points["geometry"].representative_point()
505
+
506
+ joined = gpd.sjoin(result_union, representative_points, how="inner", predicate="contains").reset_index()
507
+ joined = joined.groupby("index").agg({"geometry": "first", "new_ratio": lambda x: np.mean(list(x))})
508
+
509
+ joined.set_geometry("geometry", inplace=True)
510
+ joined.set_crs(geom.crs, inplace=True)
511
+ return joined
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.1
2
+ Name: ObjectNat
3
+ Version: 1.0.1
4
+ Summary: ObjectNat is an open-source library created for geospatial analysis created by IDU team
5
+ License: BSD-3-Clause
6
+ Author: DDonnyy
7
+ Author-email: 63115678+DDonnyy@users.noreply.github.com
8
+ Requires-Python: >=3.10,<3.13
9
+ Classifier: License :: OSI Approved :: BSD License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Requires-Dist: geopandas (>=1.0.1,<2.0.0)
15
+ Requires-Dist: networkx (>=3.4.2,<4.0.0)
16
+ Requires-Dist: numpy (>=2.1.3,<3.0.0)
17
+ Requires-Dist: pandarallel (>=1.6.5,<2.0.0)
18
+ Requires-Dist: pandas (>=2.2.0,<3.0.0)
19
+ Requires-Dist: scikit-learn (>=1.4.0,<2.0.0)
20
+ Requires-Dist: tqdm (>=4.66.2,<5.0.0)
21
+ Description-Content-Type: text/markdown
22
+
23
+ # ObjectNat
24
+
25
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
26
+ [![PyPI version](https://img.shields.io/pypi/v/objectnat.svg)](https://pypi.org/project/objectnat/)
27
+ [![CI](https://github.com/DDonnyy/ObjectNat/actions/workflows/ci_pipeline.yml/badge.svg)](https://github.com/DDonnyy/ObjecNat/actions/workflows/ci_pipeline.yml)
28
+ [![codecov](https://codecov.io/gh/DDonnyy/ObjectNat/graph/badge.svg?token=K6JFSJ02GU)](https://codecov.io/gh/DDonnyy/ObjectNat)
29
+ [![License](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](https://opensource.org/licenses/MIT)
30
+
31
+ - [РИДМИ (Russian)](README_ru.md)
32
+ <p align="center">
33
+ <img src="https://github.com/user-attachments/assets/ed0f226e-1728-4659-9e21-b4d499e703cd" alt="logo" width="400">
34
+ </p>
35
+
36
+ #### **ObjectNat** is an open-source library created for geospatial analysis created by **IDU team**
37
+
38
+ ## Features and how to use
39
+
40
+ 1. **[Isochrones and Transport Accessibility](./examples/isochrone_generator.ipynb)** — Isochrones represent areas reachable from a starting point within a given time limit along a transport network. This function enables analysis of transport accessibility using pedestrian, automobile, public transport graphs, or their combination.
41
+
42
+ The library offers multiple isochrone generation methods:
43
+ - **Baseline isochrones**: show a single area reachable within a specified time.
44
+ - **Stepped isochrones**: show accessibility ranges divided into time intervals (e.g., 5, 10, 15 minutes).
45
+
46
+ <p align="center">
47
+ <img src="https://github.com/user-attachments/assets/b1787430-63e1-4907-9198-a6171d546599" alt="isochrone_ways_15_min" width="300">
48
+ <img src="https://github.com/user-attachments/assets/64fce6bf-6509-490c-928c-dbd8daf9f570" alt="isochrone_radius_15_min" width="300">
49
+ </p>
50
+ <p align="center">
51
+ <img src="https://github.com/user-attachments/assets/ac9f8840-a867-4eb5-aec8-91a411d4e545" alt="stepped_isochrone_stepped_ways_15_min" width="300">
52
+ <img src="https://github.com/user-attachments/assets/b5429aa1-4625-44d1-982f-8bd4264148fb" alt="stepped_isochrone_stepped_radius_15_min" width="300">
53
+ <img src="https://github.com/user-attachments/assets/042c7362-70e1-45df-b2e1-02fc76bf638c" alt="stepped_isochrone_stepped_separate_15_min" width="300">
54
+ </p>
55
+
56
+
57
+ 2. **[Coverage Zones](./examples/graph_coverage.ipynb)** — Function for generating **coverage zones** from a set of source points using a transport network. It calculates the area each point can reach based on **travel time** or **distance**, then builds polygons via **Voronoi diagrams** and clips them to a custom boundary if provided.
58
+
59
+ <p align="center">
60
+ <img src="https://github.com/user-attachments/assets/fa8057d7-77aa-48a2-aa10-ea3e292a918d" alt="coverage_zones_time_10min" width="350">
61
+ <img src="https://github.com/user-attachments/assets/44362dde-c3b0-4321-9a0a-aa547f0f2e04" alt="coverage_zones_distance_600m" width="350">
62
+ </p>
63
+
64
+ 3. **[Service Provision Analysis](./examples/calculate_provision.ipynb)** — Function for evaluating the provision of residential buildings and their population with services (e.g., schools, clinics)
65
+ that have limited **capacity** and a defined **accessibility threshold** (in minutes or distance). The function models **demand-supply balance**, estimating how well services meet the needs of nearby buildings within the allowed time.
66
+
67
+ The library also supports:
68
+ - **Recalculation** of existing provision results using a new time threshold.
69
+ - **Clipping** of provision results to a custom analysis area (e.g., administrative boundaries).
70
+
71
+ <p align="center">
72
+ <img src="https://github.com/user-attachments/assets/ff1ed08d-9a35-4035-9e1f-9a7fdae5b0e0" alt="service_provision_initial" width="300">
73
+ <img src="https://github.com/user-attachments/assets/a0c0a6b0-f83f-4982-bfb3-4a476b2153ea" alt="service_provision_recalculated" width="300">
74
+ <img src="https://github.com/user-attachments/assets/f57dc1c6-21a0-458d-85f4-fe1b17c77695" alt="service_provision_clipped" width="300">
75
+ </p>
76
+
77
+ 4. **[Visibility Analysis](./examples/visibility_analysis.ipynb)** — Function for estimating visibility from a given point or multiple points to nearby buildings within a certain distance.
78
+ This can be used to assess visual accessibility in urban environments.
79
+ The library also includes a **catchment area calculator** for large-scale visibility analysis based on a dense grid of observer points (recommended: ~1000 points spaced 10–20 meters apart).
80
+ Points can be generated using a road network and distributed along edges.
81
+
82
+ The module includes:
83
+ - A **fast approximate method** for large datasets.
84
+ - A **accurate method** for detailed local analysis.
85
+
86
+ <p align="center">
87
+ <img src="https://github.com/user-attachments/assets/aa139d29-07d4-4560-b835-9646c8802fe1" alt="visibility_comparison_methods" height="250">
88
+ <img src="https://github.com/user-attachments/assets/b5b0d4b3-a02f-4ade-8772-475703cd6435" alt="visibility-catchment-area" height="250">
89
+ </p>
90
+
91
+ 5. **[Noise Simulation](./examples/noise_simulation.ipynb)** — Simulates noise propagation from a set of source points, taking into account **obstacles**, **vegetation**, and **environmental factors**.
92
+
93
+ 🔗 **[See detailed explanation in the Wiki](https://github.com/DDonnyy/ObjectNat/wiki/Noise-simulation)**
94
+
95
+ <p align="center">
96
+ <img src="https://github.com/user-attachments/assets/b3a41962-6220-49c4-90d4-2e756f9706cf" alt="noise_simulation_test_result" width="400">
97
+ </p>
98
+
99
+
100
+ 6. **[Point Clusterization](./examples/point_clusterization.ipynb)** — Function to generate **cluster polygons** from a set of input points based on:
101
+ - Minimum **distance** between points.
102
+ - Minimum **number of points** per cluster.
103
+
104
+ Additionally, the function can calculate the **relative ratio** of different service types within each cluster, enabling spatial analysis of service composition.
105
+
106
+ <p align="center">
107
+ <img src="https://github.com/user-attachments/assets/f86aac61-497a-4330-b4cf-68f4fc47fd34" alt="building_clusters" width="400">
108
+ </p>
109
+
110
+ ## City graphs
111
+
112
+ To ensure optimal performance of ObjectNat's geospatial analysis functions, it's recommended to utilize urban graphs sourced from the [IduEdu](https://github.com/DDonnyy/IduEdu) library.
113
+ **IduEdu** is an open-source Python library designed for the creation and manipulation of complex city networks derived from OpenStreetMap data.
114
+
115
+ **IduEdu** can be installed with ``pip``:
116
+ ```
117
+ pip install IduEdu
118
+ ```
119
+ ## Installation
120
+
121
+ **ObjectNat** can be installed with ``pip``:
122
+
123
+ ```
124
+ pip install ObjectNat
125
+ ```
126
+ ### Configuration changes
127
+
128
+ ```python
129
+ from objectnat import config
130
+
131
+ config.change_logger_lvl('INFO') # To mute all debug msgs
132
+ config.set_enable_tqdm(False) # To mute all tqdm's progress bars
133
+ ```
134
+ ## Contacts
135
+
136
+ - [NCCR](https://actcognitive.org/) - National Center for Cognitive Research
137
+ - [IDU](https://idu.itmo.ru/) - Institute of Design and Urban Studies
138
+ - [Natalya Chichkova](https://t.me/nancy_nat) - project manager
139
+ - [Danila Oleynikov (Donny)](https://t.me/ddonny_dd) - lead software engineer
140
+
141
+ ## Publications
142
+