ObjectNat 1.0.1__py3-none-any.whl → 1.2.0__py3-none-any.whl

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

Potentially problematic release.


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

objectnat/_api.py CHANGED
@@ -1,10 +1,11 @@
1
1
  # pylint: disable=unused-import,wildcard-import,unused-wildcard-import
2
2
 
3
- from .methods.coverage_zones import get_graph_coverage, get_radius_coverage
3
+ from .methods.coverage_zones import get_graph_coverage, get_radius_coverage, get_stepped_graph_coverage
4
4
  from .methods.isochrones import get_accessibility_isochrone_stepped, get_accessibility_isochrones
5
- from .methods.noise import simulate_noise
5
+ from .methods.noise import calculate_simplified_noise_frame, simulate_noise
6
6
  from .methods.point_clustering import get_clusters_polygon
7
7
  from .methods.provision import clip_provision, get_service_provision, recalculate_links
8
+ from .methods.utils import gdf_to_graph, graph_to_gdf
8
9
  from .methods.visibility import (
9
10
  calculate_visibility_catchment_area,
10
11
  get_visibilities_from_points,
objectnat/_version.py CHANGED
@@ -1 +1 @@
1
- VERSION = "1.0.1"
1
+ VERSION = "1.2.0"
@@ -1,2 +1,3 @@
1
1
  from .graph_coverage import get_graph_coverage
2
- from .radius_voronoi import get_radius_coverage
2
+ from .radius_voronoi_coverage import get_radius_coverage
3
+ from .stepped_coverage import get_stepped_graph_coverage
@@ -6,11 +6,11 @@ import pandas as pd
6
6
  from pyproj.exceptions import CRSError
7
7
  from shapely import Point, concave_hull
8
8
 
9
- from objectnat.methods.utils.graph_utils import get_closest_nodes_from_gdf, remove_weakly_connected_nodes
9
+ from objectnat.methods.utils.graph_utils import get_closest_nodes_from_gdf, reverse_graph
10
10
 
11
11
 
12
12
  def get_graph_coverage(
13
- gdf_from: gpd.GeoDataFrame,
13
+ gdf_to: gpd.GeoDataFrame,
14
14
  nx_graph: nx.Graph,
15
15
  weight_type: Literal["time_min", "length_meter"],
16
16
  weight_value_cutoff: float = None,
@@ -29,8 +29,8 @@ def get_graph_coverage(
29
29
 
30
30
  Parameters
31
31
  ----------
32
- gdf_from : gpd.GeoDataFrame
33
- Source points from which coverage is calculated.
32
+ gdf_to : gpd.GeoDataFrame
33
+ Source points to which coverage is calculated.
34
34
  nx_graph : nx.Graph
35
35
  NetworkX graph representing the transportation network.
36
36
  weight_type : Literal["time_min", "length_meter"]
@@ -58,29 +58,19 @@ def get_graph_coverage(
58
58
  >>> graph = get_intermodal_graph(osm_id=1114252)
59
59
  >>> coverage = get_graph_coverage(points, graph, "time_min", 15)
60
60
  """
61
- original_crs = gdf_from.crs
61
+ original_crs = gdf_to.crs
62
62
  try:
63
63
  local_crs = nx_graph.graph["crs"]
64
64
  except KeyError as exc:
65
65
  raise ValueError("Graph does not have crs attribute") from exc
66
66
 
67
67
  try:
68
- points = gdf_from.copy()
68
+ points = gdf_to.copy()
69
69
  points.to_crs(local_crs, inplace=True)
70
70
  except CRSError as e:
71
71
  raise CRSError(f"Graph crs ({local_crs}) has invalid format.") from e
72
72
 
73
- if nx_graph.is_multigraph():
74
- if nx_graph.is_directed():
75
- nx_graph = nx.DiGraph(nx_graph)
76
- else:
77
- nx_graph = nx.Graph(nx_graph)
78
- nx_graph = remove_weakly_connected_nodes(nx_graph)
79
- sparse_matrix = nx.to_scipy_sparse_array(nx_graph, weight=weight_type)
80
- transposed_matrix = sparse_matrix.transpose()
81
- reversed_graph = nx.from_scipy_sparse_array(
82
- transposed_matrix, edge_attribute=weight_type, create_using=type(nx_graph)
83
- )
73
+ nx_graph, reversed_graph = reverse_graph(nx_graph, weight_type)
84
74
 
85
75
  points.geometry = points.representative_point()
86
76
 
@@ -88,7 +78,7 @@ def get_graph_coverage(
88
78
 
89
79
  points["nearest_node"] = nearest_nodes
90
80
 
91
- _, nearest_paths = nx.multi_source_dijkstra(
81
+ nearest_paths = nx.multi_source_dijkstra_path(
92
82
  reversed_graph, nearest_nodes, weight=weight_type, cutoff=weight_value_cutoff
93
83
  )
94
84
  reachable_nodes = list(nearest_paths.keys())
@@ -0,0 +1,142 @@
1
+ from typing import Literal
2
+
3
+ import geopandas as gpd
4
+ import networkx as nx
5
+ import numpy as np
6
+ import pandas as pd
7
+ from pyproj.exceptions import CRSError
8
+ from shapely import Point, concave_hull
9
+
10
+ from objectnat.methods.isochrones.isochrone_utils import create_separated_dist_polygons
11
+ from objectnat.methods.utils.graph_utils import get_closest_nodes_from_gdf, reverse_graph
12
+
13
+
14
+ def get_stepped_graph_coverage(
15
+ gdf_to: gpd.GeoDataFrame,
16
+ nx_graph: nx.Graph,
17
+ weight_type: Literal["time_min", "length_meter"],
18
+ step_type: Literal["voronoi", "separate"],
19
+ weight_value_cutoff: float = None,
20
+ zone: gpd.GeoDataFrame = None,
21
+ step: float = None,
22
+ ):
23
+ """
24
+ Calculate stepped coverage zones from source points through a graph network using Dijkstra's algorithm
25
+ and Voronoi-based or buffer-based isochrone steps.
26
+
27
+ This function combines graph-based accessibility with stepped isochrone logic. It:
28
+ 1. Finds nearest graph nodes for each input point
29
+ 2. Computes reachability for increasing weights (e.g. time or distance) in defined steps
30
+ 3. Generates Voronoi-based or separate buffer zones around network nodes
31
+ 4. Aggregates zones into stepped coverage layers
32
+ 5. Optionally clips results to a boundary zone
33
+
34
+ Parameters
35
+ ----------
36
+ gdf_to : gpd.GeoDataFrame
37
+ Source points from which stepped coverage is calculated.
38
+ nx_graph : nx.Graph
39
+ NetworkX graph representing the transportation network.
40
+ weight_type : Literal["time_min", "length_meter"]
41
+ Type of edge weight to use for path calculation:
42
+ - "time_min": Edge travel time in minutes
43
+ - "length_meter": Edge length in meters
44
+ step_type : Literal["voronoi", "separate"]
45
+ Method for generating stepped zones:
46
+ - "voronoi": Stepped zones based on Voronoi polygons around graph nodes
47
+ - "separate": Independent buffer zones per step
48
+ weight_value_cutoff : float, optional
49
+ Maximum weight value (e.g., max travel time or distance) to limit the coverage extent.
50
+ zone : gpd.GeoDataFrame, optional
51
+ Optional boundary polygon to clip resulting stepped zones. If None, concave hull of reachable area is used.
52
+ step : float, optional
53
+ Step interval for coverage zone construction. Defaults to:
54
+ - 100 meters for distance-based weight
55
+ - 1 minute for time-based weight
56
+
57
+ Returns
58
+ -------
59
+ gpd.GeoDataFrame
60
+ GeoDataFrame with polygons representing stepped coverage zones for each input point, annotated by step range.
61
+
62
+ Notes
63
+ -----
64
+ - Input graph must have a valid CRS defined.
65
+ - MultiGraph or MultiDiGraph inputs will be simplified.
66
+ - Designed for accessibility and spatial equity analyses over multimodal networks.
67
+
68
+ Examples
69
+ --------
70
+ >>> from iduedu import get_intermodal_graph
71
+ >>> points = gpd.read_file('destinations.geojson')
72
+ >>> graph = get_intermodal_graph(osm_id=1114252)
73
+ >>> stepped_coverage = get_stepped_graph_coverage(
74
+ ... points, graph, "time_min", step_type="voronoi", weight_value_cutoff=30, step=5
75
+ ... )
76
+ >>> # Using buffer-style zones
77
+ >>> stepped_separate = get_stepped_graph_coverage(
78
+ ... points, graph, "length_meter", step_type="separate", weight_value_cutoff=1000, step=200
79
+ ... )
80
+ """
81
+ if step is None:
82
+ if weight_type == "length_meter":
83
+ step = 100
84
+ else:
85
+ step = 1
86
+ original_crs = gdf_to.crs
87
+ try:
88
+ local_crs = nx_graph.graph["crs"]
89
+ except KeyError as exc:
90
+ raise ValueError("Graph does not have crs attribute") from exc
91
+
92
+ try:
93
+ points = gdf_to.copy()
94
+ points.to_crs(local_crs, inplace=True)
95
+ except CRSError as e:
96
+ raise CRSError(f"Graph crs ({local_crs}) has invalid format.") from e
97
+
98
+ nx_graph, reversed_graph = reverse_graph(nx_graph, weight_type)
99
+
100
+ points.geometry = points.representative_point()
101
+
102
+ distances, nearest_nodes = get_closest_nodes_from_gdf(points, nx_graph)
103
+
104
+ points["nearest_node"] = nearest_nodes
105
+ points["distance"] = distances
106
+
107
+ dist = nx.multi_source_dijkstra_path_length(
108
+ reversed_graph, nearest_nodes, weight=weight_type, cutoff=weight_value_cutoff
109
+ )
110
+
111
+ graph_points = pd.DataFrame(
112
+ data=[{"node": node, "geometry": Point(data["x"], data["y"])} for node, data in nx_graph.nodes(data=True)]
113
+ )
114
+
115
+ nearest_nodes = pd.DataFrame.from_dict(dist, orient="index", columns=["dist"]).reset_index()
116
+
117
+ graph_nodes_gdf = gpd.GeoDataFrame(
118
+ graph_points.merge(nearest_nodes, left_on="node", right_on="index", how="left").reset_index(drop=True),
119
+ geometry="geometry",
120
+ crs=local_crs,
121
+ )
122
+ graph_nodes_gdf.drop(columns=["index", "node"], inplace=True)
123
+ if weight_value_cutoff is None:
124
+ weight_value_cutoff = max(nearest_nodes["dist"])
125
+ if step_type == "voronoi":
126
+ graph_nodes_gdf["dist"] = np.minimum(np.ceil(graph_nodes_gdf["dist"] / step) * step, weight_value_cutoff)
127
+ voronois = gpd.GeoDataFrame(geometry=graph_nodes_gdf.voronoi_polygons(), crs=local_crs)
128
+ zone_coverages = voronois.sjoin(graph_nodes_gdf).dissolve(by="dist", as_index=False, dropna=False)
129
+ zone_coverages = zone_coverages[["dist", "geometry"]].explode(ignore_index=True)
130
+ if zone is None:
131
+ zone = concave_hull(graph_nodes_gdf[~graph_nodes_gdf["node_to"].isna()].union_all(), ratio=0.5)
132
+ else:
133
+ zone = zone.to_crs(local_crs)
134
+ zone_coverages = zone_coverages.clip(zone).to_crs(original_crs)
135
+ else: # step_type == 'separate':
136
+ speed = 83.33 # TODO HARDCODED WALK SPEED
137
+ weight_value = weight_value_cutoff
138
+ zone_coverages = create_separated_dist_polygons(graph_nodes_gdf, weight_value, weight_type, step, speed)
139
+ if zone is not None:
140
+ zone = zone.to_crs(local_crs)
141
+ zone_coverages = zone_coverages.clip(zone).to_crs(original_crs)
142
+ return zone_coverages
@@ -5,8 +5,10 @@ import networkx as nx
5
5
  import numpy as np
6
6
  import pandas as pd
7
7
  from pyproj.exceptions import CRSError
8
+ from shapely.ops import polygonize
8
9
 
9
10
  from objectnat import config
11
+ from objectnat.methods.utils.geom_utils import polygons_to_multilinestring
10
12
  from objectnat.methods.utils.graph_utils import get_closest_nodes_from_gdf, remove_weakly_connected_nodes
11
13
 
12
14
  logger = config.logger
@@ -128,3 +130,38 @@ def _create_isochrones_gdf(
128
130
  isochrones["weight_type"] = weight_type
129
131
  isochrones["weight_value"] = weight_value
130
132
  return isochrones
133
+
134
+
135
+ def create_separated_dist_polygons(
136
+ points: gpd.GeoDataFrame, weight_value, weight_type, step, speed
137
+ ) -> gpd.GeoDataFrame:
138
+ points["dist"] = points["dist"].clip(lower=0.1)
139
+ steps = np.arange(0, weight_value + step, step)
140
+ if steps[-1] > weight_value:
141
+ steps[-1] = weight_value # Ensure last step doesn't exceed weight_value
142
+ for i in range(len(steps) - 1):
143
+ min_dist = steps[i]
144
+ max_dist = steps[i + 1]
145
+ nodes_in_step = points["dist"].between(min_dist, max_dist, inclusive="left")
146
+ nodes_in_step = nodes_in_step[nodes_in_step].index
147
+ if not nodes_in_step.empty:
148
+ buffer_size = (max_dist - points.loc[nodes_in_step, "dist"]) * 0.7
149
+ if weight_type == "time_min":
150
+ buffer_size = buffer_size * speed
151
+ points.loc[nodes_in_step, "buffer_size"] = buffer_size
152
+ points.geometry = points.geometry.buffer(points["buffer_size"])
153
+ points["dist"] = np.minimum(np.ceil(points["dist"] / step) * step, weight_value)
154
+ points = points.dissolve(by="dist", as_index=False)
155
+ polygons = gpd.GeoDataFrame(
156
+ geometry=list(polygonize(points.geometry.apply(polygons_to_multilinestring).union_all())),
157
+ crs=points.crs,
158
+ )
159
+ polygons_points = polygons.copy()
160
+ polygons_points.geometry = polygons.representative_point()
161
+ stepped_polygons = polygons_points.sjoin(points, predicate="within").reset_index()
162
+ stepped_polygons = stepped_polygons.groupby("index").agg({"dist": "mean"})
163
+ stepped_polygons["dist"] = np.minimum(np.floor(stepped_polygons["dist"] / step) * step, weight_value)
164
+ stepped_polygons["geometry"] = polygons
165
+ stepped_polygons = gpd.GeoDataFrame(stepped_polygons, geometry="geometry", crs=points.crs).reset_index(drop=True)
166
+ stepped_polygons = stepped_polygons.dissolve(by="dist", as_index=False).explode(ignore_index=True)
167
+ return stepped_polygons
@@ -3,7 +3,6 @@ from typing import Literal
3
3
  import geopandas as gpd
4
4
  import networkx as nx
5
5
  import numpy as np
6
- from shapely.ops import polygonize
7
6
 
8
7
  from objectnat import config
9
8
  from objectnat.methods.isochrones.isochrone_utils import (
@@ -12,8 +11,9 @@ from objectnat.methods.isochrones.isochrone_utils import (
12
11
  _prepare_graph_and_nodes,
13
12
  _process_pt_data,
14
13
  _validate_inputs,
14
+ create_separated_dist_polygons,
15
15
  )
16
- from objectnat.methods.utils.geom_utils import polygons_to_multilinestring, remove_inner_geom
16
+ from objectnat.methods.utils.geom_utils import remove_inner_geom
17
17
  from objectnat.methods.utils.graph_utils import graph_to_gdf
18
18
 
19
19
  logger = config.logger
@@ -116,35 +116,9 @@ def get_accessibility_isochrone_stepped(
116
116
  logger.info("Building isochrones geometry...")
117
117
  nodes, edges = graph_to_gdf(subgraph)
118
118
  nodes.loc[dist_matrix.columns, "dist"] = dist_matrix.iloc[0]
119
- steps = np.arange(0, weight_value + step, step)
120
- if steps[-1] > weight_value:
121
- steps[-1] = weight_value # Ensure last step doesn't exceed weight_value
122
119
 
123
120
  if isochrone_type == "separate":
124
- for i in range(len(steps) - 1):
125
- min_dist = steps[i]
126
- max_dist = steps[i + 1]
127
- nodes_in_step = nodes["dist"].between(min_dist, max_dist, inclusive="left")
128
- nodes_in_step = nodes_in_step[nodes_in_step].index
129
- if not nodes_in_step.empty:
130
- buffer_size = (max_dist - nodes.loc[nodes_in_step, "dist"]) * 0.7
131
- if weight_type == "time_min":
132
- buffer_size = buffer_size * speed
133
- nodes.loc[nodes_in_step, "buffer_size"] = buffer_size
134
- nodes.geometry = nodes.geometry.buffer(nodes["buffer_size"])
135
- nodes["dist"] = np.round(nodes["dist"], 0)
136
- nodes = nodes.dissolve(by="dist", as_index=False)
137
- polygons = gpd.GeoDataFrame(
138
- geometry=list(polygonize(nodes.geometry.apply(polygons_to_multilinestring).union_all())),
139
- crs=local_crs,
140
- )
141
- polygons_points = polygons.copy()
142
- polygons_points.geometry = polygons.representative_point()
143
-
144
- stepped_iso = polygons_points.sjoin(nodes, predicate="within").reset_index()
145
- stepped_iso = stepped_iso.groupby("index").agg({"dist": "mean"})
146
- stepped_iso["geometry"] = polygons
147
- stepped_iso = gpd.GeoDataFrame(stepped_iso, geometry="geometry", crs=local_crs).reset_index(drop=True)
121
+ stepped_iso = create_separated_dist_polygons(nodes, weight_value, weight_type, step, speed)
148
122
  else:
149
123
  if isochrone_type == "radius":
150
124
  isochrone_geoms = _build_radius_isochrones(
@@ -1,3 +1,4 @@
1
- from .noise_sim import simulate_noise
1
+ from .noise_simulation import simulate_noise
2
2
  from .noise_reduce import dist_to_target_db, green_noise_reduce_db
3
3
  from .noise_exceptions import InvalidStepError
4
+ from .noise_simulation_simplified import calculate_simplified_noise_frame
@@ -13,6 +13,7 @@ from tqdm import tqdm
13
13
  from objectnat import config
14
14
  from objectnat.methods.noise.noise_exceptions import InvalidStepError
15
15
  from objectnat.methods.noise.noise_reduce import dist_to_target_db, green_noise_reduce_db
16
+ from objectnat.methods.noise.noise_simulation_simplified import _eval_donuts_gdf
16
17
  from objectnat.methods.utils.geom_utils import (
17
18
  gdf_to_circle_zones_from_point,
18
19
  get_point_from_a_thorough_b,
@@ -22,26 +23,33 @@ from objectnat.methods.visibility.visibility_analysis import get_visibility_accu
22
23
 
23
24
  logger = config.logger
24
25
 
26
+ MAX_DB_VALUE = 194
27
+
25
28
 
26
29
  def simulate_noise(
27
- source_points: gpd.GeoDataFrame, obstacles: gpd.GeoDataFrame, source_noise_db, geometric_mean_freq_hz, **kwargs
30
+ source_points: gpd.GeoDataFrame,
31
+ obstacles: gpd.GeoDataFrame,
32
+ source_noise_db: float = None,
33
+ geometric_mean_freq_hz: float = None,
34
+ **kwargs,
28
35
  ):
29
36
  """
30
37
  Simulates noise propagation from a set of source points considering obstacles, trees, and environmental factors.
31
38
 
32
39
  Args:
33
- source_points (gpd.GeoDataFrame): A GeoDataFrame containing one or more points representing the noise sources.
34
- A separate simulation will be run for each point.
40
+ source_points (gpd.GeoDataFrame): A GeoDataFrame with one or more point geometries representing noise sources.
41
+ Optionally, it can include 'source_noise_db' and 'geometric_mean_freq_hz' columns for per-point simulation.
35
42
  obstacles (gpd.GeoDataFrame): A GeoDataFrame representing obstacles in the environment. If a column with
36
43
  sound absorption coefficients is present, its name should be provided in the `absorb_ratio_column` argument.
37
44
  Missing values will be filled with the `standart_absorb_ratio`.
38
- source_noise_db (float): The noise level of the point source in decibels (dB). Decibels are logarithmic units
39
- used to measure sound intensity. A value of 20 dB represents a barely audible whisper, while 140 dB
40
- is comparable to the noise of jet engines.
41
- geometric_mean_freq_hz (float): The geometric mean frequency of the sound (in Hz). This parameter influences
42
- the sound wave's propagation and scattering in the presence of trees. Lower frequencies travel longer
43
- distances than higher frequencies. It's recommended to use values between 63 Hz and 8000 Hz; values outside
44
- this range will be clamped to the nearest boundary for the sound absorption coefficient calculation.
45
+ source_noise_db (float, optional): Default noise level (dB) to use if not specified per-point. Decibels are
46
+ logarithmic units used to measure sound intensity. A value of 20 dB represents a barely audible whisper,
47
+ while 140 dB is comparable to the noise of jet engines.
48
+ geometric_mean_freq_hz (float, optional): Default frequency (Hz) to use if not specified per-point.
49
+ This parameter influences the sound wave's propagation and scattering in the presence of trees.
50
+ Lower frequencies travel longer distances than higher frequencies.
51
+ It's recommended to use values between 63 Hz and 8000 Hz; values outside this range will be clamped to the
52
+ nearest boundary for the sound absorption coefficient calculation.
45
53
 
46
54
  Optional kwargs:
47
55
  absorb_ratio_column (str, optional): The name of the column in the `obstacles` GeoDataFrame that contains the
@@ -88,12 +96,46 @@ def simulate_noise(
88
96
  reflection_n = kwargs.get("reflection_n", 3)
89
97
  dead_area_r = kwargs.get("dead_area_r", 5)
90
98
 
99
+ # Validate optional columns or default values
100
+ use_column_db = False
101
+ if "source_noise_db" in source_points.columns:
102
+ if (source_points["source_noise_db"] > MAX_DB_VALUE).any():
103
+ raise ValueError(
104
+ f"One or more values in 'source_noise_db' column exceed the physical limit of {MAX_DB_VALUE} dB."
105
+ )
106
+ if source_points["source_noise_db"].isnull().any():
107
+ raise ValueError(f"Column 'source_noise_db' contains missing (NaN) values")
108
+ use_column_db = True
109
+
110
+ use_column_freq = False
111
+ if "geometric_mean_freq_hz" in source_points.columns:
112
+ if source_points["geometric_mean_freq_hz"].isnull().any():
113
+ raise ValueError(f"Column 'geometric_mean_freq_hz' contains missing (NaN) values")
114
+ use_column_freq = True
115
+
116
+ if not use_column_db:
117
+ if source_noise_db is None:
118
+ raise ValueError(
119
+ "Either `source_noise_db` must be provided or the `source_points` must contain a 'source_noise_db' column."
120
+ )
121
+ if source_noise_db > MAX_DB_VALUE:
122
+ raise ValueError(
123
+ f"source_noise_db ({source_noise_db} dB) exceeds the physical limit of {MAX_DB_VALUE} dB in air."
124
+ )
125
+
126
+ if not use_column_freq:
127
+ if geometric_mean_freq_hz is None:
128
+ raise ValueError(
129
+ "Either `geometric_mean_freq_hz` must be provided or the `source_points` must contain a 'geometric_mean_freq_hz' column."
130
+ )
131
+ if not use_column_db and not use_column_freq and len(source_points) > 1:
132
+ logger.warning(
133
+ "`source_noise_db` and `geometric_mean_freq_hz` will be used for all points. Per-point simulation parameters not found."
134
+ )
135
+
91
136
  original_crs = source_points.crs
137
+ source_points = source_points.copy()
92
138
 
93
- div_ = (source_noise_db - target_noise_db) % db_sim_step
94
- if div_ != 0:
95
- raise InvalidStepError(source_noise_db, target_noise_db, db_sim_step, div_)
96
- # Choosing crs and simplifying obs if any
97
139
  source_points = source_points.copy()
98
140
  if len(obstacles) > 0:
99
141
  obstacles = obstacles.copy()
@@ -118,63 +160,54 @@ def simulate_noise(
118
160
  if absorb_ratio_column is None:
119
161
  obstacles["absorb_ratio"] = standart_absorb_ratio
120
162
  else:
121
- obstacles["absorb_ratio"] = obstacles[absorb_ratio_column]
122
- obstacles["absorb_ratio"] = obstacles["absorb_ratio"].fillna(standart_absorb_ratio)
163
+ obstacles["absorb_ratio"] = obstacles[absorb_ratio_column].fillna(standart_absorb_ratio)
123
164
  obstacles = obstacles[["absorb_ratio", "geometry"]]
124
165
 
125
- logger.info(
126
- dist_to_target_db(
127
- source_noise_db,
128
- target_noise_db,
129
- geometric_mean_freq_hz,
130
- air_temperature,
131
- return_desc=True,
132
- check_temp_freq=True,
133
- )
134
- )
135
- # calculating layer dist and db values
136
- dist_db = [(0, source_noise_db)]
137
- cur_db = source_noise_db - db_sim_step
138
- while cur_db != target_noise_db - db_sim_step:
139
- max_dist = dist_to_target_db(source_noise_db, cur_db, geometric_mean_freq_hz, air_temperature)
140
- dist_db.append((max_dist, cur_db))
141
- cur_db = cur_db - db_sim_step
142
-
143
166
  # creating initial task and simulating for each point
144
- all_p_res = []
167
+ task_queue = multiprocessing.Queue()
168
+ dead_area_dict = {}
145
169
  for ind, row in source_points.iterrows():
146
- logger.info(f"Started simulation for point {ind+1} / {len(source_points)}")
147
170
  source_point = row.geometry
148
- task_queue = multiprocessing.Queue()
171
+ local_db = row["source_noise_db"] if use_column_db else source_noise_db
172
+ local_freq = row["geometric_mean_freq_hz"] if use_column_freq else geometric_mean_freq_hz
173
+ div_ = (local_db - target_noise_db) % db_sim_step
174
+ if div_ != 0:
175
+ raise InvalidStepError(local_db, target_noise_db, db_sim_step, div_)
176
+ # calculating layer dist and db values
177
+ dist_db = [(0, local_db)]
178
+ cur_db = local_db - db_sim_step
179
+ while cur_db != target_noise_db - db_sim_step:
180
+ max_dist = dist_to_target_db(local_db, cur_db, local_freq, air_temperature)
181
+ dist_db.append((max_dist, cur_db))
182
+ cur_db -= db_sim_step
183
+
149
184
  args = (source_point, obstacles, trees, 0, 0, dist_db)
150
185
  kwargs = {
151
186
  "reflection_n": reflection_n,
152
- "geometric_mean_freq_hz": geometric_mean_freq_hz,
187
+ "geometric_mean_freq_hz": local_freq,
153
188
  "tree_res": tree_res,
154
189
  "min_db": target_noise_db,
190
+ "simulation_ind": ind,
155
191
  }
156
192
  task_queue.put((_noise_from_point_task, args, kwargs))
193
+ dead_area_dict[ind] = source_point.buffer(dead_area_r, resolution=2)
157
194
 
158
- noise_gdf = _parallel_split_queue(
159
- task_queue, dead_area=source_point.buffer(dead_area_r, resolution=2), dead_area_r=dead_area_r
160
- )
195
+ noise_gdf = _parallel_split_queue(task_queue, dead_area_dict=dead_area_dict, dead_area_r=dead_area_r)
161
196
 
162
- noise_gdf = gpd.GeoDataFrame(pd.concat(noise_gdf, ignore_index=True), crs=local_crs)
163
- polygons = gpd.GeoDataFrame(
164
- geometry=list(polygonize(noise_gdf.geometry.apply(polygons_to_multilinestring).union_all())), crs=local_crs
165
- )
166
- polygons_points = polygons.copy()
167
- polygons_points.geometry = polygons.representative_point()
168
- sim_result = polygons_points.sjoin(noise_gdf, predicate="within").reset_index()
169
- sim_result = sim_result.groupby("index").agg({"noise_level": "max"})
170
- sim_result["geometry"] = polygons
171
- sim_result = (
172
- gpd.GeoDataFrame(sim_result, geometry="geometry", crs=local_crs).dissolve(by="noise_level").reset_index()
173
- )
174
- sim_result["source_point_ind"] = ind
175
- all_p_res.append(sim_result)
197
+ noise_gdf = gpd.GeoDataFrame(pd.concat(noise_gdf, ignore_index=True), crs=local_crs)
198
+ polygons = gpd.GeoDataFrame(
199
+ geometry=list(polygonize(noise_gdf.geometry.apply(polygons_to_multilinestring).union_all())), crs=local_crs
200
+ )
201
+ polygons_points = polygons.copy()
202
+ polygons_points.geometry = polygons.representative_point()
203
+ sim_result = polygons_points.sjoin(noise_gdf, predicate="within").reset_index()
204
+ sim_result = sim_result.groupby("index").agg({"noise_level": "max"})
205
+ sim_result["geometry"] = polygons
206
+ sim_result = (
207
+ gpd.GeoDataFrame(sim_result, geometry="geometry", crs=local_crs).dissolve(by="noise_level").reset_index()
208
+ )
176
209
 
177
- return gpd.GeoDataFrame(pd.concat(all_p_res, ignore_index=True), crs=local_crs).to_crs(original_crs)
210
+ return sim_result.to_crs(original_crs)
178
211
 
179
212
 
180
213
  def _noise_from_point_task(task, **kwargs) -> tuple[gpd.GeoDataFrame, list[tuple] | None]: # pragma: no cover
@@ -272,22 +305,7 @@ def _noise_from_point_task(task, **kwargs) -> tuple[gpd.GeoDataFrame, list[tuple
272
305
  noise_reduce = int(round(green_noise_reduce_db(geometric_mean_freq_hz, r_tree_new)))
273
306
  reduce_polygons.append((red_polygon, noise_reduce))
274
307
 
275
- # Generating donuts - db values
276
- donuts = []
277
- don_values = []
278
- to_cut_off = point_from
279
- for i in range(len(donuts_dist_values[:-1])):
280
- cur_buffer = point_from.buffer(donuts_dist_values[i + 1][0])
281
- donuts.append(cur_buffer.difference(to_cut_off))
282
- don_values.append(donuts_dist_values[i][1])
283
- to_cut_off = cur_buffer
284
-
285
- noise_from_point = (
286
- gpd.GeoDataFrame(geometry=donuts, data={"noise_level": don_values}, crs=local_crs)
287
- .clip(vis_poly, keep_geom_type=True)
288
- .explode(ignore_index=True)
289
- )
290
-
308
+ noise_from_point = _eval_donuts_gdf(point_from, donuts_dist_values, local_crs, vis_poly)
291
309
  # intersect noise poly with noise reduce
292
310
  if len(reduce_polygons) > 0:
293
311
  reduce_polygons = gpd.GeoDataFrame(
@@ -383,34 +401,34 @@ def _noise_from_point_task(task, **kwargs) -> tuple[gpd.GeoDataFrame, list[tuple
383
401
  return noise_from_point, new_tasks
384
402
 
385
403
 
386
- def _parallel_split_queue(task_queue: multiprocessing.Queue, dead_area: Polygon, dead_area_r: int):
404
+ def _parallel_split_queue(task_queue: multiprocessing.Queue, dead_area_dict: dict, dead_area_r: int):
387
405
  results = []
388
406
  total_tasks = task_queue.qsize()
389
407
 
390
408
  with tqdm(total=total_tasks, desc="Simulating noise") as pbar:
391
409
  with concurrent.futures.ProcessPoolExecutor() as executor:
410
+ # with concurrent.futures.ThreadPoolExecutor() as executor:
392
411
  future_to_task = {}
393
412
  while True:
394
413
  while not task_queue.empty() and len(future_to_task) < executor._max_workers:
395
414
  func, task, kwargs = task_queue.get_nowait()
396
415
  future = executor.submit(func, task, **kwargs)
397
- future_to_task[future] = task
398
-
416
+ future_to_task[future] = kwargs["simulation_ind"]
399
417
  done, _ = concurrent.futures.wait(future_to_task.keys(), return_when=concurrent.futures.FIRST_COMPLETED)
400
-
401
418
  for future in done:
402
- future_to_task.pop(future)
419
+ simulation_ind = future_to_task.pop(future)
403
420
  result, new_tasks = future.result()
404
421
  if new_tasks:
405
422
  new_tasks_n = 0
406
- new_dead_area_points = [dead_area]
407
- for func, new_task, kwargs in new_tasks:
408
- if not dead_area.covers(new_task[0]):
409
- new_tasks_n = new_tasks_n + 1
410
- task_queue.put((func, new_task, kwargs))
411
- new_dead_area_points.append(new_task[0].buffer(dead_area_r, resolution=2))
412
-
413
- dead_area = unary_union(new_dead_area_points)
423
+ local_dead_area = dead_area_dict.get(simulation_ind)
424
+ new_dead_area_points = [local_dead_area]
425
+ for func, new_task, new_kwargs in new_tasks:
426
+ new_point = new_task[0]
427
+ if not local_dead_area.covers(new_point):
428
+ task_queue.put((func, new_task, new_kwargs))
429
+ new_dead_area_points.append(new_point.buffer(dead_area_r, resolution=2))
430
+ new_tasks_n += 1
431
+ dead_area_dict[simulation_ind] = unary_union(new_dead_area_points)
414
432
  total_tasks += new_tasks_n
415
433
  pbar.total = total_tasks
416
434
  pbar.refresh()
@@ -419,5 +437,4 @@ def _parallel_split_queue(task_queue: multiprocessing.Queue, dead_area: Polygon,
419
437
  time.sleep(0.01)
420
438
  if not future_to_task and task_queue.empty():
421
439
  break
422
-
423
440
  return results
@@ -0,0 +1,135 @@
1
+ # simplified version
2
+ import geopandas as gpd
3
+ import pandas as pd
4
+ from shapely.ops import polygonize, unary_union
5
+
6
+ from objectnat.methods.noise.noise_reduce import dist_to_target_db
7
+ from objectnat.methods.utils.geom_utils import (
8
+ distribute_points_on_linestrings,
9
+ distribute_points_on_polygons,
10
+ polygons_to_multilinestring,
11
+ )
12
+ from objectnat.methods.visibility.visibility_analysis import get_visibility_accurate
13
+
14
+ MAX_DB_VALUE = 194
15
+
16
+
17
+ def calculate_simplified_noise_frame(
18
+ noise_sources: gpd.GeoDataFrame, obstacles: gpd.GeoDataFrame, air_temperature, **kwargs
19
+ ) -> gpd.GeoDataFrame:
20
+ target_noise_db = kwargs.get("target_noise_db", 40)
21
+ db_sim_step = kwargs.get("db_sim_step", 5)
22
+ linestring_point_radius = kwargs.get("linestring_point_radius", 20)
23
+ polygon_point_radius = kwargs.get("polygon_point_radius", 10)
24
+
25
+ required_columns = ["source_noise_db", "geometric_mean_freq_hz"]
26
+ for col in required_columns:
27
+ if col not in noise_sources.columns:
28
+ raise ValueError(f"'{col}' column is missing in provided GeoDataFrame")
29
+ if noise_sources[col].isnull().any():
30
+ raise ValueError(f"Column '{col}' contains missing (NaN) values")
31
+ if (noise_sources["source_noise_db"] > MAX_DB_VALUE).any():
32
+ raise ValueError(
33
+ f"One or more values in 'source_noise_db' column exceed the physical limit of {MAX_DB_VALUE} dB."
34
+ )
35
+ original_crs = noise_sources.crs
36
+ if len(obstacles) > 0:
37
+ obstacles = obstacles.copy()
38
+ obstacles.geometry = obstacles.geometry.simplify(tolerance=1)
39
+ local_crs = obstacles.estimate_utm_crs()
40
+ obstacles.to_crs(local_crs, inplace=True)
41
+ noise_sources.to_crs(local_crs, inplace=True)
42
+ else:
43
+ local_crs = noise_sources.estimate_utm_crs()
44
+ noise_sources.to_crs(local_crs, inplace=True)
45
+ noise_sources.reset_index(drop=True)
46
+
47
+ noise_sources = noise_sources.explode(ignore_index=True)
48
+ noise_sources["geom_type"] = noise_sources.geom_type
49
+
50
+ grouped_sources = noise_sources.groupby(by=["source_noise_db", "geometric_mean_freq_hz", "geom_type"])
51
+
52
+ frame_result = []
53
+
54
+ for (source_db, freq_hz, geom_type), group_gdf in grouped_sources:
55
+ # calculating layer dist and db values
56
+ dist_db = [(0, source_db)]
57
+ cur_db = source_db - db_sim_step
58
+ max_dist = 0
59
+ while cur_db > target_noise_db - db_sim_step:
60
+ if cur_db - db_sim_step < target_noise_db:
61
+ cur_db = target_noise_db
62
+ max_dist = dist_to_target_db(source_db, cur_db, freq_hz, air_temperature)
63
+ dist_db.append((max_dist, cur_db))
64
+ cur_db -= db_sim_step
65
+
66
+ if geom_type == "Point":
67
+ for _, row in group_gdf.iterrows():
68
+ point_from = row.geometry
69
+ point_buffer = point_from.buffer(max_dist, resolution=16)
70
+ local_obstacles = obstacles[obstacles.intersects(point_buffer)]
71
+ vis_poly = get_visibility_accurate(point_from, obstacles=local_obstacles, view_distance=max_dist)
72
+ noise_from_feature = _eval_donuts_gdf(point_from, dist_db, local_crs, vis_poly)
73
+ frame_result.append(noise_from_feature)
74
+
75
+ elif geom_type == "LineString":
76
+ layer_points = distribute_points_on_linestrings(group_gdf, radius=linestring_point_radius, lloyd_relax_n=1)
77
+ noise_from_feature = _process_lines_or_polygons(
78
+ group_gdf, max_dist, obstacles, layer_points, dist_db, local_crs
79
+ )
80
+ frame_result.append(noise_from_feature)
81
+ elif geom_type == "Polygon":
82
+ group_gdf.geometry = group_gdf.buffer(0.1, resolution=1)
83
+ layer_points = distribute_points_on_polygons(
84
+ group_gdf, only_exterior=False, radius=polygon_point_radius, lloyd_relax_n=1
85
+ )
86
+ noise_from_feature = _process_lines_or_polygons(
87
+ group_gdf, max_dist, obstacles, layer_points, dist_db, local_crs
88
+ )
89
+ frame_result.append(noise_from_feature)
90
+ else:
91
+ pass
92
+
93
+ noise_gdf = gpd.GeoDataFrame(pd.concat(frame_result, ignore_index=True), crs=local_crs)
94
+ polygons = gpd.GeoDataFrame(
95
+ geometry=list(polygonize(noise_gdf.geometry.apply(polygons_to_multilinestring).union_all())), crs=local_crs
96
+ )
97
+ polygons_points = polygons.copy()
98
+ polygons_points.geometry = polygons.representative_point()
99
+ sim_result = polygons_points.sjoin(noise_gdf, predicate="within").reset_index()
100
+ sim_result = sim_result.groupby("index").agg({"noise_level": "max"})
101
+ sim_result["geometry"] = polygons
102
+ sim_result = (
103
+ gpd.GeoDataFrame(sim_result, geometry="geometry", crs=local_crs).dissolve(by="noise_level").reset_index()
104
+ )
105
+
106
+ return sim_result.to_crs(original_crs)
107
+
108
+
109
+ def _process_lines_or_polygons(group_gdf, max_dist, obstacles, layer_points, dist_db, local_crs) -> gpd.GeoDataFrame:
110
+ features_vision_polys = []
111
+ layer_buffer = group_gdf.buffer(max_dist, resolution=16).union_all()
112
+ local_obstacles = obstacles[obstacles.intersects(layer_buffer)]
113
+ for _, row in layer_points.iterrows():
114
+ point_from = row.geometry
115
+ vis_poly = get_visibility_accurate(point_from, obstacles=local_obstacles, view_distance=max_dist)
116
+ features_vision_polys.append(vis_poly)
117
+ features_vision_polys = unary_union(features_vision_polys)
118
+ return _eval_donuts_gdf(group_gdf.union_all(), dist_db, local_crs, features_vision_polys)
119
+
120
+
121
+ def _eval_donuts_gdf(initial_geometry, dist_db, local_crs, clip_poly) -> gpd.GeoDataFrame:
122
+ donuts = []
123
+ don_values = []
124
+ to_cut_off = initial_geometry
125
+ for i in range(len(dist_db[:-1])):
126
+ cur_buffer = initial_geometry.buffer(dist_db[i + 1][0])
127
+ donuts.append(cur_buffer.difference(to_cut_off))
128
+ don_values.append(dist_db[i][1])
129
+ to_cut_off = cur_buffer
130
+ noise_from_feature = (
131
+ gpd.GeoDataFrame(geometry=donuts, data={"noise_level": don_values}, crs=local_crs)
132
+ .clip(clip_poly, keep_geom_type=True)
133
+ .explode(ignore_index=True)
134
+ )
135
+ return noise_from_feature
@@ -0,0 +1 @@
1
+ from .graph_utils import gdf_to_graph, graph_to_gdf
@@ -15,9 +15,9 @@ def polygons_to_multilinestring(geom: Polygon | MultiPolygon):
15
15
 
16
16
  def convert_polygon(polygon: Polygon):
17
17
  lines = []
18
- exterior = LineString(polygon.exterior.coords)
18
+ exterior = LineString(polygon.exterior)
19
19
  lines.append(exterior)
20
- interior = [LineString(p.coords) for p in polygon.interiors]
20
+ interior = [LineString(p) for p in polygon.interiors]
21
21
  lines = lines + interior
22
22
  return lines
23
23
 
@@ -128,3 +128,46 @@ def combine_geometry(gdf: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
128
128
  joined["geometry"] = enclosures
129
129
  joined = gpd.GeoDataFrame(joined, geometry="geometry", crs=crs)
130
130
  return joined
131
+
132
+
133
+ def distribute_points_on_linestrings(lines: gpd.GeoDataFrame, radius, lloyd_relax_n=2) -> gpd.GeoDataFrame:
134
+ lines = lines.copy()
135
+ lines = lines.explode(ignore_index=True)
136
+ lines = lines[lines.geom_type == "LineString"]
137
+ original_crs = lines.crs
138
+ lines = lines.to_crs(crs=lines.estimate_utm_crs())
139
+ lines = lines.reset_index(drop=True)
140
+ lines = lines[["geometry"]]
141
+ radius = radius * 1.1
142
+ segmentized = lines.geometry.apply(lambda x: x.simplify(radius).segmentize(radius))
143
+ points = [Point(pt) for line in segmentized for pt in line.coords]
144
+
145
+ points = gpd.GeoDataFrame(geometry=points, crs=lines.crs)
146
+ lines["lines"] = lines.geometry
147
+ geom_concave = lines.buffer(5, resolution=1).union_all()
148
+
149
+ for i in range(lloyd_relax_n):
150
+ points.geometry = points.voronoi_polygons().clip(geom_concave).centroid
151
+ points = points.sjoin_nearest(lines, how="left")
152
+ points = points[~points.index.duplicated(keep="first")]
153
+ points["geometry"] = points["lines"].interpolate(points["lines"].project(points.geometry))
154
+ points.drop(columns=["lines", "index_right"], inplace=True)
155
+
156
+ return points.dropna().to_crs(original_crs)
157
+
158
+
159
+ def distribute_points_on_polygons(
160
+ polygons: gpd.GeoDataFrame, radius, only_exterior=True, lloyd_relax_n=2
161
+ ) -> gpd.GeoDataFrame:
162
+ polygons = polygons.copy()
163
+ polygons = polygons.explode(ignore_index=True)
164
+ polygons = polygons[polygons.geom_type == "Polygon"]
165
+
166
+ if only_exterior:
167
+ polygons.geometry = polygons.geometry.apply(lambda x: LineString(x.exterior))
168
+ else:
169
+ polygons = gpd.GeoDataFrame(
170
+ geometry=list(polygons.geometry.apply(polygons_to_multilinestring)), crs=polygons.crs
171
+ )
172
+
173
+ return distribute_points_on_linestrings(polygons, radius, lloyd_relax_n=lloyd_relax_n)
@@ -4,7 +4,7 @@ import numpy as np
4
4
  import pandas as pd
5
5
  from loguru import logger
6
6
  from scipy.spatial import KDTree
7
- from shapely import LineString
7
+ from shapely import LineString, MultiLineString, line_merge, node
8
8
  from shapely.geometry.point import Point
9
9
 
10
10
 
@@ -89,7 +89,143 @@ def graph_to_gdf(
89
89
  return nodes_gdf, edges_gdf
90
90
 
91
91
 
92
+ def gdf_to_graph(
93
+ gdf: gpd.GeoDataFrame, project_gdf_attr=True, reproject_to_utm_crs=True, speed=5, check_intersections=True
94
+ ) -> nx.DiGraph:
95
+ """
96
+ Converts a GeoDataFrame of LineStrings into a directed graph (nx.DiGraph).
97
+
98
+ This function transforms a set of linear geometries (which may or may not form a planar graph)
99
+ into a directed graph where each edge corresponds to a LineString (or its segment) from the GeoDataFrame.
100
+ Intersections are optionally checked and merged. Attributes from the original GeoDataFrame
101
+ can be projected onto the graph edges using spatial matching.
102
+
103
+ Parameters
104
+ ----------
105
+ gdf : gpd.GeoDataFrame
106
+ A GeoDataFrame containing at least one LineString geometry.
107
+ project_gdf_attr : bool, default=True
108
+ If True, attributes from the input GeoDataFrame will be spatially projected
109
+ onto the resulting graph edges. This can be an expensive operation for large datasets.
110
+ reproject_to_utm_crs : bool, default=True
111
+ If True, reprojects the GeoDataFrame to the estimated local UTM CRS
112
+ to ensure accurate edge length calculations in meters.
113
+ If False, edge lengths are still computed in UTM CRS, but the final graph
114
+ will remain in the original CRS of the input GeoDataFrame.
115
+ speed : float, default=5
116
+ Assumed travel speed in km/h used to compute edge traversal time (in minutes).
117
+ check_intersections : bool, default=True
118
+ If True, merges geometries to ensure topological correctness.
119
+ Can be disabled if the input geometries already form a proper planar graph
120
+ with no unintended intersections.
121
+
122
+ Returns
123
+ -------
124
+ nx.DiGraph
125
+ A directed graph where each edge corresponds to a line segment from the input GeoDataFrame.
126
+ Edge attributes include geometry, length in meters, travel time (in minutes), and any
127
+ additional projected attributes from the original GeoDataFrame.
128
+
129
+ Raises
130
+ ------
131
+ ValueError
132
+ If the input GeoDataFrame contains no valid LineStrings.
133
+ """
134
+
135
+ def unique_list(agg_vals):
136
+ agg_vals = list(set(agg_vals.dropna()))
137
+ if len(agg_vals) == 1:
138
+ return agg_vals[0]
139
+ return agg_vals
140
+
141
+ original_crs = gdf.crs
142
+ gdf = gdf.to_crs(gdf.estimate_utm_crs())
143
+
144
+ gdf = gdf.explode(ignore_index=True)
145
+ gdf = gdf[gdf.geom_type == "LineString"]
146
+
147
+ if len(gdf) == 0:
148
+ raise ValueError("Provided GeoDataFrame contains no valid LineStrings")
149
+
150
+ if check_intersections:
151
+ lines = line_merge(node(MultiLineString(gdf.geometry.to_list())))
152
+ else:
153
+ lines = line_merge(MultiLineString(gdf.geometry.to_list()))
154
+
155
+ lines = gpd.GeoDataFrame(geometry=list(lines.geoms), crs=gdf.crs)
156
+
157
+ if len(gdf.columns) > 1 and project_gdf_attr:
158
+ lines_centroids = lines.copy()
159
+ lines_centroids.geometry = lines_centroids.apply(
160
+ lambda row: row.geometry.line_interpolate_point(row.geometry.length / 2), axis=1
161
+ ).buffer(0.05, resolution=2)
162
+ lines_with_attrs = gpd.sjoin(lines_centroids, gdf, how="left", predicate="intersects")
163
+ aggregated_attrs = (
164
+ lines_with_attrs.drop(columns=["geometry", "index_right"]) # убрать геометрию буфера
165
+ .groupby(lines_with_attrs.index)
166
+ .agg(unique_list)
167
+ )
168
+ lines = pd.concat([lines, aggregated_attrs], axis=1)
169
+
170
+ lines["length_meter"] = np.round(lines.length, 2)
171
+ if not reproject_to_utm_crs:
172
+ lines = lines.to_crs(original_crs)
173
+
174
+ coords = lines.geometry.get_coordinates()
175
+ coords_grouped_by_index = coords.reset_index(names="old_index").groupby("old_index")
176
+ start_coords = coords_grouped_by_index.head(1).apply(lambda a: (a.x, a.y), axis=1).rename("start")
177
+ end_coords = coords_grouped_by_index.tail(1).apply(lambda a: (a.x, a.y), axis=1).rename("end")
178
+ coords = pd.concat([start_coords.reset_index(), end_coords.reset_index()], axis=1)[["start", "end"]]
179
+ lines = pd.concat([lines, coords], axis=1)
180
+ unique_coords = pd.concat([coords["start"], coords["end"]], ignore_index=True).unique()
181
+ coord_to_index = {coord: idx for idx, coord in enumerate(unique_coords)}
182
+
183
+ lines["u"] = lines["start"].map(coord_to_index)
184
+ lines["v"] = lines["end"].map(coord_to_index)
185
+
186
+ speed = speed * 1000 / 60
187
+ lines["time_min"] = np.round(lines["length_meter"] / speed, 2)
188
+
189
+ graph = nx.Graph()
190
+ for coords, node_id in coord_to_index.items():
191
+ x, y = coords
192
+ graph.add_node(node_id, x=float(x), y=float(y))
193
+
194
+ columns_to_attr = lines.columns.difference(["start", "end", "u", "v"])
195
+ for _, row in lines.iterrows():
196
+ edge_attrs = {}
197
+ for col in columns_to_attr:
198
+ edge_attrs[col] = row[col]
199
+ graph.add_edge(row.u, row.v, **edge_attrs)
200
+
201
+ graph.graph["crs"] = lines.crs
202
+ graph.graph["speed m/min"] = speed
203
+ return nx.DiGraph(graph)
204
+
205
+
92
206
  def get_closest_nodes_from_gdf(gdf: gpd.GeoDataFrame, nx_graph: nx.Graph) -> tuple:
207
+ """
208
+ Finds the closest graph nodes to the geometries in a GeoDataFrame.
209
+
210
+ Parameters
211
+ ----------
212
+ gdf : gpd.GeoDataFrame
213
+ GeoDataFrame with geometries for which the nearest graph nodes will be found.
214
+ nx_graph : nx.Graph
215
+ A NetworkX graph where nodes have 'x' and 'y' attributes (coordinates).
216
+
217
+ Returns
218
+ -------
219
+ tuple
220
+ A tuple of (distances, nearest_nodes), where:
221
+ - distances: List of distances from each geometry to the nearest node.
222
+ - nearest_nodes: List of node IDs closest to each geometry in the input GeoDataFrame.
223
+
224
+ Raises
225
+ ------
226
+ ValueError
227
+ If any node in the graph is missing 'x' or 'y' attributes.
228
+ """
93
229
  nodes_with_data = list(nx_graph.nodes(data=True))
94
230
  try:
95
231
  coordinates = np.array([(data["x"], data["y"]) for node, data in nodes_with_data])
@@ -103,6 +239,24 @@ def get_closest_nodes_from_gdf(gdf: gpd.GeoDataFrame, nx_graph: nx.Graph) -> tup
103
239
 
104
240
 
105
241
  def remove_weakly_connected_nodes(graph: nx.DiGraph) -> nx.DiGraph:
242
+ """
243
+ Removes all nodes that are not part of the largest strongly connected component in the graph.
244
+
245
+ Parameters
246
+ ----------
247
+ graph : nx.DiGraph
248
+ A directed NetworkX graph.
249
+
250
+ Returns
251
+ -------
252
+ nx.DiGraph
253
+ A new graph with only the largest strongly connected component retained.
254
+
255
+ Notes
256
+ -----
257
+ - Also logs a warning if multiple weakly connected components are detected.
258
+ - Logs the number of nodes removed and size of the remaining component.
259
+ """
106
260
  graph = graph.copy()
107
261
 
108
262
  weakly_connected_components = list(nx.weakly_connected_components(graph))
@@ -125,3 +279,42 @@ def remove_weakly_connected_nodes(graph: nx.DiGraph) -> nx.DiGraph:
125
279
  graph.remove_nodes_from(nodes_to_del)
126
280
 
127
281
  return graph
282
+
283
+
284
+ def reverse_graph(nx_graph: nx.Graph, weight: str) -> tuple[nx.Graph, nx.DiGraph]:
285
+ """
286
+ Generate a reversed version of a directed or weighted graph.
287
+
288
+ If the input graph is undirected, the original graph is returned as-is.
289
+ For directed graphs, the function returns a new graph with all edge directions reversed,
290
+ preserving the specified edge weight.
291
+
292
+ Parameters
293
+ ----------
294
+ nx_graph : nx.Graph
295
+ Input NetworkX graph (can be directed or undirected).
296
+ weight : str
297
+ Name of the edge attribute to use as weight in graph conversion.
298
+
299
+ Returns
300
+ -------
301
+ tuple[nx.Graph, nx.DiGraph]
302
+ A tuple containing:
303
+ - normalized_graph: Original graph with relabeled nodes (if needed)
304
+ - reversed_graph: Directed graph with reversed edges and preserved weights
305
+ """
306
+
307
+ if nx_graph.is_multigraph():
308
+ nx_graph = nx.DiGraph(nx_graph) if nx_graph.is_directed() else nx.Graph(nx_graph)
309
+ if not nx_graph.is_multigraph() and not nx_graph.is_directed():
310
+ return nx_graph, nx_graph
311
+
312
+ nx_graph = remove_weakly_connected_nodes(nx_graph)
313
+
314
+ mapping = {old_label: new_label for new_label, old_label in enumerate(nx_graph.nodes())}
315
+ nx_graph = nx.relabel_nodes(nx_graph, mapping)
316
+
317
+ sparse_matrix = nx.to_scipy_sparse_array(nx_graph, weight=weight)
318
+ transposed_matrix = sparse_matrix.transpose()
319
+ reversed_graph = nx.from_scipy_sparse_array(transposed_matrix, edge_attribute=weight, create_using=type(nx_graph))
320
+ return nx_graph, reversed_graph
@@ -87,9 +87,9 @@ def get_visibility_accurate(
87
87
  point_from = point_from.iloc[0].geometry
88
88
  else:
89
89
  obstacles = obstacles.copy()
90
-
90
+ if obstacles.contains(point_from).any():
91
+ return Polygon()
91
92
  obstacles.reset_index(inplace=True, drop=True)
92
-
93
93
  point_buffer = point_from.buffer(view_distance, resolution=32)
94
94
  allowed_geom_types = ["MultiPolygon", "Polygon", "LineString", "MultiLineString"]
95
95
  obstacles = obstacles[obstacles.geom_type.isin(allowed_geom_types)]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ObjectNat
3
- Version: 1.0.1
3
+ Version: 1.2.0
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,6 +12,7 @@ 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 (>=1.0.1,<2.0.0)
15
+ Requires-Dist: loguru (>=0.7.3,<0.8.0)
15
16
  Requires-Dist: networkx (>=3.4.2,<4.0.0)
16
17
  Requires-Dist: numpy (>=2.1.3,<3.0.0)
17
18
  Requires-Dist: pandarallel (>=1.6.5,<2.0.0)
@@ -44,21 +45,26 @@ Description-Content-Type: text/markdown
44
45
  - **Stepped isochrones**: show accessibility ranges divided into time intervals (e.g., 5, 10, 15 minutes).
45
46
 
46
47
  <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">
48
+ <img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/isochrone_ways_15_min.png" alt="isochrone_ways_15_min" width="300">
49
+ <img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/isochrone_radius_15_min.png" alt="isochrone_radius_15_min" width="300">
50
+ <img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/isochrone_3points_radius_8_min.png" alt="isochrone_3points_radius_8_min" width="300">
49
51
  </p>
50
52
  <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">
53
+ <img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/stepped_isochrone_ways_15_min.png" alt="stepped_isochrone_ways_15_min" width="300">
54
+ <img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/stepped_isochrone_radius_15_min.png" alt="stepped_isochrone_radius_15_min" width="300">
55
+ <img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/stepped_isochrone_separate_15_min.png" alt="stepped_isochrone_separate_15_min" width="300">
54
56
  </p>
55
57
 
58
+ 2. **[Coverage Zones](./examples/coverage_zones.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.
56
59
 
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
60
  <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">
61
+ <img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/coverage_zones_time_10min.png" alt="coverage_zones_time_10min" width="350">
62
+ <img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/coverage_zones_distance_600m.png" alt="coverage_zones_distance_600m" width="350">
63
+ <img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/coverage_zones_radius_distance_800m.png" alt="coverage_zones_distance_radius_voronoi" width="350">
64
+ </p>
65
+ <p align="center">
66
+ <img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/stepped_coverage_zones_separate.png" alt="stepped_coverage_zones_separate" width="350">
67
+ <img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/stepped_coverage_zones_voronoi.png" alt="stepped_coverage_zones_voronoi" width="350">
62
68
  </p>
63
69
 
64
70
  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)
@@ -69,9 +75,9 @@ Description-Content-Type: text/markdown
69
75
  - **Clipping** of provision results to a custom analysis area (e.g., administrative boundaries).
70
76
 
71
77
  <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">
78
+ <img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/service_provision_initial.png" alt="service_provision_initial" width="300">
79
+ <img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/service_provision_recalculated.png" alt="service_provision_recalculated" width="300">
80
+ <img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/service_provision_clipped.png" alt="service_provision_clipped" width="300">
75
81
  </p>
76
82
 
77
83
  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.
@@ -84,7 +90,7 @@ Description-Content-Type: text/markdown
84
90
  - A **accurate method** for detailed local analysis.
85
91
 
86
92
  <p align="center">
87
- <img src="https://github.com/user-attachments/assets/aa139d29-07d4-4560-b835-9646c8802fe1" alt="visibility_comparison_methods" height="250">
93
+ <img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/visibility_comparison_methods.png" alt="visibility_comparison_methods" height="250">
88
94
  <img src="https://github.com/user-attachments/assets/b5b0d4b3-a02f-4ade-8772-475703cd6435" alt="visibility-catchment-area" height="250">
89
95
  </p>
90
96
 
@@ -93,7 +99,7 @@ Description-Content-Type: text/markdown
93
99
  🔗 **[See detailed explanation in the Wiki](https://github.com/DDonnyy/ObjectNat/wiki/Noise-simulation)**
94
100
 
95
101
  <p align="center">
96
- <img src="https://github.com/user-attachments/assets/b3a41962-6220-49c4-90d4-2e756f9706cf" alt="noise_simulation_test_result" width="400">
102
+ <img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/noise_simulation_1point.png" alt="noise_simulation_1point" width="400">
97
103
  </p>
98
104
 
99
105
 
@@ -104,7 +110,7 @@ Description-Content-Type: text/markdown
104
110
  Additionally, the function can calculate the **relative ratio** of different service types within each cluster, enabling spatial analysis of service composition.
105
111
 
106
112
  <p align="center">
107
- <img src="https://github.com/user-attachments/assets/f86aac61-497a-4330-b4cf-68f4fc47fd34" alt="building_clusters" width="400">
113
+ <img src="https://raw.githubusercontent.com/DDonnyy/ObjectNat/assets/building_clusters.png" alt="building_clusters" width="400">
108
114
  </p>
109
115
 
110
116
  ## City graphs
@@ -0,0 +1,34 @@
1
+ objectnat/__init__.py,sha256=OnDvrLPLEeYIE_9qOVYgMc-PkRzIqShtGxirguEXiRU,260
2
+ objectnat/_api.py,sha256=0R1nypAQUcbQ9YSLw_MUgUWoNl8c1zMZteV8wGzdkvc,711
3
+ objectnat/_config.py,sha256=fGPsMZqA8FVBBOINxRiTFkOOZsNLyablM5G0tdKeQB4,1306
4
+ objectnat/_version.py,sha256=viNrPDalxthAcGDGV3QUhm0tvEB8vd-Uh-27v9_MV7c,18
5
+ objectnat/methods/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ objectnat/methods/coverage_zones/__init__.py,sha256=3uTDC1xf3zgQRqSQR4URp__HjZ8eVUtjK8r3mGq-zuQ,161
7
+ objectnat/methods/coverage_zones/graph_coverage.py,sha256=iYI2huF9jLPPlISeh7R05ZaaWZ7s8ZBPGIk1WUeM-EI,4355
8
+ objectnat/methods/coverage_zones/radius_voronoi_coverage.py,sha256=H4_lvSc770TO6-xq6EzgWuUf00N5vYCMO3X6UJhZYAQ,1735
9
+ objectnat/methods/coverage_zones/stepped_coverage.py,sha256=dgNDHh2nG2zXcLHOIJZyZGVgcKnoxtu5iP1_aHdOYNM,5951
10
+ objectnat/methods/isochrones/__init__.py,sha256=bDfUZPbS3_PuTEB2QcRTYjvyJtUvjbDhAw6QJvD_ih4,90
11
+ objectnat/methods/isochrones/isochrone_utils.py,sha256=dJwvoGXUypwU2_oF-rdxNZm90gOi-RUp_0WM1C2HaPU,6870
12
+ objectnat/methods/isochrones/isochrones.py,sha256=TceEQ8xJ-KZR29fAZ1v9gLl42iN2vvDmoOIi1VkvQd4,12625
13
+ objectnat/methods/noise/__init__.py,sha256=Z5hdRiiv7tDgRAyrhwrTWkonI5dyLU3vfxHl-qdzw9A,233
14
+ objectnat/methods/noise/noise_exceptions.py,sha256=nTav5kp6RNpi0kxD9cMULTApOuvAu9wEiX28fkLAnOc,634
15
+ objectnat/methods/noise/noise_init_data.py,sha256=Vp-R_yH7CgYqZEtbGAdr1iiIbgauReniLQ_a2TcszhY,503
16
+ objectnat/methods/noise/noise_reduce.py,sha256=B85ifAN_mHiBKJso-cZiSkj7588w2sA-ugGvEal4CBw,6885
17
+ objectnat/methods/noise/noise_simulation.py,sha256=1hONZuYobyI9mVzkjVXost8XzEsZS8un2v7VTQobXQk,21680
18
+ objectnat/methods/noise/noise_simulation_simplified.py,sha256=_2wZ3nb-w-77aZiida0dOTGS4Yuq4PnRKmXi9xQEhYU,6135
19
+ objectnat/methods/point_clustering/__init__.py,sha256=pX2qDUCvs9LJI36mr65vbdRml6AE8hIYYxIJLdQZQxs,61
20
+ objectnat/methods/point_clustering/cluster_points_in_polygons.py,sha256=kwiZHY3TCUCE-nN5IdhCwDESWJvSCZUfrUJU3yC1csc,5042
21
+ objectnat/methods/provision/__init__.py,sha256=0Uy66n2xH0Y45JyhIYHEVfC2rig6bMYp6PV2KkNhbK8,80
22
+ objectnat/methods/provision/provision.py,sha256=g00mN6RGFZcLg0PIMgywnO3TWiFqx3sTYqTeG3Kl33s,4749
23
+ objectnat/methods/provision/provision_exceptions.py,sha256=lznEmlmZDzGIOtapZVqZDMutIi5eGbFuVCYeVa7VZWk,1687
24
+ objectnat/methods/provision/provision_model.py,sha256=_IKS3pe_i7gLcA56baB0g3mH77T9pPP_4FzjuK_FZ5Y,14529
25
+ objectnat/methods/utils/__init__.py,sha256=sGXy4KUOe5I0UYztnB4rIl2HNd-oqnqRYrBsiU-dpNY,52
26
+ objectnat/methods/utils/geom_utils.py,sha256=PdUjQDZ8drJ1ZFCFabDJnD6oFvgHeAQrsbeivuDOdcI,6385
27
+ objectnat/methods/utils/graph_utils.py,sha256=oIKG7XqVRkXE-QQaReXxClU8aHSKoAdwj_3g-ectUgo,12487
28
+ objectnat/methods/utils/math_utils.py,sha256=Vc8U15LtFOwgIt1YSOSKWYOIiW_1XLuMGOa6ejBpEUk,839
29
+ objectnat/methods/visibility/__init__.py,sha256=Mx1kaoV-yfQUxlMkgNF4AhjSweFEJMEx3NBis5OM3mA,161
30
+ objectnat/methods/visibility/visibility_analysis.py,sha256=J-GtggH12DoZaquZuDR7edDsXtCqxI3qKSRpT2ULPGs,21251
31
+ objectnat-1.2.0.dist-info/LICENSE.txt,sha256=yPEioMfTd7JAQgAU6J13inS1BSjwd82HFlRSoIb4My8,1498
32
+ objectnat-1.2.0.dist-info/METADATA,sha256=SerHhzYYX6o31ph9Yqm7MxxozcQ4Yxhcmzt9kKyvbI0,8881
33
+ objectnat-1.2.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
34
+ objectnat-1.2.0.dist-info/RECORD,,
@@ -1,32 +0,0 @@
1
- objectnat/__init__.py,sha256=OnDvrLPLEeYIE_9qOVYgMc-PkRzIqShtGxirguEXiRU,260
2
- objectnat/_api.py,sha256=5TvsMjWjiR7kFIWnfRJxngWOrC_eKQ7Pt-TSMpjqzI0,595
3
- objectnat/_config.py,sha256=fGPsMZqA8FVBBOINxRiTFkOOZsNLyablM5G0tdKeQB4,1306
4
- objectnat/_version.py,sha256=9C8U_kMX0x98w3CUW1G2vm12pYBsE-TE72pXihbWYqU,18
5
- objectnat/methods/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- objectnat/methods/coverage_zones/__init__.py,sha256=cdXjW041ysfIlr4sGQ_Xa2nok4tHfMJTEf8pem-0m-U,95
7
- objectnat/methods/coverage_zones/graph_coverage.py,sha256=3nVZekDAmbz2ZzxsayX9KIgudlSZF3OnZ9g5kFmVt8o,4797
8
- objectnat/methods/coverage_zones/radius_voronoi.py,sha256=H4_lvSc770TO6-xq6EzgWuUf00N5vYCMO3X6UJhZYAQ,1735
9
- objectnat/methods/isochrones/__init__.py,sha256=bDfUZPbS3_PuTEB2QcRTYjvyJtUvjbDhAw6QJvD_ih4,90
10
- objectnat/methods/isochrones/isochrone_utils.py,sha256=XdgjmGgeNfgXa3ERkkGEEzrzF5TTkrI6U8hxFAFHRkg,4861
11
- objectnat/methods/isochrones/isochrones.py,sha256=GoaFGiLSsG-RRXA5jtij3zsrwxH5ThKDh8XexZ3rD3k,14046
12
- objectnat/methods/noise/__init__.py,sha256=6JV586JdOqE0OUFUyxsrCoGuDxRuB-6ItSA5CRuFdsE,152
13
- objectnat/methods/noise/noise_exceptions.py,sha256=nTav5kp6RNpi0kxD9cMULTApOuvAu9wEiX28fkLAnOc,634
14
- objectnat/methods/noise/noise_init_data.py,sha256=Vp-R_yH7CgYqZEtbGAdr1iiIbgauReniLQ_a2TcszhY,503
15
- objectnat/methods/noise/noise_reduce.py,sha256=B85ifAN_mHiBKJso-cZiSkj7588w2sA-ugGvEal4CBw,6885
16
- objectnat/methods/noise/noise_sim.py,sha256=qb0C2CD-sIEcdUWO3xSwCGjuR94xPgU81FC94TeXHBo,20278
17
- objectnat/methods/point_clustering/__init__.py,sha256=pX2qDUCvs9LJI36mr65vbdRml6AE8hIYYxIJLdQZQxs,61
18
- objectnat/methods/point_clustering/cluster_points_in_polygons.py,sha256=kwiZHY3TCUCE-nN5IdhCwDESWJvSCZUfrUJU3yC1csc,5042
19
- objectnat/methods/provision/__init__.py,sha256=0Uy66n2xH0Y45JyhIYHEVfC2rig6bMYp6PV2KkNhbK8,80
20
- objectnat/methods/provision/provision.py,sha256=g00mN6RGFZcLg0PIMgywnO3TWiFqx3sTYqTeG3Kl33s,4749
21
- objectnat/methods/provision/provision_exceptions.py,sha256=lznEmlmZDzGIOtapZVqZDMutIi5eGbFuVCYeVa7VZWk,1687
22
- objectnat/methods/provision/provision_model.py,sha256=_IKS3pe_i7gLcA56baB0g3mH77T9pPP_4FzjuK_FZ5Y,14529
23
- objectnat/methods/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
- objectnat/methods/utils/geom_utils.py,sha256=6tt7gKztDYOTqaZ1sLO4vN2sNPV9qeFyKK1BbvwwD18,4582
25
- objectnat/methods/utils/graph_utils.py,sha256=M983Hy1CXRIGg_q71tW0HVB8ZS4ZpDK5I2J3imyznUQ,4799
26
- objectnat/methods/utils/math_utils.py,sha256=Vc8U15LtFOwgIt1YSOSKWYOIiW_1XLuMGOa6ejBpEUk,839
27
- objectnat/methods/visibility/__init__.py,sha256=Mx1kaoV-yfQUxlMkgNF4AhjSweFEJMEx3NBis5OM3mA,161
28
- objectnat/methods/visibility/visibility_analysis.py,sha256=fDIZ5S7oR-5ZEsjZPFexb7oFw-1X6uj0A4xuP3pX354,21183
29
- objectnat-1.0.1.dist-info/LICENSE.txt,sha256=yPEioMfTd7JAQgAU6J13inS1BSjwd82HFlRSoIb4My8,1498
30
- objectnat-1.0.1.dist-info/METADATA,sha256=2cgaqgB4Q4sXRbrBkVrzOKUIXEjIOejwS9f9kJIuxxE,8040
31
- objectnat-1.0.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
32
- objectnat-1.0.1.dist-info/RECORD,,