ObjectNat 1.0.0__py3-none-any.whl → 1.1.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,6 +1,6 @@
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
5
  from .methods.noise import simulate_noise
6
6
  from .methods.point_clustering import get_clusters_polygon
objectnat/_version.py CHANGED
@@ -1 +1 @@
1
- VERSION = "1.0.0"
1
+ VERSION = "1.1.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
@@ -74,11 +76,11 @@ def _process_pt_data(
74
76
  nodes: gpd.GeoDataFrame, edges: gpd.GeoDataFrame, graph_type: str
75
77
  ) -> tuple[gpd.GeoDataFrame, gpd.GeoDataFrame] | tuple[None, None]:
76
78
  """Process public transport data if available."""
77
- if "desc" in nodes.columns and "stop" in nodes["desc"].unique():
78
- pt_nodes = nodes[nodes["desc"] == "stop"]
79
+ if "type" in nodes.columns and "platform" in nodes["type"].unique():
80
+ pt_nodes = nodes[(nodes["type"] != "platform") & (~nodes["type"].isna())]
79
81
  if graph_type == "intermodal":
80
82
  edges = edges[~edges["type"].isin(["walk", "boarding"])]
81
- pt_nodes = pt_nodes[["desc", "route", "geometry"]]
83
+ pt_nodes = pt_nodes[["type", "route", "geometry"]]
82
84
  edges = edges[["type", "route", "geometry"]]
83
85
  return pt_nodes, edges
84
86
  return None, None
@@ -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(
@@ -22,26 +22,33 @@ from objectnat.methods.visibility.visibility_analysis import get_visibility_accu
22
22
 
23
23
  logger = config.logger
24
24
 
25
+ MAX_DB_VALUE = 194
26
+
25
27
 
26
28
  def simulate_noise(
27
- source_points: gpd.GeoDataFrame, obstacles: gpd.GeoDataFrame, source_noise_db, geometric_mean_freq_hz, **kwargs
29
+ source_points: gpd.GeoDataFrame,
30
+ obstacles: gpd.GeoDataFrame,
31
+ source_noise_db: float = None,
32
+ geometric_mean_freq_hz: float = None,
33
+ **kwargs,
28
34
  ):
29
35
  """
30
36
  Simulates noise propagation from a set of source points considering obstacles, trees, and environmental factors.
31
37
 
32
38
  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.
39
+ source_points (gpd.GeoDataFrame): A GeoDataFrame with one or more point geometries representing noise sources.
40
+ Optionally, it can include 'source_noise_db' and 'geometric_mean_freq_hz' columns for per-point simulation.
35
41
  obstacles (gpd.GeoDataFrame): A GeoDataFrame representing obstacles in the environment. If a column with
36
42
  sound absorption coefficients is present, its name should be provided in the `absorb_ratio_column` argument.
37
43
  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.
44
+ source_noise_db (float, optional): Default noise level (dB) to use if not specified per-point. Decibels are
45
+ logarithmic units used to measure sound intensity. A value of 20 dB represents a barely audible whisper,
46
+ while 140 dB is comparable to the noise of jet engines.
47
+ geometric_mean_freq_hz (float, optional): Default frequency (Hz) to use if not specified per-point.
48
+ This parameter influences the sound wave's propagation and scattering in the presence of trees.
49
+ Lower frequencies travel longer distances than higher frequencies.
50
+ It's recommended to use values between 63 Hz and 8000 Hz; values outside this range will be clamped to the
51
+ nearest boundary for the sound absorption coefficient calculation.
45
52
 
46
53
  Optional kwargs:
47
54
  absorb_ratio_column (str, optional): The name of the column in the `obstacles` GeoDataFrame that contains the
@@ -88,12 +95,40 @@ def simulate_noise(
88
95
  reflection_n = kwargs.get("reflection_n", 3)
89
96
  dead_area_r = kwargs.get("dead_area_r", 5)
90
97
 
98
+ # Validate optional columns or default values
99
+ use_column_db = False
100
+ if "source_noise_db" in source_points.columns:
101
+ if (source_points["source_noise_db"] > MAX_DB_VALUE).any():
102
+ raise ValueError(
103
+ f"One or more values in 'source_noise_db' column exceed the physical limit of {MAX_DB_VALUE} dB."
104
+ )
105
+ use_column_db = True
106
+
107
+ use_column_freq = "geometric_mean_freq_hz" in source_points.columns
108
+
109
+ if not use_column_db:
110
+ if source_noise_db is None:
111
+ raise ValueError(
112
+ "Either `source_noise_db` must be provided or the `source_points` must contain a 'source_noise_db' column."
113
+ )
114
+ if source_noise_db > MAX_DB_VALUE:
115
+ raise ValueError(
116
+ f"source_noise_db ({source_noise_db} dB) exceeds the physical limit of {MAX_DB_VALUE} dB in air."
117
+ )
118
+
119
+ if not use_column_freq:
120
+ if geometric_mean_freq_hz is None:
121
+ raise ValueError(
122
+ "Either `geometric_mean_freq_hz` must be provided or the `source_points` must contain a 'geometric_mean_freq_hz' column."
123
+ )
124
+ if not use_column_db and not use_column_freq and len(source_points) > 1:
125
+ logger.warning(
126
+ "`source_noise_db` and `geometric_mean_freq_hz` will be used for all points. Per-point simulation parameters not found."
127
+ )
128
+
91
129
  original_crs = source_points.crs
130
+ source_points = source_points.copy()
92
131
 
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
132
  source_points = source_points.copy()
98
133
  if len(obstacles) > 0:
99
134
  obstacles = obstacles.copy()
@@ -118,63 +153,54 @@ def simulate_noise(
118
153
  if absorb_ratio_column is None:
119
154
  obstacles["absorb_ratio"] = standart_absorb_ratio
120
155
  else:
121
- obstacles["absorb_ratio"] = obstacles[absorb_ratio_column]
122
- obstacles["absorb_ratio"] = obstacles["absorb_ratio"].fillna(standart_absorb_ratio)
156
+ obstacles["absorb_ratio"] = obstacles[absorb_ratio_column].fillna(standart_absorb_ratio)
123
157
  obstacles = obstacles[["absorb_ratio", "geometry"]]
124
158
 
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
159
  # creating initial task and simulating for each point
144
- all_p_res = []
160
+ task_queue = multiprocessing.Queue()
161
+ dead_area_dict = {}
145
162
  for ind, row in source_points.iterrows():
146
- logger.info(f"Started simulation for point {ind+1} / {len(source_points)}")
147
163
  source_point = row.geometry
148
- task_queue = multiprocessing.Queue()
164
+ local_db = row["source_noise_db"] if use_column_db else source_noise_db
165
+ local_freq = row["geometric_mean_freq_hz"] if use_column_freq else geometric_mean_freq_hz
166
+ div_ = (local_db - target_noise_db) % db_sim_step
167
+ if div_ != 0:
168
+ raise InvalidStepError(local_db, target_noise_db, db_sim_step, div_)
169
+ # calculating layer dist and db values
170
+ dist_db = [(0, local_db)]
171
+ cur_db = local_db - db_sim_step
172
+ while cur_db != target_noise_db - db_sim_step:
173
+ max_dist = dist_to_target_db(local_db, cur_db, local_freq, air_temperature)
174
+ dist_db.append((max_dist, cur_db))
175
+ cur_db -= db_sim_step
176
+
149
177
  args = (source_point, obstacles, trees, 0, 0, dist_db)
150
178
  kwargs = {
151
179
  "reflection_n": reflection_n,
152
- "geometric_mean_freq_hz": geometric_mean_freq_hz,
180
+ "geometric_mean_freq_hz": local_freq,
153
181
  "tree_res": tree_res,
154
182
  "min_db": target_noise_db,
183
+ "simulation_ind": ind,
155
184
  }
156
185
  task_queue.put((_noise_from_point_task, args, kwargs))
186
+ dead_area_dict[ind] = source_point.buffer(dead_area_r, resolution=2)
157
187
 
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
- )
188
+ noise_gdf = _parallel_split_queue(task_queue, dead_area_dict=dead_area_dict, dead_area_r=dead_area_r)
161
189
 
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)
190
+ noise_gdf = gpd.GeoDataFrame(pd.concat(noise_gdf, ignore_index=True), crs=local_crs)
191
+ polygons = gpd.GeoDataFrame(
192
+ geometry=list(polygonize(noise_gdf.geometry.apply(polygons_to_multilinestring).union_all())), crs=local_crs
193
+ )
194
+ polygons_points = polygons.copy()
195
+ polygons_points.geometry = polygons.representative_point()
196
+ sim_result = polygons_points.sjoin(noise_gdf, predicate="within").reset_index()
197
+ sim_result = sim_result.groupby("index").agg({"noise_level": "max"})
198
+ sim_result["geometry"] = polygons
199
+ sim_result = (
200
+ gpd.GeoDataFrame(sim_result, geometry="geometry", crs=local_crs).dissolve(by="noise_level").reset_index()
201
+ )
176
202
 
177
- return gpd.GeoDataFrame(pd.concat(all_p_res, ignore_index=True), crs=local_crs).to_crs(original_crs)
203
+ return sim_result.to_crs(original_crs)
178
204
 
179
205
 
180
206
  def _noise_from_point_task(task, **kwargs) -> tuple[gpd.GeoDataFrame, list[tuple] | None]: # pragma: no cover
@@ -383,34 +409,34 @@ def _noise_from_point_task(task, **kwargs) -> tuple[gpd.GeoDataFrame, list[tuple
383
409
  return noise_from_point, new_tasks
384
410
 
385
411
 
386
- def _parallel_split_queue(task_queue: multiprocessing.Queue, dead_area: Polygon, dead_area_r: int):
412
+ def _parallel_split_queue(task_queue: multiprocessing.Queue, dead_area_dict: dict, dead_area_r: int):
387
413
  results = []
388
414
  total_tasks = task_queue.qsize()
389
415
 
390
416
  with tqdm(total=total_tasks, desc="Simulating noise") as pbar:
391
417
  with concurrent.futures.ProcessPoolExecutor() as executor:
418
+ # with concurrent.futures.ThreadPoolExecutor() as executor:
392
419
  future_to_task = {}
393
420
  while True:
394
421
  while not task_queue.empty() and len(future_to_task) < executor._max_workers:
395
422
  func, task, kwargs = task_queue.get_nowait()
396
423
  future = executor.submit(func, task, **kwargs)
397
- future_to_task[future] = task
398
-
424
+ future_to_task[future] = kwargs["simulation_ind"]
399
425
  done, _ = concurrent.futures.wait(future_to_task.keys(), return_when=concurrent.futures.FIRST_COMPLETED)
400
-
401
426
  for future in done:
402
- future_to_task.pop(future)
427
+ simulation_ind = future_to_task.pop(future)
403
428
  result, new_tasks = future.result()
404
429
  if new_tasks:
405
430
  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)
431
+ local_dead_area = dead_area_dict.get(simulation_ind)
432
+ new_dead_area_points = [local_dead_area]
433
+ for func, new_task, new_kwargs in new_tasks:
434
+ new_point = new_task[0]
435
+ if not local_dead_area.covers(new_point):
436
+ task_queue.put((func, new_task, new_kwargs))
437
+ new_dead_area_points.append(new_point.buffer(dead_area_r, resolution=2))
438
+ new_tasks_n += 1
439
+ dead_area_dict[simulation_ind] = unary_union(new_dead_area_points)
414
440
  total_tasks += new_tasks_n
415
441
  pbar.total = total_tasks
416
442
  pbar.refresh()
@@ -419,5 +445,4 @@ def _parallel_split_queue(task_queue: multiprocessing.Queue, dead_area: Polygon,
419
445
  time.sleep(0.01)
420
446
  if not future_to_task and task_queue.empty():
421
447
  break
422
-
423
448
  return results
@@ -90,6 +90,28 @@ def graph_to_gdf(
90
90
 
91
91
 
92
92
  def get_closest_nodes_from_gdf(gdf: gpd.GeoDataFrame, nx_graph: nx.Graph) -> tuple:
93
+ """
94
+ Finds the closest graph nodes to the geometries in a GeoDataFrame.
95
+
96
+ Parameters
97
+ ----------
98
+ gdf : gpd.GeoDataFrame
99
+ GeoDataFrame with geometries for which the nearest graph nodes will be found.
100
+ nx_graph : nx.Graph
101
+ A NetworkX graph where nodes have 'x' and 'y' attributes (coordinates).
102
+
103
+ Returns
104
+ -------
105
+ tuple
106
+ A tuple of (distances, nearest_nodes), where:
107
+ - distances: List of distances from each geometry to the nearest node.
108
+ - nearest_nodes: List of node IDs closest to each geometry in the input GeoDataFrame.
109
+
110
+ Raises
111
+ ------
112
+ ValueError
113
+ If any node in the graph is missing 'x' or 'y' attributes.
114
+ """
93
115
  nodes_with_data = list(nx_graph.nodes(data=True))
94
116
  try:
95
117
  coordinates = np.array([(data["x"], data["y"]) for node, data in nodes_with_data])
@@ -103,6 +125,24 @@ def get_closest_nodes_from_gdf(gdf: gpd.GeoDataFrame, nx_graph: nx.Graph) -> tup
103
125
 
104
126
 
105
127
  def remove_weakly_connected_nodes(graph: nx.DiGraph) -> nx.DiGraph:
128
+ """
129
+ Removes all nodes that are not part of the largest strongly connected component in the graph.
130
+
131
+ Parameters
132
+ ----------
133
+ graph : nx.DiGraph
134
+ A directed NetworkX graph.
135
+
136
+ Returns
137
+ -------
138
+ nx.DiGraph
139
+ A new graph with only the largest strongly connected component retained.
140
+
141
+ Notes
142
+ -----
143
+ - Also logs a warning if multiple weakly connected components are detected.
144
+ - Logs the number of nodes removed and size of the remaining component.
145
+ """
106
146
  graph = graph.copy()
107
147
 
108
148
  weakly_connected_components = list(nx.weakly_connected_components(graph))
@@ -125,3 +165,42 @@ def remove_weakly_connected_nodes(graph: nx.DiGraph) -> nx.DiGraph:
125
165
  graph.remove_nodes_from(nodes_to_del)
126
166
 
127
167
  return graph
168
+
169
+
170
+ def reverse_graph(nx_graph: nx.Graph, weight: str) -> tuple[nx.Graph, nx.DiGraph]:
171
+ """
172
+ Generate a reversed version of a directed or weighted graph.
173
+
174
+ If the input graph is undirected, the original graph is returned as-is.
175
+ For directed graphs, the function returns a new graph with all edge directions reversed,
176
+ preserving the specified edge weight.
177
+
178
+ Parameters
179
+ ----------
180
+ nx_graph : nx.Graph
181
+ Input NetworkX graph (can be directed or undirected).
182
+ weight : str
183
+ Name of the edge attribute to use as weight in graph conversion.
184
+
185
+ Returns
186
+ -------
187
+ tuple[nx.Graph, nx.DiGraph]
188
+ A tuple containing:
189
+ - normalized_graph: Original graph with relabeled nodes (if needed)
190
+ - reversed_graph: Directed graph with reversed edges and preserved weights
191
+ """
192
+
193
+ if nx_graph.is_multigraph():
194
+ nx_graph = nx.DiGraph(nx_graph) if nx_graph.is_directed() else nx.Graph(nx_graph)
195
+ if not nx_graph.is_multigraph() and not nx_graph.is_directed():
196
+ return nx_graph, nx_graph
197
+
198
+ nx_graph = remove_weakly_connected_nodes(nx_graph)
199
+
200
+ mapping = {old_label: new_label for new_label, old_label in enumerate(nx_graph.nodes())}
201
+ nx_graph = nx.relabel_nodes(nx_graph, mapping)
202
+
203
+ sparse_matrix = nx.to_scipy_sparse_array(nx_graph, weight=weight)
204
+ transposed_matrix = sparse_matrix.transpose()
205
+ reversed_graph = nx.from_scipy_sparse_array(transposed_matrix, edge_attribute=weight, create_using=type(nx_graph))
206
+ return nx_graph, reversed_graph
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ObjectNat
3
- Version: 1.0.0
3
+ Version: 1.1.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,11 +12,11 @@ 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)
18
19
  Requires-Dist: pandas (>=2.2.0,<3.0.0)
19
- Requires-Dist: pytest (>=8.3.5,<9.0.0)
20
20
  Requires-Dist: scikit-learn (>=1.4.0,<2.0.0)
21
21
  Requires-Dist: tqdm (>=4.66.2,<5.0.0)
22
22
  Description-Content-Type: text/markdown
@@ -45,21 +45,26 @@ Description-Content-Type: text/markdown
45
45
  - **Stepped isochrones**: show accessibility ranges divided into time intervals (e.g., 5, 10, 15 minutes).
46
46
 
47
47
  <p align="center">
48
- <img src="https://github.com/user-attachments/assets/b1787430-63e1-4907-9198-a6171d546599" alt="isochrone_ways_15_min" width="300">
49
- <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">
50
51
  </p>
51
52
  <p align="center">
52
- <img src="https://github.com/user-attachments/assets/ac9f8840-a867-4eb5-aec8-91a411d4e545" alt="stepped_isochrone_stepped_ways_15_min" width="300">
53
- <img src="https://github.com/user-attachments/assets/b5429aa1-4625-44d1-982f-8bd4264148fb" alt="stepped_isochrone_stepped_radius_15_min" width="300">
54
- <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">
55
56
  </p>
56
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.
57
59
 
58
- 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.
59
-
60
60
  <p align="center">
61
- <img src="https://github.com/user-attachments/assets/fa8057d7-77aa-48a2-aa10-ea3e292a918d" alt="coverage_zones_time_10min" width="350">
62
- <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">
63
68
  </p>
64
69
 
65
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)
@@ -70,9 +75,9 @@ Description-Content-Type: text/markdown
70
75
  - **Clipping** of provision results to a custom analysis area (e.g., administrative boundaries).
71
76
 
72
77
  <p align="center">
73
- <img src="https://github.com/user-attachments/assets/ff1ed08d-9a35-4035-9e1f-9a7fdae5b0e0" alt="service_provision_initial" width="300">
74
- <img src="https://github.com/user-attachments/assets/a0c0a6b0-f83f-4982-bfb3-4a476b2153ea" alt="service_provision_recalculated" width="300">
75
- <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">
76
81
  </p>
77
82
 
78
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.
@@ -85,7 +90,7 @@ Description-Content-Type: text/markdown
85
90
  - A **accurate method** for detailed local analysis.
86
91
 
87
92
  <p align="center">
88
- <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">
89
94
  <img src="https://github.com/user-attachments/assets/b5b0d4b3-a02f-4ade-8772-475703cd6435" alt="visibility-catchment-area" height="250">
90
95
  </p>
91
96
 
@@ -94,7 +99,7 @@ Description-Content-Type: text/markdown
94
99
  🔗 **[See detailed explanation in the Wiki](https://github.com/DDonnyy/ObjectNat/wiki/Noise-simulation)**
95
100
 
96
101
  <p align="center">
97
- <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">
98
103
  </p>
99
104
 
100
105
 
@@ -105,7 +110,7 @@ Description-Content-Type: text/markdown
105
110
  Additionally, the function can calculate the **relative ratio** of different service types within each cluster, enabling spatial analysis of service composition.
106
111
 
107
112
  <p align="center">
108
- <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">
109
114
  </p>
110
115
 
111
116
  ## City graphs
@@ -1,19 +1,20 @@
1
1
  objectnat/__init__.py,sha256=OnDvrLPLEeYIE_9qOVYgMc-PkRzIqShtGxirguEXiRU,260
2
- objectnat/_api.py,sha256=5TvsMjWjiR7kFIWnfRJxngWOrC_eKQ7Pt-TSMpjqzI0,595
2
+ objectnat/_api.py,sha256=aTUevxMYHWnRxQhoLNkV80OrlRQmtkW8d0bh6jURRa4,623
3
3
  objectnat/_config.py,sha256=fGPsMZqA8FVBBOINxRiTFkOOZsNLyablM5G0tdKeQB4,1306
4
- objectnat/_version.py,sha256=lzGFsymf0DtA_1oAZcPbeQ557iY-1BRkekhfA2qaFh8,18
4
+ objectnat/_version.py,sha256=DFGnvNkISEu0RDsAfc03O0dRz9PfmIb63Kyi-am-trE,18
5
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
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
9
10
  objectnat/methods/isochrones/__init__.py,sha256=bDfUZPbS3_PuTEB2QcRTYjvyJtUvjbDhAw6QJvD_ih4,90
10
- objectnat/methods/isochrones/isochrone_utils.py,sha256=KubqeysYetj1dbWJ_WGrewfM_jWYmRFjj8AWz8aEc1I,4825
11
- objectnat/methods/isochrones/isochrones.py,sha256=GoaFGiLSsG-RRXA5jtij3zsrwxH5ThKDh8XexZ3rD3k,14046
11
+ objectnat/methods/isochrones/isochrone_utils.py,sha256=dJwvoGXUypwU2_oF-rdxNZm90gOi-RUp_0WM1C2HaPU,6870
12
+ objectnat/methods/isochrones/isochrones.py,sha256=TceEQ8xJ-KZR29fAZ1v9gLl42iN2vvDmoOIi1VkvQd4,12625
12
13
  objectnat/methods/noise/__init__.py,sha256=6JV586JdOqE0OUFUyxsrCoGuDxRuB-6ItSA5CRuFdsE,152
13
14
  objectnat/methods/noise/noise_exceptions.py,sha256=nTav5kp6RNpi0kxD9cMULTApOuvAu9wEiX28fkLAnOc,634
14
15
  objectnat/methods/noise/noise_init_data.py,sha256=Vp-R_yH7CgYqZEtbGAdr1iiIbgauReniLQ_a2TcszhY,503
15
16
  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/noise/noise_sim.py,sha256=6R6TsrIkUWb5qglAWejR6yCbjqSHOydnKc0pblW9UNU,21716
17
18
  objectnat/methods/point_clustering/__init__.py,sha256=pX2qDUCvs9LJI36mr65vbdRml6AE8hIYYxIJLdQZQxs,61
18
19
  objectnat/methods/point_clustering/cluster_points_in_polygons.py,sha256=kwiZHY3TCUCE-nN5IdhCwDESWJvSCZUfrUJU3yC1csc,5042
19
20
  objectnat/methods/provision/__init__.py,sha256=0Uy66n2xH0Y45JyhIYHEVfC2rig6bMYp6PV2KkNhbK8,80
@@ -22,11 +23,11 @@ objectnat/methods/provision/provision_exceptions.py,sha256=lznEmlmZDzGIOtapZVqZD
22
23
  objectnat/methods/provision/provision_model.py,sha256=_IKS3pe_i7gLcA56baB0g3mH77T9pPP_4FzjuK_FZ5Y,14529
23
24
  objectnat/methods/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
25
  objectnat/methods/utils/geom_utils.py,sha256=6tt7gKztDYOTqaZ1sLO4vN2sNPV9qeFyKK1BbvwwD18,4582
25
- objectnat/methods/utils/graph_utils.py,sha256=M983Hy1CXRIGg_q71tW0HVB8ZS4ZpDK5I2J3imyznUQ,4799
26
+ objectnat/methods/utils/graph_utils.py,sha256=-72BWU7gi-5scOKOjx94a7Aseum6gLvznborP7rYkLw,7534
26
27
  objectnat/methods/utils/math_utils.py,sha256=Vc8U15LtFOwgIt1YSOSKWYOIiW_1XLuMGOa6ejBpEUk,839
27
28
  objectnat/methods/visibility/__init__.py,sha256=Mx1kaoV-yfQUxlMkgNF4AhjSweFEJMEx3NBis5OM3mA,161
28
29
  objectnat/methods/visibility/visibility_analysis.py,sha256=fDIZ5S7oR-5ZEsjZPFexb7oFw-1X6uj0A4xuP3pX354,21183
29
- objectnat-1.0.0.dist-info/LICENSE.txt,sha256=yPEioMfTd7JAQgAU6J13inS1BSjwd82HFlRSoIb4My8,1498
30
- objectnat-1.0.0.dist-info/METADATA,sha256=ybm0Wwz002c0AWq9qJw6g6ZTveQB4yZJLekYC3kB70I,8079
31
- objectnat-1.0.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
32
- objectnat-1.0.0.dist-info/RECORD,,
30
+ objectnat-1.1.0.dist-info/LICENSE.txt,sha256=yPEioMfTd7JAQgAU6J13inS1BSjwd82HFlRSoIb4My8,1498
31
+ objectnat-1.1.0.dist-info/METADATA,sha256=qJyn7mHu--UnDLqGd8WOUV9xqEA573hWub5FsHjNB_Y,8881
32
+ objectnat-1.1.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
33
+ objectnat-1.1.0.dist-info/RECORD,,