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.
Files changed (89) hide show
  1. zero2geospatial3d/__init__.py +105 -0
  2. zero2geospatial3d/acoustics.py +284 -0
  3. zero2geospatial3d/classify.py +379 -0
  4. zero2geospatial3d/cli.py +367 -0
  5. zero2geospatial3d/comfort.py +687 -0
  6. zero2geospatial3d/data/epw_stations.json.gz +0 -0
  7. zero2geospatial3d/daylight.py +337 -0
  8. zero2geospatial3d/dispersion.py +249 -0
  9. zero2geospatial3d/epw.py +162 -0
  10. zero2geospatial3d/geo.py +138 -0
  11. zero2geospatial3d/grid.py +119 -0
  12. zero2geospatial3d/microclimate.py +388 -0
  13. zero2geospatial3d/morphology.py +283 -0
  14. zero2geospatial3d/nsga2.py +315 -0
  15. zero2geospatial3d/osm.py +450 -0
  16. zero2geospatial3d/osm_tags.py +180 -0
  17. zero2geospatial3d/py.typed +0 -0
  18. zero2geospatial3d/results.py +231 -0
  19. zero2geospatial3d/road_sources.py +141 -0
  20. zero2geospatial3d/scene.py +555 -0
  21. zero2geospatial3d/server.py +332 -0
  22. zero2geospatial3d/shading.py +443 -0
  23. zero2geospatial3d/solar.py +207 -0
  24. zero2geospatial3d/terrain.py +610 -0
  25. zero2geospatial3d/urban_form.py +326 -0
  26. zero2geospatial3d/walkability.py +380 -0
  27. zero2geospatial3d/weather.py +373 -0
  28. zero2geospatial3d/web/index.html +50 -0
  29. zero2geospatial3d/web/src/analysis/climate.js +278 -0
  30. zero2geospatial3d/web/src/analysis/compliance.js +345 -0
  31. zero2geospatial3d/web/src/analysis/gpu.js +431 -0
  32. zero2geospatial3d/web/src/analysis/optimize.js +208 -0
  33. zero2geospatial3d/web/src/analysis/runners.js +1644 -0
  34. zero2geospatial3d/web/src/analysis/terrain_results.js +145 -0
  35. zero2geospatial3d/web/src/app.js +630 -0
  36. zero2geospatial3d/web/src/core/acoustics.js +341 -0
  37. zero2geospatial3d/web/src/core/classify.js +158 -0
  38. zero2geospatial3d/web/src/core/comfort.js +686 -0
  39. zero2geospatial3d/web/src/core/daylight_rule.js +199 -0
  40. zero2geospatial3d/web/src/core/dispersion.js +264 -0
  41. zero2geospatial3d/web/src/core/microclimate.js +372 -0
  42. zero2geospatial3d/web/src/core/morphology.js +153 -0
  43. zero2geospatial3d/web/src/core/nsga2.js +181 -0
  44. zero2geospatial3d/web/src/core/party_walls.js +60 -0
  45. zero2geospatial3d/web/src/core/road_sources.js +127 -0
  46. zero2geospatial3d/web/src/core/solar.js +112 -0
  47. zero2geospatial3d/web/src/core/terrain_analysis.js +267 -0
  48. zero2geospatial3d/web/src/core/terrain_math.js +279 -0
  49. zero2geospatial3d/web/src/core/urban_form.js +405 -0
  50. zero2geospatial3d/web/src/core/walkability.js +316 -0
  51. zero2geospatial3d/web/src/core/weather.js +225 -0
  52. zero2geospatial3d/web/src/core/wind.js +131 -0
  53. zero2geospatial3d/web/src/export/check_pdf.js +74 -0
  54. zero2geospatial3d/web/src/export/exporters.js +556 -0
  55. zero2geospatial3d/web/src/export/pdf.js +149 -0
  56. zero2geospatial3d/web/src/export/snapshot.js +234 -0
  57. zero2geospatial3d/web/src/export/snapshot_scene.js +150 -0
  58. zero2geospatial3d/web/src/main.js +75 -0
  59. zero2geospatial3d/web/src/scene/objects.js +645 -0
  60. zero2geospatial3d/web/src/scene/overlay.js +145 -0
  61. zero2geospatial3d/web/src/scene/terrain.js +546 -0
  62. zero2geospatial3d/web/src/scene/viewer.js +275 -0
  63. zero2geospatial3d/web/src/scene/wind_streamlines.js +336 -0
  64. zero2geospatial3d/web/src/style.css +225 -0
  65. zero2geospatial3d/web/src/ui/building_report.js +404 -0
  66. zero2geospatial3d/web/src/ui/charts.js +441 -0
  67. zero2geospatial3d/web/src/ui/colorize.js +101 -0
  68. zero2geospatial3d/web/src/ui/compare.js +362 -0
  69. zero2geospatial3d/web/src/ui/dom.js +104 -0
  70. zero2geospatial3d/web/src/ui/panel_analyze.js +742 -0
  71. zero2geospatial3d/web/src/ui/panel_compare.js +294 -0
  72. zero2geospatial3d/web/src/ui/panel_layers.js +172 -0
  73. zero2geospatial3d/web/src/ui/panel_terrain.js +277 -0
  74. zero2geospatial3d/web/src/ui/qgis_link.js +127 -0
  75. zero2geospatial3d/web/src/ui/sunclock.js +45 -0
  76. zero2geospatial3d/web/src/ui/tools.js +197 -0
  77. zero2geospatial3d/web/src/ui/viewtools.js +173 -0
  78. zero2geospatial3d/web/vendor/three/addons/controls/OrbitControls.js +1417 -0
  79. zero2geospatial3d/web/vendor/three/addons/exporters/GLTFExporter.js +3309 -0
  80. zero2geospatial3d/web/vendor/three/addons/utils/TextureUtils.js +98 -0
  81. zero2geospatial3d/web/vendor/three/three.module.js +53044 -0
  82. zero2geospatial3d/wind.py +178 -0
  83. zero2geospatial3d_sdk-0.1.0.dist-info/METADATA +219 -0
  84. zero2geospatial3d_sdk-0.1.0.dist-info/RECORD +89 -0
  85. zero2geospatial3d_sdk-0.1.0.dist-info/WHEEL +5 -0
  86. zero2geospatial3d_sdk-0.1.0.dist-info/entry_points.txt +2 -0
  87. zero2geospatial3d_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
  88. zero2geospatial3d_sdk-0.1.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +52 -0
  89. 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