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,315 @@
|
|
|
1
|
+
"""NSGA-II multi-objective optimiser.
|
|
2
|
+
|
|
3
|
+
Deb, Pratap, Agarwal and Meyarivan (2002), "A fast and elitist multiobjective
|
|
4
|
+
genetic algorithm: NSGA-II", IEEE Trans. Evol. Comput. 6(2):182-197,
|
|
5
|
+
doi:10.1109/4235.996017, with constraint domination, simulated binary
|
|
6
|
+
crossover and polynomial mutation. All objectives are minimised. The run is
|
|
7
|
+
seeded (mulberry32, bit-identical to the plugin's viewer) so it can be
|
|
8
|
+
repeated exactly.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import math
|
|
14
|
+
from dataclasses import dataclass, field, replace
|
|
15
|
+
from typing import Any, Callable, Optional, Sequence, Union
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"Individual",
|
|
19
|
+
"NSGA2Result",
|
|
20
|
+
"Problem",
|
|
21
|
+
"crowding_distance",
|
|
22
|
+
"dominates",
|
|
23
|
+
"make_rng",
|
|
24
|
+
"non_dominated_sort",
|
|
25
|
+
"nsga2",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
_M32 = 0xFFFFFFFF
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _imul(a: int, b: int) -> int:
|
|
32
|
+
return (a * b) & _M32
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def make_rng(seed: int = 1) -> Callable[[], float]:
|
|
36
|
+
"""mulberry32: small, fast, seedable PRNG returning floats in ``[0, 1)``."""
|
|
37
|
+
state = [seed & _M32]
|
|
38
|
+
|
|
39
|
+
def rng() -> float:
|
|
40
|
+
state[0] = (state[0] + 0x6D2B79F5) & _M32
|
|
41
|
+
t = state[0]
|
|
42
|
+
t = _imul(t ^ (t >> 15), t | 1)
|
|
43
|
+
t = (t ^ ((t + _imul(t ^ (t >> 7), t | 61)) & _M32)) & _M32
|
|
44
|
+
return ((t ^ (t >> 14)) & _M32) / 4294967296
|
|
45
|
+
|
|
46
|
+
return rng
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class Individual:
|
|
51
|
+
"""One candidate solution."""
|
|
52
|
+
|
|
53
|
+
x: list[float] = field(default_factory=list)
|
|
54
|
+
objectives: list[float] = field(default_factory=list)
|
|
55
|
+
violation: float = 0.0
|
|
56
|
+
extra: Any = None
|
|
57
|
+
rank: int = 0
|
|
58
|
+
crowding: float = 0.0
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
EvalResult = Union[dict, Sequence[float]]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class Problem:
|
|
66
|
+
"""Optimisation problem.
|
|
67
|
+
|
|
68
|
+
``evaluate(x)`` returns either a sequence of objectives or a dict with
|
|
69
|
+
``objectives`` and optional ``violation`` (>= 0, 0 = feasible) and
|
|
70
|
+
``extra``.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
lower: Sequence[float]
|
|
74
|
+
upper: Sequence[float]
|
|
75
|
+
evaluate: Callable[[list[float]], EvalResult]
|
|
76
|
+
integer: bool = False
|
|
77
|
+
seeds: Optional[Sequence[Sequence[float]]] = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class NSGA2Result:
|
|
82
|
+
"""Final population, its first front, evaluation count and per-generation history."""
|
|
83
|
+
|
|
84
|
+
population: list[Individual]
|
|
85
|
+
front: list[Individual]
|
|
86
|
+
evaluations: int
|
|
87
|
+
history: list[dict]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def dominates(a: Individual, b: Individual) -> bool:
|
|
91
|
+
"""``a`` dominates ``b`` (constraint domination: feasibility first)."""
|
|
92
|
+
if a.violation > 0 or b.violation > 0:
|
|
93
|
+
return a.violation < b.violation
|
|
94
|
+
better = False
|
|
95
|
+
for ai, bi in zip(a.objectives, b.objectives):
|
|
96
|
+
if ai > bi:
|
|
97
|
+
return False
|
|
98
|
+
if ai < bi:
|
|
99
|
+
better = True
|
|
100
|
+
return better
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def non_dominated_sort(pop: Sequence[Individual]) -> list[list[int]]:
|
|
104
|
+
"""Fast non-dominated sort: list of fronts (lists of indices); sets ``rank``."""
|
|
105
|
+
n = len(pop)
|
|
106
|
+
dominated_by: list[list[int]] = [[] for _ in range(n)]
|
|
107
|
+
count = [0] * n
|
|
108
|
+
fronts: list[list[int]] = [[]]
|
|
109
|
+
for p in range(n):
|
|
110
|
+
for q in range(n):
|
|
111
|
+
if p == q:
|
|
112
|
+
continue
|
|
113
|
+
if dominates(pop[p], pop[q]):
|
|
114
|
+
dominated_by[p].append(q)
|
|
115
|
+
elif dominates(pop[q], pop[p]):
|
|
116
|
+
count[p] += 1
|
|
117
|
+
if count[p] == 0:
|
|
118
|
+
pop[p].rank = 0
|
|
119
|
+
fronts[0].append(p)
|
|
120
|
+
i = 0
|
|
121
|
+
while fronts[i]:
|
|
122
|
+
nxt: list[int] = []
|
|
123
|
+
for p in fronts[i]:
|
|
124
|
+
for q in dominated_by[p]:
|
|
125
|
+
count[q] -= 1
|
|
126
|
+
if count[q] == 0:
|
|
127
|
+
pop[q].rank = i + 1
|
|
128
|
+
nxt.append(q)
|
|
129
|
+
i += 1
|
|
130
|
+
fronts.append(nxt)
|
|
131
|
+
fronts.pop()
|
|
132
|
+
return fronts
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def crowding_distance(pop: Sequence[Individual], front: Sequence[int]) -> None:
|
|
136
|
+
"""Crowding distance within one front; sets ``crowding``."""
|
|
137
|
+
for i in front:
|
|
138
|
+
pop[i].crowding = 0.0
|
|
139
|
+
if len(front) <= 2:
|
|
140
|
+
for i in front:
|
|
141
|
+
pop[i].crowding = math.inf
|
|
142
|
+
return
|
|
143
|
+
m = len(pop[front[0]].objectives)
|
|
144
|
+
for k in range(m):
|
|
145
|
+
srt = sorted(front, key=lambda i: pop[i].objectives[k]) # noqa: B023
|
|
146
|
+
lo = pop[srt[0]].objectives[k]
|
|
147
|
+
hi = pop[srt[-1]].objectives[k]
|
|
148
|
+
pop[srt[0]].crowding = math.inf
|
|
149
|
+
pop[srt[-1]].crowding = math.inf
|
|
150
|
+
if hi - lo <= 0:
|
|
151
|
+
continue
|
|
152
|
+
for j in range(1, len(srt) - 1):
|
|
153
|
+
pop[srt[j]].crowding += (
|
|
154
|
+
pop[srt[j + 1]].objectives[k] - pop[srt[j - 1]].objectives[k]
|
|
155
|
+
) / (hi - lo)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _better(a: Individual, b: Individual) -> bool:
|
|
159
|
+
if a.rank != b.rank:
|
|
160
|
+
return a.rank < b.rank
|
|
161
|
+
return a.crowding > b.crowding
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _sbx(
|
|
165
|
+
rng: Callable[[], float], x1: float, x2: float, lo: float, hi: float, eta: float
|
|
166
|
+
) -> tuple[float, float]:
|
|
167
|
+
if rng() > 0.5 or abs(x1 - x2) < 1e-12:
|
|
168
|
+
return x1, x2
|
|
169
|
+
u = rng()
|
|
170
|
+
if u <= 0.5:
|
|
171
|
+
beta = (2 * u) ** (1 / (eta + 1))
|
|
172
|
+
else:
|
|
173
|
+
beta = (1 / (2 * (1 - u))) ** (1 / (eta + 1))
|
|
174
|
+
c1 = 0.5 * ((1 + beta) * x1 + (1 - beta) * x2)
|
|
175
|
+
c2 = 0.5 * ((1 - beta) * x1 + (1 + beta) * x2)
|
|
176
|
+
return min(hi, max(lo, c1)), min(hi, max(lo, c2))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _poly_mutate(rng: Callable[[], float], x: float, lo: float, hi: float, eta: float) -> float:
|
|
180
|
+
span = hi - lo
|
|
181
|
+
if span <= 0:
|
|
182
|
+
return x
|
|
183
|
+
u = rng()
|
|
184
|
+
if u < 0.5:
|
|
185
|
+
delta = (2 * u) ** (1 / (eta + 1)) - 1
|
|
186
|
+
else:
|
|
187
|
+
delta = 1 - (2 * (1 - u)) ** (1 / (eta + 1))
|
|
188
|
+
return min(hi, max(lo, x + delta * span))
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def nsga2(
|
|
192
|
+
problem: Problem,
|
|
193
|
+
population_size: int = 24,
|
|
194
|
+
generations: int = 20,
|
|
195
|
+
seed: int = 1,
|
|
196
|
+
eta_c: float = 15,
|
|
197
|
+
eta_m: float = 20,
|
|
198
|
+
crossover_prob: float = 0.9,
|
|
199
|
+
on_generation: Optional[Callable[[dict], None]] = None,
|
|
200
|
+
should_stop: Optional[Callable[[], bool]] = None,
|
|
201
|
+
) -> NSGA2Result:
|
|
202
|
+
"""Run NSGA-II on ``problem`` and return the final population and front."""
|
|
203
|
+
rng = make_rng(seed)
|
|
204
|
+
lower = [float(v) for v in problem.lower]
|
|
205
|
+
upper = [float(v) for v in problem.upper]
|
|
206
|
+
n = len(lower)
|
|
207
|
+
pm = 1 / max(1, n)
|
|
208
|
+
|
|
209
|
+
def rnd(x: list[float]) -> list[float]:
|
|
210
|
+
if not problem.integer:
|
|
211
|
+
return x
|
|
212
|
+
return [min(upper[i], max(lower[i], math.floor(v + 0.5))) for i, v in enumerate(x)]
|
|
213
|
+
|
|
214
|
+
cache: dict[tuple, Individual] = {}
|
|
215
|
+
evaluations = 0
|
|
216
|
+
|
|
217
|
+
def evaluate(x: list[float]) -> Individual:
|
|
218
|
+
nonlocal evaluations
|
|
219
|
+
key = tuple(x)
|
|
220
|
+
if key in cache:
|
|
221
|
+
return replace(cache[key])
|
|
222
|
+
r = problem.evaluate(list(x))
|
|
223
|
+
evaluations += 1
|
|
224
|
+
if isinstance(r, dict):
|
|
225
|
+
objs = list(r["objectives"])
|
|
226
|
+
viol = max(0.0, float(r.get("violation") or 0.0))
|
|
227
|
+
extra = r.get("extra")
|
|
228
|
+
else:
|
|
229
|
+
objs, viol, extra = list(r), 0.0, None
|
|
230
|
+
ind = Individual(x=list(x), objectives=objs, violation=viol, extra=extra)
|
|
231
|
+
cache[key] = ind
|
|
232
|
+
return replace(ind)
|
|
233
|
+
|
|
234
|
+
pop: list[Individual] = []
|
|
235
|
+
for s in problem.seeds or []:
|
|
236
|
+
if len(pop) < population_size:
|
|
237
|
+
pop.append(evaluate(rnd([float(v) for v in s])))
|
|
238
|
+
while len(pop) < population_size:
|
|
239
|
+
x = [lo + rng() * (upper[i] - lo) for i, lo in enumerate(lower)]
|
|
240
|
+
pop.append(evaluate(rnd(x)))
|
|
241
|
+
|
|
242
|
+
def rank_all(p: list[Individual]) -> list[list[int]]:
|
|
243
|
+
fronts = non_dominated_sort(p)
|
|
244
|
+
for f in fronts:
|
|
245
|
+
crowding_distance(p, f)
|
|
246
|
+
return fronts
|
|
247
|
+
|
|
248
|
+
history: list[dict] = []
|
|
249
|
+
rank_all(pop)
|
|
250
|
+
for g in range(generations):
|
|
251
|
+
if should_stop is not None and should_stop():
|
|
252
|
+
break
|
|
253
|
+
|
|
254
|
+
def tournament(pool: list[Individual]) -> Individual:
|
|
255
|
+
a = pool[int(math.floor(rng() * len(pool)))]
|
|
256
|
+
b = pool[int(math.floor(rng() * len(pool)))]
|
|
257
|
+
return a if _better(a, b) else b
|
|
258
|
+
|
|
259
|
+
children: list[Individual] = []
|
|
260
|
+
while len(children) < population_size:
|
|
261
|
+
p1 = tournament(pop).x
|
|
262
|
+
p2 = tournament(pop).x
|
|
263
|
+
c1 = list(p1)
|
|
264
|
+
c2 = list(p2)
|
|
265
|
+
if rng() < crossover_prob:
|
|
266
|
+
for i in range(n):
|
|
267
|
+
c1[i], c2[i] = _sbx(rng, p1[i], p2[i], lower[i], upper[i], eta_c)
|
|
268
|
+
for i in range(n):
|
|
269
|
+
if rng() < pm:
|
|
270
|
+
c1[i] = _poly_mutate(rng, c1[i], lower[i], upper[i], eta_m)
|
|
271
|
+
if rng() < pm:
|
|
272
|
+
c2[i] = _poly_mutate(rng, c2[i], lower[i], upper[i], eta_m)
|
|
273
|
+
children.append(evaluate(rnd(c1)))
|
|
274
|
+
if len(children) < population_size:
|
|
275
|
+
children.append(evaluate(rnd(c2)))
|
|
276
|
+
seen: set = set()
|
|
277
|
+
unique: list[Individual] = []
|
|
278
|
+
dupes: list[Individual] = []
|
|
279
|
+
for ind in pop + children:
|
|
280
|
+
key = tuple(ind.x)
|
|
281
|
+
if key in seen:
|
|
282
|
+
dupes.append(ind)
|
|
283
|
+
else:
|
|
284
|
+
seen.add(key)
|
|
285
|
+
unique.append(ind)
|
|
286
|
+
merged = (
|
|
287
|
+
unique
|
|
288
|
+
if len(unique) >= population_size
|
|
289
|
+
else unique + dupes[: population_size - len(unique)]
|
|
290
|
+
)
|
|
291
|
+
fronts = rank_all(merged)
|
|
292
|
+
nxt: list[Individual] = []
|
|
293
|
+
for f in fronts:
|
|
294
|
+
if len(nxt) + len(f) <= population_size:
|
|
295
|
+
nxt.extend(merged[i] for i in f)
|
|
296
|
+
continue
|
|
297
|
+
srt = sorted(f, key=lambda i: -merged[i].crowding)
|
|
298
|
+
nxt.extend(merged[i] for i in srt[: population_size - len(nxt)])
|
|
299
|
+
break
|
|
300
|
+
pop = nxt
|
|
301
|
+
rank_all(pop)
|
|
302
|
+
front = [p for p in pop if p.rank == 0 and p.violation == 0]
|
|
303
|
+
history.append({"generation": g + 1, "evaluations": evaluations, "front_size": len(front)})
|
|
304
|
+
if on_generation is not None:
|
|
305
|
+
on_generation(
|
|
306
|
+
{
|
|
307
|
+
"generation": g + 1,
|
|
308
|
+
"generations": generations,
|
|
309
|
+
"population": pop,
|
|
310
|
+
"front": front,
|
|
311
|
+
"evaluations": evaluations,
|
|
312
|
+
}
|
|
313
|
+
)
|
|
314
|
+
front = [p for p in pop if p.rank == 0]
|
|
315
|
+
return NSGA2Result(population=pop, front=front, evaluations=evaluations, history=history)
|