ObjectNat 1.3.3__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.
Files changed (33) hide show
  1. objectnat/__init__.py +9 -0
  2. objectnat/_api.py +14 -0
  3. objectnat/_config.py +43 -0
  4. objectnat/_version.py +1 -0
  5. objectnat/methods/__init__.py +0 -0
  6. objectnat/methods/coverage_zones/__init__.py +3 -0
  7. objectnat/methods/coverage_zones/graph_coverage.py +105 -0
  8. objectnat/methods/coverage_zones/radius_voronoi_coverage.py +39 -0
  9. objectnat/methods/coverage_zones/stepped_coverage.py +136 -0
  10. objectnat/methods/isochrones/__init__.py +1 -0
  11. objectnat/methods/isochrones/isochrone_utils.py +167 -0
  12. objectnat/methods/isochrones/isochrones.py +282 -0
  13. objectnat/methods/noise/__init__.py +3 -0
  14. objectnat/methods/noise/noise_init_data.py +10 -0
  15. objectnat/methods/noise/noise_reduce.py +155 -0
  16. objectnat/methods/noise/noise_simulation.py +453 -0
  17. objectnat/methods/noise/noise_simulation_simplified.py +222 -0
  18. objectnat/methods/point_clustering/__init__.py +1 -0
  19. objectnat/methods/point_clustering/cluster_points_in_polygons.py +115 -0
  20. objectnat/methods/provision/__init__.py +1 -0
  21. objectnat/methods/provision/provision.py +213 -0
  22. objectnat/methods/provision/provision_exceptions.py +59 -0
  23. objectnat/methods/provision/provision_model.py +323 -0
  24. objectnat/methods/utils/__init__.py +1 -0
  25. objectnat/methods/utils/geom_utils.py +173 -0
  26. objectnat/methods/utils/graph_utils.py +306 -0
  27. objectnat/methods/utils/math_utils.py +32 -0
  28. objectnat/methods/visibility/__init__.py +6 -0
  29. objectnat/methods/visibility/visibility_analysis.py +485 -0
  30. objectnat-1.3.3.dist-info/METADATA +202 -0
  31. objectnat-1.3.3.dist-info/RECORD +33 -0
  32. objectnat-1.3.3.dist-info/WHEEL +4 -0
  33. objectnat-1.3.3.dist-info/licenses/LICENSE.txt +28 -0
@@ -0,0 +1,485 @@
1
+ import math
2
+ from multiprocessing import cpu_count
3
+
4
+ import geopandas as gpd
5
+ import numpy as np
6
+ import pandas as pd
7
+ from shapely import LineString, MultiPolygon, Point, Polygon
8
+ from shapely.ops import unary_union
9
+ from tqdm.contrib.concurrent import process_map
10
+
11
+ from objectnat import config
12
+ from objectnat.methods.utils.geom_utils import (
13
+ combine_geometry,
14
+ explode_linestring,
15
+ get_point_from_a_thorough_b,
16
+ point_side_of_line,
17
+ polygons_to_multilinestring,
18
+ )
19
+ from objectnat.methods.utils.math_utils import min_max_normalization
20
+
21
+ logger = config.logger
22
+
23
+
24
+ def _ensure_crs(point_from, obstacles):
25
+ if isinstance(point_from, gpd.GeoDataFrame):
26
+ if point_from.empty:
27
+ raise ValueError("GeoDataFrame 'point_from' is empty.")
28
+ return_gdf = True
29
+ original_crs = point_from.crs
30
+ local_crs = point_from.estimate_utm_crs()
31
+ point_from = point_from.to_crs(local_crs)
32
+ if len(point_from) > 1:
33
+ logger.warning(
34
+ "This method processes only a single point. "
35
+ f"The GeoDataFrame contains {len(point_from)} points – "
36
+ "only the first geometry will be used for visibility calculation."
37
+ )
38
+ point_geom = point_from.iloc[0].geometry
39
+ if len(obstacles) > 0:
40
+ obstacles = obstacles.to_crs(local_crs)
41
+
42
+ else:
43
+ return_gdf = False
44
+
45
+ if len(obstacles) > 0:
46
+ original_crs = obstacles.crs
47
+ local_crs = obstacles.estimate_utm_crs()
48
+ obstacles = obstacles.to_crs(local_crs)
49
+ point_geom = gpd.GeoDataFrame(geometry=[point_from], crs=original_crs).to_crs(local_crs).iloc[0].geometry
50
+ else:
51
+ original_crs = 4326
52
+ logger.warning("No information about CRS was found, accepting everything in 4326")
53
+ point_gdf = gpd.GeoDataFrame(geometry=[point_from], crs=original_crs)
54
+ local_crs = point_gdf.estimate_utm_crs()
55
+
56
+ point_gdf = point_gdf.to_crs(local_crs)
57
+ point_geom = point_gdf.geometry.iloc[0]
58
+
59
+ return point_geom, obstacles, original_crs, local_crs, return_gdf
60
+
61
+
62
+ def get_visibility_accurate(
63
+ point_from: Point | gpd.GeoDataFrame, obstacles: gpd.GeoDataFrame, view_distance, return_max_view_dist=False
64
+ ) -> Polygon | gpd.GeoDataFrame | tuple[Polygon | gpd.GeoDataFrame, float]:
65
+ """
66
+ Function to get accurate visibility from a given point to buildings within a given distance.
67
+
68
+ Args:
69
+ point_from (Point | gpd.GeoDataFrame):
70
+ The point or GeoDataFrame with 1 point from which the line of sight is drawn.
71
+ If Point is provided it should be in the same crs as obstacles.
72
+ obstacles (gpd.GeoDataFrame):
73
+ A GeoDataFrame containing the geometry of the obstacles.
74
+ view_distance (float):
75
+ The distance of view from the point.
76
+ return_max_view_dist (bool):
77
+ If True, the max view distance is returned with view polygon in tuple.
78
+
79
+ Returns:
80
+ (Polygon | gpd.GeoDataFrame | tuple[Polygon | gpd.GeoDataFrame, float]):
81
+ A polygon representing the area of visibility from the given point or polygon with max view distance.
82
+ if point_from was a GeoDataFrame, return GeoDataFrame with one feature, else Polygon.
83
+
84
+ Notes:
85
+ If a quick result is important, consider using the `get_visibility()` function instead.
86
+ However, please note that `get_visibility()` may provide less accurate results.
87
+ """
88
+
89
+ def find_furthest_point(point_from, view_polygon):
90
+ try:
91
+ res = round(max(Point(coords).distance(point_from) for coords in view_polygon.exterior.coords), 1)
92
+ except Exception as e:
93
+ print(view_polygon)
94
+ raise e
95
+ return res
96
+
97
+ point_geom, obstacles, original_crs, local_crs, return_gdf = _ensure_crs(point_from, obstacles)
98
+
99
+ if len(obstacles) > 0 and obstacles.contains(point_geom).any():
100
+ return gpd.GeoDataFrame() if return_gdf else Point()
101
+
102
+ point_buffer = point_geom.buffer(view_distance, resolution=32)
103
+
104
+ if len(obstacles) == 0:
105
+ full_vision_gdf = gpd.GeoDataFrame(geometry=[point_buffer], crs=local_crs).to_crs(original_crs)
106
+ return full_vision_gdf if return_gdf else full_vision_gdf.iloc[0].geometry
107
+
108
+ obstacles.reset_index(inplace=True, drop=True)
109
+
110
+ allowed_geom_types = ["MultiPolygon", "Polygon", "LineString", "MultiLineString"]
111
+
112
+ obstacles = obstacles[obstacles.geom_type.isin(allowed_geom_types)]
113
+ s = obstacles.intersects(point_buffer)
114
+ obstacles_in_buffer = obstacles.loc[s[s].index].geometry
115
+
116
+ buildings_lines_in_buffer = gpd.GeoSeries(
117
+ pd.Series(
118
+ obstacles_in_buffer.apply(polygons_to_multilinestring).explode(index_parts=False).apply(explode_linestring)
119
+ ).explode()
120
+ )
121
+
122
+ buildings_lines_in_buffer = buildings_lines_in_buffer.loc[buildings_lines_in_buffer.intersects(point_buffer)]
123
+
124
+ buildings_in_buffer_points = gpd.GeoSeries(
125
+ [Point(line.coords[0]) for line in buildings_lines_in_buffer.geometry]
126
+ + [Point(line.coords[-1]) for line in buildings_lines_in_buffer.geometry]
127
+ )
128
+
129
+ max_dist = max(view_distance, buildings_in_buffer_points.distance(point_geom).max())
130
+ polygons = []
131
+ buildings_lines_in_buffer = gpd.GeoDataFrame(geometry=buildings_lines_in_buffer, crs=obstacles.crs).reset_index()
132
+ logger.debug("Calculation vis polygon")
133
+ while not buildings_lines_in_buffer.empty:
134
+ gdf_sindex = buildings_lines_in_buffer.sindex
135
+ # TODO check if 2 walls are nearest and use the widest angle between points
136
+ nearest_wall_sind = gdf_sindex.nearest(point_geom, return_all=False, max_distance=max_dist)
137
+ nearest_wall = buildings_lines_in_buffer.loc[nearest_wall_sind[1]].iloc[0]
138
+ wall_points = [Point(coords) for coords in nearest_wall.geometry.coords]
139
+
140
+ # Calculate angles and sort by angle
141
+ points_with_angle = sorted(
142
+ [(pt, math.atan2(pt.y - point_geom.y, pt.x - point_geom.x)) for pt in wall_points], key=lambda x: x[1]
143
+ )
144
+ delta_angle = 2 * math.pi + points_with_angle[0][1] - points_with_angle[-1][1]
145
+ if round(delta_angle, 10) == round(math.pi, 10):
146
+ wall_b_centroid = obstacles_in_buffer.loc[nearest_wall["index"]].centroid
147
+ p1 = get_point_from_a_thorough_b(point_geom, points_with_angle[0][0], max_dist)
148
+ p2 = get_point_from_a_thorough_b(point_geom, points_with_angle[1][0], max_dist)
149
+ polygon = LineString([p1, p2])
150
+ polygon = polygon.buffer(
151
+ distance=max_dist * point_side_of_line(polygon, wall_b_centroid), single_sided=True
152
+ )
153
+ else:
154
+ if delta_angle > math.pi:
155
+ delta_angle = 2 * math.pi - delta_angle
156
+ a = math.sqrt((max_dist**2) * (1 + (math.tan(delta_angle / 2) ** 2)))
157
+ p1 = get_point_from_a_thorough_b(point_geom, points_with_angle[0][0], a)
158
+ p2 = get_point_from_a_thorough_b(point_geom, points_with_angle[-1][0], a)
159
+ polygon = Polygon([points_with_angle[0][0], p1, p2, points_with_angle[1][0]])
160
+
161
+ polygons.append(polygon)
162
+ buildings_lines_in_buffer.drop(nearest_wall_sind[1], inplace=True)
163
+
164
+ if not polygon.is_valid or polygon.area < 1:
165
+ buildings_lines_in_buffer.reset_index(drop=True, inplace=True)
166
+ continue
167
+
168
+ lines_to_kick = buildings_lines_in_buffer.within(polygon)
169
+ buildings_lines_in_buffer = buildings_lines_in_buffer.loc[~lines_to_kick]
170
+ buildings_lines_in_buffer.reset_index(drop=True, inplace=True)
171
+ logger.debug("Done calculating!")
172
+ res = point_buffer.difference(unary_union(polygons + obstacles_in_buffer.to_list()))
173
+
174
+ if isinstance(res, MultiPolygon):
175
+ res = list(res.geoms)
176
+ for polygon in res:
177
+ if polygon.intersects(point_geom):
178
+ res = polygon
179
+ break
180
+
181
+ result_gdf = gpd.GeoDataFrame(geometry=[res], crs=local_crs).to_crs(original_crs)
182
+ if return_gdf:
183
+ return result_gdf
184
+ res = result_gdf.to_crs(original_crs).iloc[0].geometry
185
+ if return_max_view_dist:
186
+ return res, find_furthest_point(point_geom, res)
187
+ return res
188
+
189
+
190
+ def get_visibility(
191
+ point_from: Point | gpd.GeoDataFrame, obstacles: gpd.GeoDataFrame, view_distance: float, resolution: int = 32
192
+ ) -> Polygon | gpd.GeoDataFrame:
193
+ """
194
+ Function to get a quick estimate of visibility from a given point to buildings within a given distance.
195
+
196
+ Args:
197
+ point_from (Point | gpd.GeoDataFrame):
198
+ The point or GeoDataFrame with 1 point from which the line of sight is drawn.
199
+ If Point is provided it should be in the same crs as obstacles.
200
+ obstacles (gpd.GeoDataFrame):
201
+ A GeoDataFrame containing the geometry of the buildings.
202
+ view_distance (float):
203
+ The distance of view from the point.
204
+ resolution (int) :
205
+ Buffer resolution for more accuracy (may give result slower)
206
+
207
+ Returns:
208
+ (Polygon | gpd.GeoDataFrame):
209
+ A polygon representing the area of visibility from the given point.
210
+ if point_from was a GeoDataFrame, return GeoDataFrame with one feature, else Polygon.
211
+
212
+ Notes:
213
+ This function provides a quicker but less accurate result compared to `get_visibility_accurate()`.
214
+ If accuracy is important, consider using `get_visibility_accurate()` instead.
215
+ """
216
+
217
+ obstacles = obstacles.copy()
218
+
219
+ point_geom, obstacles, original_crs, local_crs, return_gdf = _ensure_crs(point_from, obstacles)
220
+ print(return_gdf)
221
+ if len(obstacles) > 0 and obstacles.contains(point_geom).any():
222
+ return Polygon()
223
+
224
+ point_buffer = point_geom.buffer(view_distance, resolution=resolution)
225
+
226
+ if len(obstacles) == 0:
227
+ full_vision_gdf = gpd.GeoDataFrame(geometry=[point_buffer], crs=local_crs).to_crs(original_crs)
228
+ return full_vision_gdf if return_gdf else full_vision_gdf.iloc[0].geometry
229
+
230
+ s = obstacles.intersects(point_buffer)
231
+ buildings_in_buffer = obstacles.loc[s[s].index].reset_index(drop=True)
232
+ buffer_exterior_ = list(point_buffer.exterior.coords)
233
+ line_geometry = [LineString([point_geom, ext]) for ext in buffer_exterior_]
234
+ buffer_lines_gdf = gpd.GeoDataFrame(geometry=line_geometry)
235
+ united_buildings = buildings_in_buffer.union_all()
236
+ if united_buildings:
237
+ splited_lines = buffer_lines_gdf["geometry"].apply(lambda x: x.difference(united_buildings))
238
+ else:
239
+ splited_lines = buffer_lines_gdf["geometry"]
240
+
241
+ splited_lines_gdf = gpd.GeoDataFrame(geometry=splited_lines).explode(index_parts=True)
242
+ splited_lines_list = []
243
+
244
+ for _, v in splited_lines_gdf.groupby(level=0):
245
+ splited_lines_list.append(v.iloc[0]["geometry"].coords[-1])
246
+ circuit = Polygon(splited_lines_list)
247
+ if united_buildings:
248
+ circuit = circuit.difference(united_buildings)
249
+
250
+ result_gdf = gpd.GeoDataFrame(geometry=[circuit], crs=local_crs).to_crs(original_crs)
251
+ return result_gdf if return_gdf else result_gdf.iloc[0].geometry
252
+
253
+
254
+ def get_visibilities_from_points(
255
+ points: gpd.GeoDataFrame,
256
+ obstacles: gpd.GeoDataFrame,
257
+ view_distance: int,
258
+ sectors_n=None,
259
+ max_workers: int = cpu_count(),
260
+ ) -> list[Polygon]:
261
+ """
262
+ Calculate visibility polygons from a set of points considering obstacles within a specified view distance.
263
+
264
+ Args:
265
+ points (gpd.GeoDataFrame):
266
+ GeoDataFrame containing the points from which visibility is calculated.
267
+ obstacles (gpd.GeoDataFrame):
268
+ GeoDataFrame containing the obstacles that block visibility.
269
+ view_distance (int):
270
+ The maximum distance from each point within which visibility is calculated.
271
+ sectors_n (int, optional):
272
+ Number of sectors to divide the view into for more detailed visibility calculations. Defaults to None.
273
+ max_workers (int, optional):
274
+ Maximum workers in multiproccesing, multipocessing.cpu_count() by default.
275
+
276
+ Returns:
277
+ (list[Polygon]):
278
+ A list of visibility polygons for each input point.
279
+
280
+ Notes:
281
+ This function uses `get_visibility_accurate()` in multiprocessing way.
282
+
283
+ """
284
+ if points.crs != obstacles.crs:
285
+ raise ValueError(f"CRS mismatch, points crs:{points.crs} != obstacles crs:{obstacles.crs}")
286
+ if points.crs.is_geographic:
287
+ logger.warning("Points crs is geographic, it may produce invalid results")
288
+ # remove points inside polygons
289
+ joined = gpd.sjoin(points, obstacles, how="left", predicate="intersects")
290
+ points = joined[joined.index_right.isnull()]
291
+
292
+ # remove unused obstacles
293
+ points_view = points.geometry.buffer(view_distance).union_all()
294
+ s = obstacles.intersects(points_view)
295
+ buildings_in_buffer = obstacles.loc[s[s].index].reset_index(drop=True)
296
+
297
+ buildings_in_buffer.geometry = buildings_in_buffer.geometry.apply(
298
+ lambda geom: MultiPolygon([geom]) if isinstance(geom, Polygon) else geom
299
+ )
300
+ args = [(point, buildings_in_buffer, view_distance, sectors_n) for point in points.geometry]
301
+ all_visions = process_map(
302
+ _multiprocess_get_vis,
303
+ args,
304
+ chunksize=5,
305
+ desc="Calculating Visibility Catchment Area from each Point, it might take a while for a "
306
+ "big amount of points",
307
+ max_workers=max_workers,
308
+ )
309
+
310
+ # could return sectorized visions if sectors_n is set
311
+ return all_visions
312
+
313
+
314
+ def calculate_visibility_catchment_area(
315
+ points: gpd.GeoDataFrame, obstacles: gpd.GeoDataFrame, view_distance: int | float, max_workers: int = cpu_count()
316
+ ) -> gpd.GeoDataFrame: # pragma: no cover
317
+ """
318
+ Calculate visibility catchment areas for a large urban area based on given points and obstacles.
319
+ This function is designed to work with at least 1000 points spaced 10-20 meters apart for optimal results.
320
+ Points can be generated using a road graph.
321
+
322
+ Args:
323
+ points (gpd.GeoDataFrame): GeoDataFrame containing the points from which visibility is calculated.
324
+ obstacles (gpd.GeoDataFrame): GeoDataFrame containing the obstacles that block visibility.
325
+ view_distance (int | float): The maximum distance from each point within which visibility is calculated.
326
+ max_workers (int): Maximum workers in multiproccesing, multipocessing.cpu_count() by default.
327
+
328
+ Returns:
329
+ (gpd.GeoDataFrame): GeoDataFrame containing the calculated visibility catchment areas.
330
+ """
331
+
332
+ def filter_geoms(x):
333
+ if x.geom_type == "GeometryCollection":
334
+ return MultiPolygon([y for y in x.geoms if y.geom_type in ["Polygon", "MultiPolygon"]])
335
+ return x
336
+
337
+ def calc_group_factor(x):
338
+ return np.mean(x.new_ratio) * x.count_n
339
+
340
+ def unary_union_groups(x):
341
+ return unary_union(MultiPolygon(list(x["geometry"])).buffer(0))
342
+
343
+ raise NotImplementedError("This method is temporarily unsupported.")
344
+
345
+ local_crs = obstacles.estimate_utm_crs()
346
+ obstacles = obstacles.to_crs(local_crs)
347
+ points = points.to_crs(local_crs)
348
+
349
+ sectors_n = 12
350
+ logger.info("Calculating Visibility Catchment Area from each point")
351
+ all_visions_sectorized = get_visibilities_from_points(points, obstacles, view_distance, sectors_n, max_workers)
352
+ all_visions_sectorized = gpd.GeoDataFrame(
353
+ geometry=[item for sublist in all_visions_sectorized for item in sublist], crs=local_crs
354
+ )
355
+ logger.info("Calculating non-vision part...")
356
+ all_visions_unary = all_visions_sectorized.union_all()
357
+ convex = all_visions_unary.convex_hull
358
+ dif = convex.difference(all_visions_unary)
359
+
360
+ del convex, all_visions_unary
361
+
362
+ buf_area = (math.pi * view_distance**2) / sectors_n
363
+ all_visions_sectorized["ratio"] = all_visions_sectorized.area / buf_area
364
+ all_visions_sectorized["ratio"] = min_max_normalization(
365
+ all_visions_sectorized["ratio"].values, new_min=1, new_max=10
366
+ )
367
+ groups = all_visions_sectorized.sample(frac=1).groupby(all_visions_sectorized.index // 6000)
368
+ groups = [group for _, group in groups]
369
+
370
+ del all_visions_sectorized
371
+
372
+ groups_result = process_map(
373
+ _process_group,
374
+ groups,
375
+ desc="Counting intersections in each group...",
376
+ max_workers=max_workers,
377
+ )
378
+ logger.info("Calculating all groups intersection...")
379
+ all_in = combine_geometry(gpd.GeoDataFrame(data=pd.concat(groups_result), geometry="geometry", crs=local_crs))
380
+
381
+ del groups_result
382
+
383
+ all_in["count_n"] = all_in["index_right"].apply(len)
384
+
385
+ logger.info("Calculating intersection's parameters")
386
+ # all_in["factor"] = all_in.parallel_apply(calc_group_factor, axis=1) # TODO replace pandarallel methods
387
+ threshold = all_in["factor"].quantile(0.3)
388
+ all_in = all_in[all_in["factor"] > threshold]
389
+
390
+ all_in["factor_normalized"] = np.round(
391
+ min_max_normalization(np.sqrt(all_in["factor"].values), new_min=1, new_max=5)
392
+ ).astype(int)
393
+ logger.info("Calculating normalized groups geometry...")
394
+ all_in = (
395
+ all_in.groupby("factor_normalized").parallel_apply(unary_union_groups).reset_index()
396
+ ) # TODO replace pandarallel methods
397
+ all_in = gpd.GeoDataFrame(data=all_in.rename(columns={0: "geometry"}), geometry="geometry", crs=32636)
398
+
399
+ all_in = all_in.explode(index_parts=True).reset_index(drop=True)
400
+ all_in["area"] = all_in.area
401
+ threshold = all_in["area"].quantile(0.9)
402
+ all_in = all_in[all_in["area"] > threshold]
403
+ all_in = all_in.groupby("factor_normalized").apply(unary_union_groups).reset_index()
404
+ all_in = gpd.GeoDataFrame(data=all_in.rename(columns={0: "geometry"}), geometry="geometry", crs=32636)
405
+
406
+ all_in.geometry = all_in.geometry.buffer(20).buffer(-20).difference(dif)
407
+
408
+ all_in.sort_values(by="factor_normalized", ascending=False, inplace=True)
409
+ all_in.reset_index(drop=True, inplace=True)
410
+ logger.info("Smoothing normalized groups geometry...")
411
+ for ind, row in all_in.iloc[:-1].iterrows():
412
+ for ind2 in range(ind + 1, len(all_in)):
413
+ current_geometry = all_in.at[ind2, "geometry"]
414
+ all_in.at[ind2, "geometry"] = current_geometry.difference(row.geometry)
415
+ all_in["geometry"] = all_in["geometry"].apply(filter_geoms)
416
+
417
+ all_in = all_in.explode(index_parts=True)
418
+ logger.info("Done!")
419
+ return all_in
420
+
421
+
422
+ def _multiprocess_get_vis(args): # pragma: no cover
423
+ point, buildings, view_distance, sectors_n = args
424
+ result = get_visibility_accurate(point, buildings, view_distance)
425
+
426
+ if sectors_n is not None:
427
+ sectors = []
428
+
429
+ cx, cy = point.x, point.y
430
+
431
+ angle_increment = 2 * math.pi / sectors_n
432
+ view_distance = math.sqrt((view_distance**2) * (1 + (math.tan(angle_increment / 2) ** 2)))
433
+ for i in range(sectors_n):
434
+ angle1 = i * angle_increment
435
+ angle2 = (i + 1) * angle_increment
436
+
437
+ x1, y1 = cx + view_distance * math.cos(angle1), cy + view_distance * math.sin(angle1)
438
+ x2, y2 = cx + view_distance * math.cos(angle2), cy + view_distance * math.sin(angle2)
439
+
440
+ sector_triangle = Polygon([point, (x1, y1), (x2, y2)])
441
+ sector = result.intersection(sector_triangle)
442
+
443
+ if not sector.is_empty:
444
+ sectors.append(sector)
445
+ result = sectors
446
+ return result
447
+
448
+
449
+ def _process_group(group): # pragma: no cover
450
+ geom = group
451
+ combined_geometry = combine_geometry(geom)
452
+ combined_geometry.drop(columns=["index", "index_right"], inplace=True)
453
+ combined_geometry["count_n"] = combined_geometry["ratio"].apply(len)
454
+ combined_geometry["new_ratio"] = combined_geometry.apply(
455
+ lambda x: np.power(np.prod(x.ratio), 1 / x.count_n) * x.count_n, axis=1
456
+ )
457
+
458
+ threshold = combined_geometry["new_ratio"].quantile(0.25)
459
+ combined_geometry = combined_geometry[combined_geometry["new_ratio"] > threshold]
460
+
461
+ combined_geometry["new_ratio_normalized"] = min_max_normalization(
462
+ combined_geometry["new_ratio"].values, new_min=1, new_max=10
463
+ )
464
+
465
+ combined_geometry["new_ratio_normalized"] = np.round(combined_geometry["new_ratio_normalized"]).astype(int)
466
+
467
+ result_union = (
468
+ combined_geometry.groupby("new_ratio_normalized")
469
+ .agg({"geometry": lambda x: unary_union(MultiPolygon(list(x)).buffer(0))})
470
+ .reset_index(drop=True)
471
+ )
472
+ result_union.set_geometry("geometry", inplace=True)
473
+ result_union.set_crs(geom.crs, inplace=True)
474
+
475
+ result_union = result_union.explode("geometry", index_parts=False).reset_index(drop=True)
476
+
477
+ representative_points = combined_geometry.copy()
478
+ representative_points["geometry"] = representative_points["geometry"].representative_point()
479
+
480
+ joined = gpd.sjoin(result_union, representative_points, how="inner", predicate="contains").reset_index()
481
+ joined = joined.groupby("index").agg({"geometry": "first", "new_ratio": lambda x: np.mean(list(x))})
482
+
483
+ joined.set_geometry("geometry", inplace=True)
484
+ joined.set_crs(geom.crs, inplace=True)
485
+ return joined
@@ -0,0 +1,202 @@
1
+ Metadata-Version: 2.4
2
+ Name: ObjectNat
3
+ Version: 1.3.3
4
+ Summary: ObjectNat is an open-source library created for geospatial analysis created by IDU team
5
+ License: BSD-3-Clause
6
+ License-File: LICENSE.txt
7
+ Author: DDonnyy
8
+ Author-email: 63115678+DDonnyy@users.noreply.github.com
9
+ Requires-Python: >=3.11,<3.13
10
+ Classifier: License :: OSI Approved :: BSD License
11
+ Classifier: Programming Language :: Python :: 3
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: loguru (>=0.7.3,<0.8.0)
16
+ Requires-Dist: networkx (>=3.4.2,<4.0.0)
17
+ Requires-Dist: numpy (>=2.1.3,<3.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/x-rst
22
+
23
+ ObjectNat
24
+ =========
25
+
26
+ Object-oriented Network Analysis Tools
27
+ --------------------------------------
28
+
29
+ .. |badge-black| image:: https://img.shields.io/badge/code%20style-black-000000.svg
30
+ :target: https://github.com/psf/black
31
+ :alt: Code style: black
32
+
33
+ .. |badge-pypi| image:: https://img.shields.io/pypi/v/objectnat.svg
34
+ :target: https://pypi.org/project/objectnat/
35
+ :alt: PyPI version
36
+
37
+ .. |badge-ci| image:: https://github.com/IDUclub/ObjectNat/actions/workflows/ci_pipeline.yml/badge.svg
38
+ :target: https://github.com/IDUclub/ObjectNat/actions/workflows/ci_pipeline.yml
39
+ :alt: CI
40
+
41
+ .. |badge-codecov| image:: https://codecov.io/gh/DDonnyy/ObjectNat/graph/badge.svg?token=K6JFSJ02GU
42
+ :target: https://codecov.io/gh/DDonnyy/ObjectNat
43
+ :alt: Coverage
44
+
45
+ .. |badge-license| image:: https://img.shields.io/badge/license-BSD--3--Clause-blue.svg
46
+ :target: https://opensource.org/licenses/BSD-3-Clause
47
+ :alt: License
48
+
49
+ .. |badge-docs| image:: https://img.shields.io/badge/docs-latest-4aa0d5?logo=readthedocs
50
+ :target: https://iduclub.github.io/ObjectNat/
51
+ :alt: Docs
52
+
53
+ |badge-black| |badge-pypi| |badge-ci| |badge-codecov| |badge-license| |badge-docs|
54
+
55
+ `РИДМИ (Russian) <https://github.com/IDUclub/ObjectNat/blob/master/README_RU.rst>`__
56
+
57
+ .. image:: https://raw.githubusercontent.com/IDUclub/ObjectNat/master/docs/_static/ONlogo.svg
58
+ :align: center
59
+ :width: 400
60
+ :alt: ObjectNat logo
61
+
62
+
63
+ **ObjectNat** is an open-source library developed by the **IDU** team
64
+ for spatial and network analysis in urban studies.
65
+ The library provides tools for analyzing **accessibility**, **visibility**,
66
+ **noise propagation**, and **service provision**.
67
+ ----
68
+
69
+ Key Features
70
+ ------------
71
+
72
+ Each feature includes a **Jupyter Notebook example** and **full documentation**.
73
+
74
+ 1. **Isochrones and Transport Accessibility**
75
+
76
+ Isochrones represent areas reachable from an origin point within a specified time along a transport network.
77
+ This feature allows the analysis of transport accessibility using pedestrian, road,
78
+ public transport, or multimodal graphs.
79
+
80
+ The library supports several methods for building isochrones:
81
+
82
+ - **Basic isochrones**: display a single zone reachable within a specified time.
83
+ - **Step isochrones**: divide the accessibility area into time intervals (e.g., 3, 5, 10 minutes).
84
+
85
+
86
+ 📘 `Example <https://iduclub.github.io/ObjectNat/methods/examples/isochrones.html>`__
87
+ 🔗 `Documentation <https://iduclub.github.io/ObjectNat/methods/isochrones.html>`__
88
+
89
+ 2. **Graph Coverage Zones from Points**
90
+
91
+ A function for generating **coverage areas** from a set of origin points using a transport network.
92
+ It computes the area reachable from each point by **travel time** or **distance**,
93
+ then builds polygons using **Voronoi diagrams** and clips them by a given boundary if specified.
94
+
95
+ 📘 `Example <https://iduclub.github.io/ObjectNat/methods/examples/coverage.html>`__
96
+ 🔗 `Documentation <https://iduclub.github.io/ObjectNat/methods/coverage.html>`__
97
+
98
+ 3. **Service Provision Analysis**
99
+
100
+ A function to evaluate how well residential buildings and their populations are provided
101
+ with services (e.g., schools, clinics) that have limited **capacity**
102
+ and a defined **accessibility threshold** (in minutes or meters).
103
+ The function models the **balance between supply and demand**,
104
+ assessing how well services meet the needs of nearby buildings within an acceptable time.
105
+
106
+ 📘 `Example <https://iduclub.github.io/ObjectNat/methods/examples/provision.html>`__
107
+ 🔗 `Documentation <https://iduclub.github.io/ObjectNat/methods/provision.html>`__
108
+
109
+ 4. **Visibility Analysis**
110
+
111
+ A function for evaluating visibility from a given point or set of points to nearby buildings within a given radius.
112
+ It is used to assess visual accessibility in urban environments.
113
+ A module is also implemented for computing **visibility coverage zones**
114
+ using a dense observer grid (recommended ~1000 points with a 10–20 m spacing).
115
+ Points can be generated along the transport network and distributed across its edges.
116
+
117
+ 📘 `Example <https://iduclub.github.io/ObjectNat/methods/examples/visibility.html>`__
118
+ 🔗 `Documentation <https://iduclub.github.io/ObjectNat/methods/visibility.html>`__
119
+
120
+ 5. **Noise Simulation & Noise Frame**
121
+
122
+ Simulation of noise propagation from sources, taking into account **obstacles**, **vegetation**,
123
+ and **environmental factors**.
124
+
125
+ 📘 `Example <https://iduclub.github.io/ObjectNat/methods/examples/noise.html>`__
126
+ 🔗 `Documentation <https://iduclub.github.io/ObjectNat/methods/noise.html>`__
127
+ 🧠 `Detailed theory <https://github.com/DDonnyy/ObjectNat/wiki/Noise-simulation>`__
128
+
129
+ 6. **Point Clusterization**
130
+
131
+ A function for constructing **cluster polygons** based on a set of points using:
132
+
133
+ - Minimum **distance** between points.
134
+ - Minimum **number of points** in a cluster.
135
+
136
+ The function can also compute the **ratio of service types** in each cluster
137
+ for spatial analysis of service composition.
138
+
139
+ 📘 `Example <https://iduclub.github.io/ObjectNat/methods/examples/clustering.html>`__
140
+ 🔗 `Documentation <https://iduclub.github.io/ObjectNat/methods/clustering.html>`__
141
+
142
+ ----
143
+
144
+ City Graphs via *IduEdu*
145
+ ------------------------
146
+
147
+ For optimal performance, **ObjectNat** is recommended to be used with graphs
148
+ created by the `IduEdu <https://github.com/IDUclub/IduEdu>`_ library.
149
+
150
+ **IduEdu** is an open-source Python library designed for building and processing
151
+ complex urban networks based on OpenStreetMap data.
152
+
153
+
154
+ **IduEdu** can be installed via ``pip``::
155
+
156
+ pip install IduEdu
157
+
158
+ Example usage::
159
+
160
+ from iduedu import get_4326_boundary, get_intermodal_graph
161
+
162
+ poly = get_4326_boundary(osm_id=1114252)
163
+ G_intermodal = get_intermodal_graph(territory=poly, clip_by_territory=True)
164
+
165
+ ----
166
+
167
+ Installation
168
+ ------------
169
+
170
+ **ObjectNat** can be installed via ``pip``::
171
+
172
+ pip install ObjectNat
173
+
174
+ ----
175
+
176
+ Configuration
177
+ -------------
178
+
179
+ You can adjust logging and progress bar output using the config module::
180
+
181
+ from objectnat import config
182
+
183
+ config.change_logger_lvl("INFO") # mute debug logs
184
+ config.set_enable_tqdm(False) # disable tqdm progress bars
185
+
186
+ ----
187
+
188
+ Contacts
189
+ --------
190
+
191
+ - `NCCR <https://actcognitive.org/>`_ — National Center for Cognitive Research
192
+ - `IDU <https://idu.itmo.ru/>`_ — Institute of Design and Urban Studies
193
+ - `Natalya Chichkova <https://t.me/nancy_nat>`_ — Project Manager
194
+ - `Danila Oleynikov (Donny) <https://t.me/ddonny_dd>`_ — Lead Software Engineer
195
+
196
+ ----
197
+
198
+ Publications
199
+ ------------
200
+
201
+ Coming soon.
202
+