ObjectNat 0.2.6__py3-none-any.whl → 1.0.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 +6 -8
- objectnat/_config.py +0 -24
- objectnat/_version.py +1 -1
- objectnat/methods/coverage_zones/__init__.py +2 -0
- objectnat/methods/coverage_zones/graph_coverage.py +118 -0
- objectnat/methods/coverage_zones/radius_voronoi.py +45 -0
- objectnat/methods/isochrones/__init__.py +1 -0
- objectnat/methods/isochrones/isochrone_utils.py +130 -0
- objectnat/methods/isochrones/isochrones.py +325 -0
- objectnat/methods/noise/__init__.py +3 -0
- objectnat/methods/noise/noise_exceptions.py +14 -0
- objectnat/methods/noise/noise_init_data.py +10 -0
- objectnat/methods/noise/noise_reduce.py +155 -0
- objectnat/methods/noise/noise_sim.py +423 -0
- objectnat/methods/point_clustering/__init__.py +1 -0
- objectnat/methods/{cluster_points_in_polygons.py → point_clustering/cluster_points_in_polygons.py} +22 -28
- objectnat/methods/provision/__init__.py +1 -0
- objectnat/methods/provision/provision.py +10 -7
- objectnat/methods/provision/provision_exceptions.py +4 -4
- objectnat/methods/provision/provision_model.py +21 -20
- objectnat/methods/utils/__init__.py +0 -0
- objectnat/methods/utils/geom_utils.py +130 -0
- objectnat/methods/utils/graph_utils.py +127 -0
- objectnat/methods/utils/math_utils.py +32 -0
- objectnat/methods/visibility/__init__.py +6 -0
- objectnat/methods/{visibility_analysis.py → visibility/visibility_analysis.py} +222 -243
- objectnat-1.0.0.dist-info/METADATA +143 -0
- objectnat-1.0.0.dist-info/RECORD +32 -0
- objectnat/methods/balanced_buildings.py +0 -69
- objectnat/methods/coverage_zones.py +0 -90
- objectnat/methods/isochrones.py +0 -143
- objectnat/methods/living_buildings_osm.py +0 -168
- objectnat-0.2.6.dist-info/METADATA +0 -113
- objectnat-0.2.6.dist-info/RECORD +0 -19
- {objectnat-0.2.6.dist-info → objectnat-1.0.0.dist-info}/LICENSE.txt +0 -0
- {objectnat-0.2.6.dist-info → objectnat-1.0.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
import geopandas as gpd
|
|
4
|
+
import networkx as nx
|
|
5
|
+
import numpy as np
|
|
6
|
+
from shapely.ops import polygonize
|
|
7
|
+
|
|
8
|
+
from objectnat import config
|
|
9
|
+
from objectnat.methods.isochrones.isochrone_utils import (
|
|
10
|
+
_calculate_distance_matrix,
|
|
11
|
+
_create_isochrones_gdf,
|
|
12
|
+
_prepare_graph_and_nodes,
|
|
13
|
+
_process_pt_data,
|
|
14
|
+
_validate_inputs,
|
|
15
|
+
)
|
|
16
|
+
from objectnat.methods.utils.geom_utils import polygons_to_multilinestring, remove_inner_geom
|
|
17
|
+
from objectnat.methods.utils.graph_utils import graph_to_gdf
|
|
18
|
+
|
|
19
|
+
logger = config.logger
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_accessibility_isochrone_stepped(
|
|
23
|
+
isochrone_type: Literal["radius", "ways", "separate"],
|
|
24
|
+
point: gpd.GeoDataFrame,
|
|
25
|
+
weight_value: float,
|
|
26
|
+
weight_type: Literal["time_min", "length_meter"],
|
|
27
|
+
nx_graph: nx.Graph,
|
|
28
|
+
step: float = None,
|
|
29
|
+
**kwargs,
|
|
30
|
+
) -> tuple[gpd.GeoDataFrame, gpd.GeoDataFrame | None, gpd.GeoDataFrame | None]:
|
|
31
|
+
"""
|
|
32
|
+
Calculate stepped accessibility isochrones for a single point with specified intervals.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
isochrone_type : Literal["radius", "ways", "separate"]
|
|
37
|
+
Visualization method for stepped isochrones:
|
|
38
|
+
- "radius": Voronoi-based in circular buffers
|
|
39
|
+
- "ways": Voronoi-based in road network polygons
|
|
40
|
+
- "separate": Circular buffers for each step
|
|
41
|
+
point : gpd.GeoDataFrame
|
|
42
|
+
Single source point for isochrone calculation (uses first geometry if multiple provided).
|
|
43
|
+
weight_value : float
|
|
44
|
+
Maximum travel time (minutes) or distance (meters) threshold.
|
|
45
|
+
weight_type : Literal["time_min", "length_meter"]
|
|
46
|
+
Type of weight calculation:
|
|
47
|
+
- "time_min": Time-based in minutes
|
|
48
|
+
- "length_meter": Distance-based in meters
|
|
49
|
+
nx_graph : nx.Graph
|
|
50
|
+
NetworkX graph representing the transportation network.
|
|
51
|
+
step : float, optional
|
|
52
|
+
Interval between isochrone steps. Defaults to:
|
|
53
|
+
- 100 meters for distance-based
|
|
54
|
+
- 1 minute for time-based
|
|
55
|
+
**kwargs
|
|
56
|
+
Additional buffer parameters:
|
|
57
|
+
- buffer_factor: Size multiplier for buffers (default: 0.7)
|
|
58
|
+
- road_buffer_size: Buffer size for road edges in meters (default: 5)
|
|
59
|
+
|
|
60
|
+
Returns
|
|
61
|
+
-------
|
|
62
|
+
tuple[gpd.GeoDataFrame, gpd.GeoDataFrame | None, gpd.GeoDataFrame | None]
|
|
63
|
+
Tuple containing:
|
|
64
|
+
- stepped_isochrones: GeoDataFrame with stepped polygons and distance/time attributes
|
|
65
|
+
- pt_stops: Public transport stops within isochrones (if available)
|
|
66
|
+
- pt_routes: Public transport routes within isochrones (if available)
|
|
67
|
+
|
|
68
|
+
Examples
|
|
69
|
+
--------
|
|
70
|
+
>>> from iduedu import get_intermodal_graph # pip install iduedu to get OSM city network graph
|
|
71
|
+
>>> graph = get_intermodal_graph(polygon=my_territory_polygon)
|
|
72
|
+
>>> point = gpd.GeoDataFrame(geometry=[Point(30.33, 59.95)], crs=4326)
|
|
73
|
+
>>> # Stepped radius isochrones with 5-minute intervals
|
|
74
|
+
>>> radius_stepped, stops, _ = get_accessibility_isochrone_stepped(
|
|
75
|
+
... "radius", point, 30, "time_min", graph, step=5
|
|
76
|
+
... )
|
|
77
|
+
>>> # Stepped road isochrones with 200m intervals
|
|
78
|
+
>>> ways_stepped, _, routes = get_accessibility_isochrone_stepped(
|
|
79
|
+
... "ways", point, 1000, "length_meter", graph, step=200
|
|
80
|
+
... )
|
|
81
|
+
>>> # Voronoi-based stepped isochrones
|
|
82
|
+
>>> separate_stepped, stops, _ = get_accessibility_isochrone_stepped(
|
|
83
|
+
... "separate", point, 15, "time_min", graph
|
|
84
|
+
... )
|
|
85
|
+
"""
|
|
86
|
+
buffer_params = {
|
|
87
|
+
"buffer_factor": 0.7,
|
|
88
|
+
"road_buffer_size": 5,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
buffer_params.update(kwargs)
|
|
92
|
+
original_crs = point.crs
|
|
93
|
+
point = point.copy()
|
|
94
|
+
if len(point) > 1:
|
|
95
|
+
logger.warning(
|
|
96
|
+
f"This method processes only single point. The GeoDataFrame contains {len(point)} points - "
|
|
97
|
+
"only the first geometry will be used for isochrone calculation. "
|
|
98
|
+
)
|
|
99
|
+
point = point.iloc[[0]]
|
|
100
|
+
|
|
101
|
+
local_crs, graph_type = _validate_inputs(point, weight_value, weight_type, nx_graph)
|
|
102
|
+
|
|
103
|
+
if step is None:
|
|
104
|
+
if weight_type == "length_meter":
|
|
105
|
+
step = 100
|
|
106
|
+
else:
|
|
107
|
+
step = 1
|
|
108
|
+
nx_graph, points, dist_nearest, speed = _prepare_graph_and_nodes(
|
|
109
|
+
point, nx_graph, graph_type, weight_type, weight_value
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
dist_matrix, subgraph = _calculate_distance_matrix(
|
|
113
|
+
nx_graph, points["nearest_node"].values, weight_type, weight_value, dist_nearest
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
logger.info("Building isochrones geometry...")
|
|
117
|
+
nodes, edges = graph_to_gdf(subgraph)
|
|
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
|
+
|
|
123
|
+
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)
|
|
148
|
+
else:
|
|
149
|
+
if isochrone_type == "radius":
|
|
150
|
+
isochrone_geoms = _build_radius_isochrones(
|
|
151
|
+
dist_matrix, weight_value, weight_type, speed, nodes, buffer_params["buffer_factor"]
|
|
152
|
+
)
|
|
153
|
+
else: # isochrone_type == 'ways':
|
|
154
|
+
if graph_type in ["intermodal", "walk"]:
|
|
155
|
+
isochrone_edges = edges[edges["type"] == "walk"]
|
|
156
|
+
else:
|
|
157
|
+
isochrone_edges = edges.copy()
|
|
158
|
+
all_isochrones_edges = isochrone_edges.buffer(buffer_params["road_buffer_size"], resolution=1).union_all()
|
|
159
|
+
all_isochrones_edges = gpd.GeoDataFrame(geometry=[all_isochrones_edges], crs=local_crs)
|
|
160
|
+
isochrone_geoms = _build_ways_isochrones(
|
|
161
|
+
dist_matrix=dist_matrix,
|
|
162
|
+
weight_value=weight_value,
|
|
163
|
+
weight_type=weight_type,
|
|
164
|
+
speed=speed,
|
|
165
|
+
nodes=nodes,
|
|
166
|
+
all_isochrones_edges=all_isochrones_edges,
|
|
167
|
+
buffer_factor=buffer_params["buffer_factor"],
|
|
168
|
+
)
|
|
169
|
+
nodes = nodes.clip(isochrone_geoms[0], keep_geom_type=True)
|
|
170
|
+
nodes["dist"] = np.minimum(np.ceil(nodes["dist"] / step) * step, weight_value)
|
|
171
|
+
voronois = gpd.GeoDataFrame(geometry=nodes.voronoi_polygons(), crs=local_crs)
|
|
172
|
+
stepped_iso = (
|
|
173
|
+
voronois.sjoin(nodes[["dist", "geometry"]]).dissolve(by="dist", as_index=False).drop(columns="index_right")
|
|
174
|
+
)
|
|
175
|
+
stepped_iso = stepped_iso.clip(isochrone_geoms[0], keep_geom_type=True)
|
|
176
|
+
|
|
177
|
+
pt_nodes, pt_edges = _process_pt_data(nodes, edges, graph_type)
|
|
178
|
+
if pt_nodes is not None:
|
|
179
|
+
pt_nodes.to_crs(original_crs, inplace=True)
|
|
180
|
+
if pt_edges is not None:
|
|
181
|
+
pt_edges.to_crs(original_crs, inplace=True)
|
|
182
|
+
return stepped_iso.to_crs(original_crs), pt_nodes, pt_edges
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def get_accessibility_isochrones(
|
|
186
|
+
isochrone_type: Literal["radius", "ways"],
|
|
187
|
+
points: gpd.GeoDataFrame,
|
|
188
|
+
weight_value: float,
|
|
189
|
+
weight_type: Literal["time_min", "length_meter"],
|
|
190
|
+
nx_graph: nx.Graph,
|
|
191
|
+
**kwargs,
|
|
192
|
+
) -> tuple[gpd.GeoDataFrame, gpd.GeoDataFrame | None, gpd.GeoDataFrame | None]:
|
|
193
|
+
"""
|
|
194
|
+
Calculate accessibility isochrones from input points based on the provided city graph.
|
|
195
|
+
|
|
196
|
+
Supports two types of isochrones:
|
|
197
|
+
- 'radius': Circular buffer-based isochrones
|
|
198
|
+
- 'ways': Road network-based isochrones
|
|
199
|
+
|
|
200
|
+
Parameters
|
|
201
|
+
----------
|
|
202
|
+
isochrone_type : Literal["radius", "ways"]
|
|
203
|
+
Type of isochrone to calculate:
|
|
204
|
+
- "radius": Creates circular buffers around reachable nodes
|
|
205
|
+
- "ways": Creates polygons based on reachable road network
|
|
206
|
+
points : gpd.GeoDataFrame
|
|
207
|
+
GeoDataFrame containing source points for isochrone calculation.
|
|
208
|
+
weight_value : float
|
|
209
|
+
Maximum travel time (minutes) or distance (meters) threshold.
|
|
210
|
+
weight_type : Literal["time_min", "length_meter"]
|
|
211
|
+
Type of weight calculation:
|
|
212
|
+
- "time_min": Time-based accessibility in minutes
|
|
213
|
+
- "length_meter": Distance-based accessibility in meters
|
|
214
|
+
nx_graph : nx.Graph
|
|
215
|
+
NetworkX graph representing the transportation network.
|
|
216
|
+
Must contain CRS and speed attributes for time calculations.
|
|
217
|
+
**kwargs
|
|
218
|
+
Additional buffer parameters:
|
|
219
|
+
- buffer_factor: Size multiplier for buffers (default: 0.7)
|
|
220
|
+
- road_buffer_size: Buffer size for road edges in meters (default: 5)
|
|
221
|
+
|
|
222
|
+
Returns
|
|
223
|
+
-------
|
|
224
|
+
tuple[gpd.GeoDataFrame, gpd.GeoDataFrame | None, gpd.GeoDataFrame | None]
|
|
225
|
+
Tuple containing:
|
|
226
|
+
- isochrones: GeoDataFrame with calculated isochrone polygons
|
|
227
|
+
- pt_stops: Public transport stops within isochrones (if available)
|
|
228
|
+
- pt_routes: Public transport routes within isochrones (if available)
|
|
229
|
+
|
|
230
|
+
Examples
|
|
231
|
+
--------
|
|
232
|
+
>>> from iduedu import get_intermodal_graph # pip install iduedu to get OSM city network graph
|
|
233
|
+
>>> graph = get_intermodal_graph(polygon=my_territory_polygon)
|
|
234
|
+
>>> points = gpd.GeoDataFrame(geometry=[Point(30.33, 59.95)], crs=4326)
|
|
235
|
+
>>> # Radius isochrones
|
|
236
|
+
>>> radius_iso, stops, routes = get_accessibility_isochrones(
|
|
237
|
+
... "radius", points, 15, "time_min", graph, buffer_factor=0.8
|
|
238
|
+
... )
|
|
239
|
+
>>> # Road network isochrones
|
|
240
|
+
>>> ways_iso, stops, routes = get_accessibility_isochrones(
|
|
241
|
+
... "ways", points, 1000, "length_meter", graph, road_buffer_size=7
|
|
242
|
+
... )
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
buffer_params = {
|
|
246
|
+
"buffer_factor": 0.7,
|
|
247
|
+
"road_buffer_size": 5,
|
|
248
|
+
}
|
|
249
|
+
original_crs = points.crs
|
|
250
|
+
buffer_params.update(kwargs)
|
|
251
|
+
|
|
252
|
+
points = points.copy()
|
|
253
|
+
local_crs, graph_type = _validate_inputs(points, weight_value, weight_type, nx_graph)
|
|
254
|
+
|
|
255
|
+
nx_graph, points, dist_nearest, speed = _prepare_graph_and_nodes(
|
|
256
|
+
points, nx_graph, graph_type, weight_type, weight_value
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
weight_cutoff = (
|
|
260
|
+
weight_value + (100 if weight_type == "length_meter" else 1) if isochrone_type == "ways" else weight_value
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
dist_matrix, subgraph = _calculate_distance_matrix(
|
|
264
|
+
nx_graph, points["nearest_node"].values, weight_type, weight_cutoff, dist_nearest
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
logger.info("Building isochrones geometry...")
|
|
268
|
+
nodes, edges = graph_to_gdf(subgraph)
|
|
269
|
+
if isochrone_type == "radius":
|
|
270
|
+
isochrone_geoms = _build_radius_isochrones(
|
|
271
|
+
dist_matrix, weight_value, weight_type, speed, nodes, buffer_params["buffer_factor"]
|
|
272
|
+
)
|
|
273
|
+
else: # isochrone_type == 'ways':
|
|
274
|
+
if graph_type in ["intermodal", "walk"]:
|
|
275
|
+
isochrone_edges = edges[edges["type"] == "walk"]
|
|
276
|
+
else:
|
|
277
|
+
isochrone_edges = edges.copy()
|
|
278
|
+
all_isochrones_edges = isochrone_edges.buffer(buffer_params["road_buffer_size"], resolution=1).union_all()
|
|
279
|
+
all_isochrones_edges = gpd.GeoDataFrame(geometry=[all_isochrones_edges], crs=local_crs)
|
|
280
|
+
isochrone_geoms = _build_ways_isochrones(
|
|
281
|
+
dist_matrix=dist_matrix,
|
|
282
|
+
weight_value=weight_value,
|
|
283
|
+
weight_type=weight_type,
|
|
284
|
+
speed=speed,
|
|
285
|
+
nodes=nodes,
|
|
286
|
+
all_isochrones_edges=all_isochrones_edges,
|
|
287
|
+
buffer_factor=buffer_params["buffer_factor"],
|
|
288
|
+
)
|
|
289
|
+
isochrones = _create_isochrones_gdf(points, isochrone_geoms, dist_matrix, local_crs, weight_type, weight_value)
|
|
290
|
+
pt_nodes, pt_edges = _process_pt_data(nodes, edges, graph_type)
|
|
291
|
+
if pt_nodes is not None:
|
|
292
|
+
pt_nodes.to_crs(original_crs, inplace=True)
|
|
293
|
+
if pt_edges is not None:
|
|
294
|
+
pt_edges.to_crs(original_crs, inplace=True)
|
|
295
|
+
return isochrones.to_crs(original_crs), pt_nodes, pt_edges
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _build_radius_isochrones(dist_matrix, weight_value, weight_type, speed, nodes, buffer_factor):
|
|
299
|
+
results = []
|
|
300
|
+
for source in dist_matrix.index:
|
|
301
|
+
buffers = (weight_value - dist_matrix.loc[source]) * buffer_factor
|
|
302
|
+
if weight_type == "time_min":
|
|
303
|
+
buffers = buffers * speed
|
|
304
|
+
buffers = nodes.merge(buffers, left_index=True, right_index=True)
|
|
305
|
+
buffers.geometry = buffers.geometry.buffer(buffers[source], resolution=8)
|
|
306
|
+
results.append(buffers.union_all())
|
|
307
|
+
return results
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _build_ways_isochrones(dist_matrix, weight_value, weight_type, speed, nodes, all_isochrones_edges, buffer_factor):
|
|
311
|
+
results = []
|
|
312
|
+
for source in dist_matrix.index:
|
|
313
|
+
reachable_nodes = dist_matrix.loc[source]
|
|
314
|
+
reachable_nodes = reachable_nodes[reachable_nodes <= weight_value]
|
|
315
|
+
reachable_nodes = (weight_value - reachable_nodes) * buffer_factor
|
|
316
|
+
if weight_type == "time_min":
|
|
317
|
+
reachable_nodes = reachable_nodes * speed
|
|
318
|
+
reachable_nodes = nodes.merge(reachable_nodes, left_index=True, right_index=True)
|
|
319
|
+
clip_zone = reachable_nodes.buffer(reachable_nodes[source], resolution=4).union_all()
|
|
320
|
+
|
|
321
|
+
isochrone_edges = all_isochrones_edges.clip(clip_zone, keep_geom_type=True).explode(ignore_index=True)
|
|
322
|
+
geom_to_keep = isochrone_edges.sjoin(reachable_nodes, how="inner").index.unique()
|
|
323
|
+
isochrone = remove_inner_geom(isochrone_edges.loc[geom_to_keep].union_all())
|
|
324
|
+
results.append(isochrone)
|
|
325
|
+
return results
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class InvalidStepError(ValueError):
|
|
2
|
+
def __init__(self, source_noise_db, target_noise_db, db_sim_step, div_, *args):
|
|
3
|
+
if args:
|
|
4
|
+
self.message = args[0]
|
|
5
|
+
else:
|
|
6
|
+
self.message = (
|
|
7
|
+
f"The difference between `source_noise_db`({source_noise_db}) and `target_noise_db`({target_noise_db})"
|
|
8
|
+
f" is not divisible by the step size ({db_sim_step}, remainder = {div_})"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
def __str__(self):
|
|
12
|
+
if self.message:
|
|
13
|
+
return self.message
|
|
14
|
+
return "The difference between `source_noise_db` and `target_noise_db` is not divisible by the step size"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
data = {
|
|
4
|
+
30: {63: 0, 125: 0.0002, 250: 0.0009, 500: 0.003, 1000: 0.0075, 2000: 0.014, 4000: 0.025, 8000: 0.064},
|
|
5
|
+
20: {63: 0, 125: 0.0003, 250: 0.0011, 500: 0.0028, 1000: 0.0052, 2000: 0.0096, 4000: 0.025, 8000: 0.083},
|
|
6
|
+
10: {63: 0, 125: 0.0004, 250: 0.001, 500: 0.002, 1000: 0.0039, 2000: 0.01, 4000: 0.035, 8000: 0.125},
|
|
7
|
+
0: {63: 0, 125: 0.0004, 250: 0.0008, 500: 0.0017, 1000: 0.0049, 2000: 0.017, 4000: 0.058, 8000: 0.156},
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
air_resist_ratio = pd.DataFrame(data)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy.optimize import fsolve
|
|
3
|
+
|
|
4
|
+
from objectnat import config
|
|
5
|
+
|
|
6
|
+
from .noise_init_data import air_resist_ratio
|
|
7
|
+
|
|
8
|
+
logger = config.logger
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_air_resist_ratio(temp, freq, check_temp_freq=False):
|
|
12
|
+
if check_temp_freq:
|
|
13
|
+
if temp > max(air_resist_ratio.columns) or temp < min(air_resist_ratio.columns):
|
|
14
|
+
logger.warning(
|
|
15
|
+
f"The specified temperature of {temp}°C is outside the tabulated data range. "
|
|
16
|
+
f"The air resistance coefficient for these values may be inaccurate. "
|
|
17
|
+
f"Recommended temperature range: {min(air_resist_ratio.columns)}°C "
|
|
18
|
+
f"to {max(air_resist_ratio.columns)}°C."
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
if freq > max(air_resist_ratio.index) or freq < min(air_resist_ratio.index):
|
|
22
|
+
logger.warning(
|
|
23
|
+
f"The specified geometric mean frequency of {freq} Hz is outside the tabulated data range."
|
|
24
|
+
f" The air resistance coefficient for these values may be inaccurate."
|
|
25
|
+
f" Recommended frequency range: {min(air_resist_ratio.index)} Hz to {max(air_resist_ratio.index)} Hz."
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def get_nearest_values(array, value):
|
|
29
|
+
sorted_array = sorted(array)
|
|
30
|
+
if value in sorted_array:
|
|
31
|
+
return [value]
|
|
32
|
+
if value > max(sorted_array):
|
|
33
|
+
return [sorted_array[-1]]
|
|
34
|
+
if value < min(sorted_array):
|
|
35
|
+
return [sorted_array[0]]
|
|
36
|
+
|
|
37
|
+
for i, val in enumerate(sorted_array):
|
|
38
|
+
if value < val:
|
|
39
|
+
return sorted_array[max(i - 1, 0)], sorted_array[i]
|
|
40
|
+
return sorted_array[-2], sorted_array[-1]
|
|
41
|
+
|
|
42
|
+
nearest_temp = get_nearest_values(air_resist_ratio.columns, temp)
|
|
43
|
+
nearest_freq = get_nearest_values(air_resist_ratio.index, freq)
|
|
44
|
+
|
|
45
|
+
if len(nearest_temp) == 1 and len(nearest_freq) == 1:
|
|
46
|
+
return air_resist_ratio.loc[nearest_freq[0], nearest_temp[0]]
|
|
47
|
+
|
|
48
|
+
if len(nearest_temp) == 2 and len(nearest_freq) == 2:
|
|
49
|
+
freq1, freq2 = nearest_freq
|
|
50
|
+
temp1, temp2 = nearest_temp
|
|
51
|
+
|
|
52
|
+
coef_temp1_freq1 = air_resist_ratio.loc[freq1, temp1]
|
|
53
|
+
coef_temp1_freq2 = air_resist_ratio.loc[freq2, temp1]
|
|
54
|
+
coef_temp2_freq1 = air_resist_ratio.loc[freq1, temp2]
|
|
55
|
+
coef_temp2_freq2 = air_resist_ratio.loc[freq2, temp2]
|
|
56
|
+
|
|
57
|
+
weight_temp1 = (temp2 - temp) / (temp2 - temp1)
|
|
58
|
+
weight_temp2 = (temp - temp1) / (temp2 - temp1)
|
|
59
|
+
weight_freq1 = (freq2 - freq) / (freq2 - freq1)
|
|
60
|
+
weight_freq2 = (freq - freq1) / (freq2 - freq1)
|
|
61
|
+
|
|
62
|
+
coef_freq1 = coef_temp1_freq1 * weight_temp1 + coef_temp2_freq1 * weight_temp2
|
|
63
|
+
coef_freq2 = coef_temp1_freq2 * weight_temp1 + coef_temp2_freq2 * weight_temp2
|
|
64
|
+
|
|
65
|
+
final_coef = coef_freq1 * weight_freq1 + coef_freq2 * weight_freq2
|
|
66
|
+
|
|
67
|
+
return final_coef
|
|
68
|
+
|
|
69
|
+
if len(nearest_temp) == 2 and len(nearest_freq) == 1:
|
|
70
|
+
temp1, temp2 = nearest_temp
|
|
71
|
+
freq1 = nearest_freq[0]
|
|
72
|
+
|
|
73
|
+
coef_temp1 = air_resist_ratio.loc[freq1, temp1]
|
|
74
|
+
coef_temp2 = air_resist_ratio.loc[freq1, temp2]
|
|
75
|
+
|
|
76
|
+
weight_temp1 = (temp2 - temp) / (temp2 - temp1)
|
|
77
|
+
weight_temp2 = (temp - temp1) / (temp2 - temp1)
|
|
78
|
+
|
|
79
|
+
return coef_temp1 * weight_temp1 + coef_temp2 * weight_temp2
|
|
80
|
+
|
|
81
|
+
if len(nearest_temp) == 1 and len(nearest_freq) == 2:
|
|
82
|
+
temp1 = nearest_temp[0]
|
|
83
|
+
freq1, freq2 = nearest_freq
|
|
84
|
+
|
|
85
|
+
coef_freq1 = air_resist_ratio.loc[freq1, temp1]
|
|
86
|
+
coef_freq2 = air_resist_ratio.loc[freq2, temp1]
|
|
87
|
+
|
|
88
|
+
weight_freq1 = (freq2 - freq) / (freq2 - freq1)
|
|
89
|
+
weight_freq2 = (freq - freq1) / (freq2 - freq1)
|
|
90
|
+
|
|
91
|
+
return coef_freq1 * weight_freq1 + coef_freq2 * weight_freq2
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def dist_to_target_db(
|
|
95
|
+
init_noise_db, target_noise_db, geometric_mean_freq_hz, air_temperature, return_desc=False, check_temp_freq=False
|
|
96
|
+
) -> float | str:
|
|
97
|
+
"""
|
|
98
|
+
Calculates the distance required for a sound wave to decay from an initial noise level to a target noise level,
|
|
99
|
+
based on the geometric mean frequency of the sound and the air temperature. Optionally, can return a description
|
|
100
|
+
of the sound propagation behavior.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
init_noise_db (float): The initial noise level of the source in decibels (dB). This is the starting sound
|
|
104
|
+
intensity.
|
|
105
|
+
target_noise_db (float): The target noise level in decibels (dB), representing the level to which the sound
|
|
106
|
+
decays over distance.
|
|
107
|
+
geometric_mean_freq_hz (float): The geometric mean frequency of the sound (in Hz). This frequency influences
|
|
108
|
+
the attenuation of sound over distance. Higher frequencies decay faster than lower ones.
|
|
109
|
+
air_temperature (float): The temperature of the air in degrees Celsius. This influences the air's resistance
|
|
110
|
+
to sound propagation.
|
|
111
|
+
return_desc (bool, optional): If set to `True`, the function will return a description of the sound decay
|
|
112
|
+
process instead of the calculated distance.
|
|
113
|
+
check_temp_freq (bool, optional): If `True`, the function will check whether the temperature and frequency
|
|
114
|
+
are within valid ranges.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
float or str: If `return_desc` is `False`, the function returns the distance (in meters) over which the sound
|
|
118
|
+
decays from `init_noise_db` to `target_noise_db`. If `return_desc` is `True`, a descriptive string is returned
|
|
119
|
+
explaining the calculation and the conditions.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def equation(r):
|
|
123
|
+
return l - l_ist + 20 * np.log10(r) + k * r
|
|
124
|
+
|
|
125
|
+
l_ist = init_noise_db
|
|
126
|
+
l = target_noise_db
|
|
127
|
+
k = get_air_resist_ratio(air_temperature, geometric_mean_freq_hz, check_temp_freq)
|
|
128
|
+
initial_guess = 1
|
|
129
|
+
r_solution = fsolve(equation, initial_guess)
|
|
130
|
+
if return_desc:
|
|
131
|
+
string = (
|
|
132
|
+
f"Noise level of {init_noise_db} dB "
|
|
133
|
+
f"with a geometric mean frequency of {geometric_mean_freq_hz} Hz "
|
|
134
|
+
f"at an air temperature of {air_temperature}°C decays to {target_noise_db} dB "
|
|
135
|
+
f"over a distance of {r_solution[0]} meters. Air resistance coefficient: {k}."
|
|
136
|
+
)
|
|
137
|
+
return string
|
|
138
|
+
return r_solution[0]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def green_noise_reduce_db(geometric_mean_freq_hz, r_tree) -> float:
|
|
142
|
+
"""
|
|
143
|
+
Calculates the amount of noise reduction (in dB) provided by vegetation of a given thickness at a specified
|
|
144
|
+
geometric mean frequency. The function models the reduction based on the interaction of the sound with trees or
|
|
145
|
+
vegetation.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
geometric_mean_freq_hz (float): The geometric mean frequency of the sound (in Hz).
|
|
149
|
+
r_tree (float): The thickness or density of the vegetation (in meters).
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
float: The noise reduction (in dB) achieved by the vegetation. This value indicates how much quieter the sound
|
|
153
|
+
will be after passing through or interacting with the vegetation of the specified thickness.
|
|
154
|
+
"""
|
|
155
|
+
return round(0.08 * r_tree * ((geometric_mean_freq_hz ** (1 / 3)) / 8), 1)
|