zero2geospatial3d-sdk 0.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.
- zero2geospatial3d/__init__.py +105 -0
- zero2geospatial3d/acoustics.py +284 -0
- zero2geospatial3d/classify.py +379 -0
- zero2geospatial3d/cli.py +367 -0
- zero2geospatial3d/comfort.py +687 -0
- zero2geospatial3d/data/epw_stations.json.gz +0 -0
- zero2geospatial3d/daylight.py +337 -0
- zero2geospatial3d/dispersion.py +249 -0
- zero2geospatial3d/epw.py +162 -0
- zero2geospatial3d/geo.py +138 -0
- zero2geospatial3d/grid.py +119 -0
- zero2geospatial3d/microclimate.py +388 -0
- zero2geospatial3d/morphology.py +283 -0
- zero2geospatial3d/nsga2.py +315 -0
- zero2geospatial3d/osm.py +450 -0
- zero2geospatial3d/osm_tags.py +180 -0
- zero2geospatial3d/py.typed +0 -0
- zero2geospatial3d/results.py +231 -0
- zero2geospatial3d/road_sources.py +141 -0
- zero2geospatial3d/scene.py +555 -0
- zero2geospatial3d/server.py +332 -0
- zero2geospatial3d/shading.py +443 -0
- zero2geospatial3d/solar.py +207 -0
- zero2geospatial3d/terrain.py +610 -0
- zero2geospatial3d/urban_form.py +326 -0
- zero2geospatial3d/walkability.py +380 -0
- zero2geospatial3d/weather.py +373 -0
- zero2geospatial3d/web/index.html +50 -0
- zero2geospatial3d/web/src/analysis/climate.js +278 -0
- zero2geospatial3d/web/src/analysis/compliance.js +345 -0
- zero2geospatial3d/web/src/analysis/gpu.js +431 -0
- zero2geospatial3d/web/src/analysis/optimize.js +208 -0
- zero2geospatial3d/web/src/analysis/runners.js +1644 -0
- zero2geospatial3d/web/src/analysis/terrain_results.js +145 -0
- zero2geospatial3d/web/src/app.js +630 -0
- zero2geospatial3d/web/src/core/acoustics.js +341 -0
- zero2geospatial3d/web/src/core/classify.js +158 -0
- zero2geospatial3d/web/src/core/comfort.js +686 -0
- zero2geospatial3d/web/src/core/daylight_rule.js +199 -0
- zero2geospatial3d/web/src/core/dispersion.js +264 -0
- zero2geospatial3d/web/src/core/microclimate.js +372 -0
- zero2geospatial3d/web/src/core/morphology.js +153 -0
- zero2geospatial3d/web/src/core/nsga2.js +181 -0
- zero2geospatial3d/web/src/core/party_walls.js +60 -0
- zero2geospatial3d/web/src/core/road_sources.js +127 -0
- zero2geospatial3d/web/src/core/solar.js +112 -0
- zero2geospatial3d/web/src/core/terrain_analysis.js +267 -0
- zero2geospatial3d/web/src/core/terrain_math.js +279 -0
- zero2geospatial3d/web/src/core/urban_form.js +405 -0
- zero2geospatial3d/web/src/core/walkability.js +316 -0
- zero2geospatial3d/web/src/core/weather.js +225 -0
- zero2geospatial3d/web/src/core/wind.js +131 -0
- zero2geospatial3d/web/src/export/check_pdf.js +74 -0
- zero2geospatial3d/web/src/export/exporters.js +556 -0
- zero2geospatial3d/web/src/export/pdf.js +149 -0
- zero2geospatial3d/web/src/export/snapshot.js +234 -0
- zero2geospatial3d/web/src/export/snapshot_scene.js +150 -0
- zero2geospatial3d/web/src/main.js +75 -0
- zero2geospatial3d/web/src/scene/objects.js +645 -0
- zero2geospatial3d/web/src/scene/overlay.js +145 -0
- zero2geospatial3d/web/src/scene/terrain.js +546 -0
- zero2geospatial3d/web/src/scene/viewer.js +275 -0
- zero2geospatial3d/web/src/scene/wind_streamlines.js +336 -0
- zero2geospatial3d/web/src/style.css +225 -0
- zero2geospatial3d/web/src/ui/building_report.js +404 -0
- zero2geospatial3d/web/src/ui/charts.js +441 -0
- zero2geospatial3d/web/src/ui/colorize.js +101 -0
- zero2geospatial3d/web/src/ui/compare.js +362 -0
- zero2geospatial3d/web/src/ui/dom.js +104 -0
- zero2geospatial3d/web/src/ui/panel_analyze.js +742 -0
- zero2geospatial3d/web/src/ui/panel_compare.js +294 -0
- zero2geospatial3d/web/src/ui/panel_layers.js +172 -0
- zero2geospatial3d/web/src/ui/panel_terrain.js +277 -0
- zero2geospatial3d/web/src/ui/qgis_link.js +127 -0
- zero2geospatial3d/web/src/ui/sunclock.js +45 -0
- zero2geospatial3d/web/src/ui/tools.js +197 -0
- zero2geospatial3d/web/src/ui/viewtools.js +173 -0
- zero2geospatial3d/web/vendor/three/addons/controls/OrbitControls.js +1417 -0
- zero2geospatial3d/web/vendor/three/addons/exporters/GLTFExporter.js +3309 -0
- zero2geospatial3d/web/vendor/three/addons/utils/TextureUtils.js +98 -0
- zero2geospatial3d/web/vendor/three/three.module.js +53044 -0
- zero2geospatial3d/wind.py +178 -0
- zero2geospatial3d_sdk-0.1.0.dist-info/METADATA +219 -0
- zero2geospatial3d_sdk-0.1.0.dist-info/RECORD +89 -0
- zero2geospatial3d_sdk-0.1.0.dist-info/WHEEL +5 -0
- zero2geospatial3d_sdk-0.1.0.dist-info/entry_points.txt +2 -0
- zero2geospatial3d_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- zero2geospatial3d_sdk-0.1.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +52 -0
- zero2geospatial3d_sdk-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""zero2geospatial3d: headless 3D terrain and building analysis.
|
|
2
|
+
|
|
3
|
+
A pure-Python (numpy) port of the 02 Geo3D Analyzer QGIS plugin
|
|
4
|
+
("zero2geospatial3d"): sun position and sun hours, shadow casting, sky view
|
|
5
|
+
factor, solar irradiation and PV yield, UTCI / PET thermal comfort, surface
|
|
6
|
+
energy balance, pedestrian wind, ventilation and traffic dispersion, ISO
|
|
7
|
+
9613-2 style noise screening, BRE 209 daylight rules, green view, solar
|
|
8
|
+
envelopes, walkability, terrain derivatives, sections, flooding and cut/fill,
|
|
9
|
+
plus EPW weather, OpenStreetMap download and a bundled browser viewer.
|
|
10
|
+
|
|
11
|
+
Quick start::
|
|
12
|
+
|
|
13
|
+
from zero2geospatial3d import Scene, Building, shading
|
|
14
|
+
|
|
15
|
+
scene = Scene.flat(200, 200, cell=2, lat=38.42, lon=27.14, utc_offset=3)
|
|
16
|
+
ring = [[-10, -10], [10, -10], [10, 10], [-10, 10], [-10, -10]]
|
|
17
|
+
scene.buildings.append(Building([[ring]], height=24))
|
|
18
|
+
hours = shading.sun_hours(scene.surface(), scene.lat, scene.lon, scene.utc_offset,
|
|
19
|
+
month=3, day=21)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
__version__ = "0.1.0"
|
|
25
|
+
|
|
26
|
+
from . import (
|
|
27
|
+
acoustics,
|
|
28
|
+
classify,
|
|
29
|
+
comfort,
|
|
30
|
+
daylight,
|
|
31
|
+
dispersion,
|
|
32
|
+
epw,
|
|
33
|
+
geo,
|
|
34
|
+
microclimate,
|
|
35
|
+
morphology,
|
|
36
|
+
nsga2,
|
|
37
|
+
osm,
|
|
38
|
+
osm_tags,
|
|
39
|
+
results,
|
|
40
|
+
road_sources,
|
|
41
|
+
scene,
|
|
42
|
+
shading,
|
|
43
|
+
solar,
|
|
44
|
+
terrain,
|
|
45
|
+
urban_form,
|
|
46
|
+
walkability,
|
|
47
|
+
weather,
|
|
48
|
+
wind,
|
|
49
|
+
)
|
|
50
|
+
from .comfort import mean_radiant_temperature, pet, utci
|
|
51
|
+
from .grid import Grid, make_grid
|
|
52
|
+
from .scene import Building, Road, Scene, Tree, load_scene
|
|
53
|
+
from .shading import shadow_mask, sky_view_factor, sun_hours, viewshed
|
|
54
|
+
from .solar import solar_position, sun_path, sunrise_sunset
|
|
55
|
+
from .weather import Weather, clear_sky_year, parse_epw, read_epw
|
|
56
|
+
|
|
57
|
+
__all__ = [
|
|
58
|
+
"__version__",
|
|
59
|
+
# data model
|
|
60
|
+
"Grid",
|
|
61
|
+
"make_grid",
|
|
62
|
+
"Scene",
|
|
63
|
+
"Building",
|
|
64
|
+
"Tree",
|
|
65
|
+
"Road",
|
|
66
|
+
"load_scene",
|
|
67
|
+
# frequently used functions
|
|
68
|
+
"solar_position",
|
|
69
|
+
"sun_path",
|
|
70
|
+
"sunrise_sunset",
|
|
71
|
+
"shadow_mask",
|
|
72
|
+
"sun_hours",
|
|
73
|
+
"sky_view_factor",
|
|
74
|
+
"viewshed",
|
|
75
|
+
"utci",
|
|
76
|
+
"pet",
|
|
77
|
+
"mean_radiant_temperature",
|
|
78
|
+
"Weather",
|
|
79
|
+
"parse_epw",
|
|
80
|
+
"read_epw",
|
|
81
|
+
"clear_sky_year",
|
|
82
|
+
# modules
|
|
83
|
+
"acoustics",
|
|
84
|
+
"classify",
|
|
85
|
+
"comfort",
|
|
86
|
+
"daylight",
|
|
87
|
+
"dispersion",
|
|
88
|
+
"epw",
|
|
89
|
+
"geo",
|
|
90
|
+
"microclimate",
|
|
91
|
+
"morphology",
|
|
92
|
+
"nsga2",
|
|
93
|
+
"osm",
|
|
94
|
+
"osm_tags",
|
|
95
|
+
"results",
|
|
96
|
+
"road_sources",
|
|
97
|
+
"scene",
|
|
98
|
+
"shading",
|
|
99
|
+
"solar",
|
|
100
|
+
"terrain",
|
|
101
|
+
"urban_form",
|
|
102
|
+
"walkability",
|
|
103
|
+
"weather",
|
|
104
|
+
"wind",
|
|
105
|
+
]
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Urban road-noise propagation screening.
|
|
2
|
+
|
|
3
|
+
ISO 9613-2 / CNOSSOS-EU inspired: geometric divergence, air absorption,
|
|
4
|
+
ground attenuation, Maekawa single-edge barrier diffraction over buildings,
|
|
5
|
+
foliage attenuation through trees and a facade reflection bonus. Levels are
|
|
6
|
+
pedestrian-level dB(A) on an obstacle-height grid.
|
|
7
|
+
|
|
8
|
+
Grids may be flat (row-major from the north-west cell) or ``(rows, cols)``;
|
|
9
|
+
``(min_x, max_y)`` is the centre of the north-west cell.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import math
|
|
15
|
+
from typing import Any, Callable, Optional, Sequence
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
from .road_sources import is_solid, sample_road_lines, street_centrelines
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"maekawa_diffraction",
|
|
23
|
+
"NOISE_CLASSES",
|
|
24
|
+
"noise_class",
|
|
25
|
+
"noise_propagation",
|
|
26
|
+
"building_facade_noise",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def maekawa_diffraction(path_diff_metres: float, freq_hz: float = 500.0) -> float:
|
|
31
|
+
"""Maekawa barrier attenuation in dB, capped at 20 dB (ISO 9613-2 single edge).
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
path_diff_metres: path difference in metres (<= 0 when unobstructed).
|
|
35
|
+
freq_hz: frequency in Hz.
|
|
36
|
+
"""
|
|
37
|
+
if not path_diff_metres > 0:
|
|
38
|
+
return 0.0
|
|
39
|
+
lam = 343.0 / max(20.0, freq_hz)
|
|
40
|
+
n = (2.0 / lam) * path_diff_metres
|
|
41
|
+
val = 10 * math.log10(max(1.0, 3 + 20 * max(0.0, n)))
|
|
42
|
+
return min(20.0, max(0.0, val))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
NOISE_CLASSES: list[dict[str, Any]] = [
|
|
46
|
+
{"min": 0, "max": 55, "label": "Quiet", "color": "#2b83ba"},
|
|
47
|
+
{"min": 55, "max": 65, "label": "Moderate", "color": "#abdda4"},
|
|
48
|
+
{"min": 65, "max": 70, "label": "Loud", "color": "#fdae61"},
|
|
49
|
+
{"min": 70, "max": math.inf, "label": "High noise", "color": "#d7191c"},
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def noise_class(dba: float) -> Optional[dict[str, Any]]:
|
|
54
|
+
"""Noise class dict for a level in dB(A), ``None`` for a non-finite value."""
|
|
55
|
+
if dba is None or not math.isfinite(dba):
|
|
56
|
+
return None
|
|
57
|
+
for c in NOISE_CLASSES:
|
|
58
|
+
if dba < c["max"]:
|
|
59
|
+
return c
|
|
60
|
+
return NOISE_CLASSES[-1]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
Source = dict[str, Any]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _noise_sources(
|
|
67
|
+
roads: Any,
|
|
68
|
+
weights: Any,
|
|
69
|
+
cols: int,
|
|
70
|
+
rows: int,
|
|
71
|
+
cell: float,
|
|
72
|
+
heights: np.ndarray,
|
|
73
|
+
porosity: Optional[np.ndarray],
|
|
74
|
+
min_x: float,
|
|
75
|
+
max_y: float,
|
|
76
|
+
) -> list[Source]:
|
|
77
|
+
step = max(3.0, cell * 1.5)
|
|
78
|
+
sources, given = sample_road_lines(roads, weights, step)
|
|
79
|
+
if sources or given:
|
|
80
|
+
return sources
|
|
81
|
+
streets = street_centrelines(heights, porosity, cols, rows, cell, min_x, max_y, 25)
|
|
82
|
+
if streets:
|
|
83
|
+
return streets
|
|
84
|
+
sy = max_y - (rows - 1) * cell
|
|
85
|
+
grid_step = max(1, int(math.floor(step / cell + 0.5)))
|
|
86
|
+
return [{"x": min_x + c * cell, "y": sy, "length": step} for c in range(0, cols, grid_step)]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _cluster(items: Sequence[Source], size: float) -> list[Source]:
|
|
90
|
+
bins: dict[tuple[int, int], Source] = {}
|
|
91
|
+
for s in items:
|
|
92
|
+
if not s["length"] > 0:
|
|
93
|
+
continue
|
|
94
|
+
key = (math.floor(s["x"] / size), math.floor(s["y"] / size))
|
|
95
|
+
b = bins.get(key)
|
|
96
|
+
if b is None:
|
|
97
|
+
b = {"x": 0.0, "y": 0.0, "length": 0.0, "members": []}
|
|
98
|
+
bins[key] = b
|
|
99
|
+
b["x"] += s["x"] * s["length"]
|
|
100
|
+
b["y"] += s["y"] * s["length"]
|
|
101
|
+
b["length"] += s["length"]
|
|
102
|
+
b["members"].append(s)
|
|
103
|
+
out = list(bins.values())
|
|
104
|
+
for b in out:
|
|
105
|
+
b["x"] /= b["length"]
|
|
106
|
+
b["y"] /= b["length"]
|
|
107
|
+
return out
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _near_solid_facades(solid: np.ndarray, cell: float) -> np.ndarray:
|
|
111
|
+
"""Open cells with a solid building within 12 m."""
|
|
112
|
+
rows, cols = solid.shape
|
|
113
|
+
rad = int(math.ceil(12 / cell))
|
|
114
|
+
near = np.zeros_like(solid)
|
|
115
|
+
for dr in range(-rad, rad + 1):
|
|
116
|
+
for dc in range(-rad, rad + 1):
|
|
117
|
+
if math.hypot(dc * cell, dr * cell) > 12:
|
|
118
|
+
continue
|
|
119
|
+
shifted = np.zeros_like(solid)
|
|
120
|
+
r0, r1 = max(0, -dr), min(rows, rows - dr)
|
|
121
|
+
c0, c1 = max(0, -dc), min(cols, cols - dc)
|
|
122
|
+
if r0 >= r1 or c0 >= c1:
|
|
123
|
+
continue
|
|
124
|
+
shifted[r0:r1, c0:c1] = solid[r0 + dr : r1 + dr, c0 + dc : c1 + dc]
|
|
125
|
+
near |= shifted
|
|
126
|
+
return near & ~solid
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def noise_propagation(
|
|
130
|
+
*,
|
|
131
|
+
heights: Any,
|
|
132
|
+
cols: int,
|
|
133
|
+
rows: int,
|
|
134
|
+
cell: float,
|
|
135
|
+
porosity: Any = None,
|
|
136
|
+
min_x: Optional[float] = None,
|
|
137
|
+
max_y: Optional[float] = None,
|
|
138
|
+
roads: Optional[Sequence[Sequence[Sequence[float]]]] = None,
|
|
139
|
+
road_weights: Optional[Sequence[float]] = None,
|
|
140
|
+
sound_power_db: float = 82.0,
|
|
141
|
+
receiver_height: float = 1.5,
|
|
142
|
+
source_height: float = 0.5,
|
|
143
|
+
ground_factor: float = 0.5,
|
|
144
|
+
max_dist: float = 400.0,
|
|
145
|
+
on_progress: Optional[Callable[[float], None]] = None,
|
|
146
|
+
) -> np.ndarray:
|
|
147
|
+
"""Pedestrian-level road noise Lden in dB(A), shape ``(rows, cols)``.
|
|
148
|
+
|
|
149
|
+
Solid building cells are NaN; open cells are clamped to 35..90 dB(A).
|
|
150
|
+
Without roads, street centrelines between buildings (or the south edge of
|
|
151
|
+
a scene without buildings) act as sources. Distant sources are merged in
|
|
152
|
+
two cluster levels exactly as in the web viewer.
|
|
153
|
+
"""
|
|
154
|
+
if min_x is None:
|
|
155
|
+
min_x = 0.5 * cell
|
|
156
|
+
if max_y is None:
|
|
157
|
+
max_y = (rows - 0.5) * cell
|
|
158
|
+
h = np.asarray(heights, dtype=float).reshape(rows, cols)
|
|
159
|
+
por = None if porosity is None else np.asarray(porosity, dtype=float).reshape(rows, cols)
|
|
160
|
+
solid = is_solid(h, por)
|
|
161
|
+
porous = por > 0 if por is not None else np.zeros_like(solid)
|
|
162
|
+
sources = _noise_sources(roads, road_weights, cols, rows, cell, h, por, min_x, max_y)
|
|
163
|
+
cluster_size = max(8.0, 2 * max(3.0, cell * 1.5))
|
|
164
|
+
big_size = 4 * cluster_size
|
|
165
|
+
clusters = _cluster(sources, cluster_size)
|
|
166
|
+
big = _cluster(clusters, big_size)
|
|
167
|
+
near = 8 * cluster_size
|
|
168
|
+
mid = 5 * big_size
|
|
169
|
+
near_facades = _near_solid_facades(solid, cell)
|
|
170
|
+
dz = receiver_height - source_height
|
|
171
|
+
hl = h.tolist()
|
|
172
|
+
pl = porous.tolist()
|
|
173
|
+
|
|
174
|
+
def energy_from(rx: float, ry: float, src: Source, step_m: float, bonus: float) -> float:
|
|
175
|
+
dx = rx - src["x"]
|
|
176
|
+
dy = ry - src["y"]
|
|
177
|
+
dist2d = math.hypot(dx, dy)
|
|
178
|
+
if dist2d > max_dist:
|
|
179
|
+
return 0.0
|
|
180
|
+
d_direct = math.sqrt(dx * dx + dy * dy + dz * dz)
|
|
181
|
+
d = max(2.0, d_direct)
|
|
182
|
+
max_pd = 0.0
|
|
183
|
+
d_tree = 0.0
|
|
184
|
+
if dist2d >= 1:
|
|
185
|
+
n_steps = max(2, int(math.ceil(dist2d / step_m)))
|
|
186
|
+
seg = dist2d / n_steps
|
|
187
|
+
for k in range(1, n_steps):
|
|
188
|
+
t = k / n_steps
|
|
189
|
+
gc = math.floor((src["x"] + t * dx - min_x) / cell + 0.5)
|
|
190
|
+
gr = math.floor((max_y - (src["y"] + t * dy)) / cell + 0.5)
|
|
191
|
+
if gc < 0 or gc >= cols or gr < 0 or gr >= rows:
|
|
192
|
+
continue
|
|
193
|
+
hh = hl[gr][gc]
|
|
194
|
+
if pl[gr][gc]:
|
|
195
|
+
d_tree += seg
|
|
196
|
+
elif hh > 0 and hh > source_height + t * dz:
|
|
197
|
+
d1 = t * dist2d
|
|
198
|
+
d2 = (1 - t) * dist2d
|
|
199
|
+
pd = (
|
|
200
|
+
math.hypot(d1, hh - source_height)
|
|
201
|
+
+ math.hypot(d2, hh - receiver_height)
|
|
202
|
+
- d_direct
|
|
203
|
+
)
|
|
204
|
+
if pd > max_pd:
|
|
205
|
+
max_pd = pd
|
|
206
|
+
a_bar = maekawa_diffraction(max_pd)
|
|
207
|
+
a_tree = min(4.0, 0.08 * d_tree)
|
|
208
|
+
a_div = 20 * math.log10(d) + 8
|
|
209
|
+
a_air = 0.0019 * d # ISO 9613-2 Table 2 at 500 Hz: about 1.9 dB/km
|
|
210
|
+
a_ground = 1.5 * ground_factor * math.log10(max(1.0, d / 2))
|
|
211
|
+
lw = sound_power_db + 10 * math.log10(max(0.1, src["length"]))
|
|
212
|
+
return 10 ** ((lw - a_div - a_air - a_ground - a_bar - a_tree + bonus) / 10)
|
|
213
|
+
|
|
214
|
+
fine = max(1.0, cell)
|
|
215
|
+
coarse = 2 * fine
|
|
216
|
+
out = np.empty((rows, cols))
|
|
217
|
+
for r in range(rows):
|
|
218
|
+
ry = max_y - r * cell
|
|
219
|
+
for c in range(cols):
|
|
220
|
+
if solid[r, c]:
|
|
221
|
+
out[r, c] = math.nan
|
|
222
|
+
continue
|
|
223
|
+
rx = min_x + c * cell
|
|
224
|
+
bonus = 1.8 if near_facades[r, c] else 0.0
|
|
225
|
+
e = 0.0
|
|
226
|
+
for big_k in big:
|
|
227
|
+
d_k = math.hypot(rx - big_k["x"], ry - big_k["y"])
|
|
228
|
+
if d_k > max_dist + big_size:
|
|
229
|
+
continue
|
|
230
|
+
if d_k > mid:
|
|
231
|
+
e += energy_from(rx, ry, big_k, max(coarse, d_k / 24), bonus)
|
|
232
|
+
continue
|
|
233
|
+
for k in big_k["members"]:
|
|
234
|
+
dk = math.hypot(rx - k["x"], ry - k["y"])
|
|
235
|
+
if dk > max_dist + cluster_size:
|
|
236
|
+
continue
|
|
237
|
+
if dk > near:
|
|
238
|
+
e += energy_from(rx, ry, k, max(coarse, dk / 24), bonus)
|
|
239
|
+
else:
|
|
240
|
+
for src in k["members"]:
|
|
241
|
+
e += energy_from(rx, ry, src, fine, bonus)
|
|
242
|
+
out[r, c] = max(35.0, min(90.0, 10 * math.log10(e))) if e > 0 else 35.0
|
|
243
|
+
if on_progress is not None:
|
|
244
|
+
on_progress((r + 1) / rows)
|
|
245
|
+
return out
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def building_facade_noise(
|
|
249
|
+
noise_grid: Any, ids: Any, cols: int, rows: int
|
|
250
|
+
) -> dict[int, dict[str, float]]:
|
|
251
|
+
"""Per-building facade noise statistics from the open cells within 2 cells.
|
|
252
|
+
|
|
253
|
+
Returns ``{building_id: {"max", "min", "mean", "quiet_facade"}}``;
|
|
254
|
+
``quiet_facade`` is 1 when one side is below 55 dB(A) or the facades
|
|
255
|
+
differ by at least 6 dB(A).
|
|
256
|
+
"""
|
|
257
|
+
result: dict[int, dict[str, float]] = {}
|
|
258
|
+
if ids is None:
|
|
259
|
+
return result
|
|
260
|
+
idf = np.asarray(ids).ravel()
|
|
261
|
+
if idf.size != cols * rows:
|
|
262
|
+
return result
|
|
263
|
+
noise = np.asarray(noise_grid, dtype=float).reshape(rows, cols)
|
|
264
|
+
idg = idf.reshape(rows, cols)
|
|
265
|
+
finite = np.isfinite(noise)
|
|
266
|
+
for key in np.unique(idf[idf > 0]):
|
|
267
|
+
mine = idg == key
|
|
268
|
+
ring = np.zeros_like(mine)
|
|
269
|
+
for dr in range(-2, 3):
|
|
270
|
+
for dc in range(-2, 3):
|
|
271
|
+
if dr == 0 and dc == 0:
|
|
272
|
+
continue
|
|
273
|
+
r0, r1 = max(0, -dr), min(rows, rows - dr)
|
|
274
|
+
c0, c1 = max(0, -dc), min(cols, cols - dc)
|
|
275
|
+
ring[r0:r1, c0:c1] |= mine[r0 + dr : r1 + dr, c0 + dc : c1 + dc]
|
|
276
|
+
ring &= (idg != key) & finite
|
|
277
|
+
vals = noise[ring]
|
|
278
|
+
if vals.size == 0:
|
|
279
|
+
result[int(key)] = {"max": 35.0, "min": 35.0, "mean": 35.0, "quiet_facade": 1}
|
|
280
|
+
continue
|
|
281
|
+
mx, mn = float(vals.max()), float(vals.min())
|
|
282
|
+
quiet = 1 if (mn < 55 or (mx - mn) >= 6) else 0
|
|
283
|
+
result[int(key)] = {"max": mx, "min": mn, "mean": float(vals.mean()), "quiet_facade": quiet}
|
|
284
|
+
return result
|