voxcity 0.6.26__py3-none-any.whl → 0.7.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.
- voxcity/__init__.py +14 -8
- voxcity/downloader/__init__.py +2 -1
- voxcity/downloader/gba.py +210 -0
- voxcity/downloader/gee.py +5 -1
- voxcity/downloader/mbfp.py +1 -1
- voxcity/downloader/oemj.py +80 -8
- voxcity/downloader/utils.py +73 -73
- voxcity/errors.py +30 -0
- voxcity/exporter/__init__.py +13 -5
- voxcity/exporter/cityles.py +633 -538
- voxcity/exporter/envimet.py +728 -708
- voxcity/exporter/magicavoxel.py +334 -297
- voxcity/exporter/netcdf.py +238 -211
- voxcity/exporter/obj.py +1481 -1406
- voxcity/generator/__init__.py +44 -0
- voxcity/generator/api.py +675 -0
- voxcity/generator/grids.py +379 -0
- voxcity/generator/io.py +94 -0
- voxcity/generator/pipeline.py +282 -0
- voxcity/generator/voxelizer.py +380 -0
- voxcity/geoprocessor/__init__.py +75 -6
- voxcity/geoprocessor/conversion.py +153 -0
- voxcity/geoprocessor/draw.py +62 -12
- voxcity/geoprocessor/heights.py +199 -0
- voxcity/geoprocessor/io.py +101 -0
- voxcity/geoprocessor/merge_utils.py +91 -0
- voxcity/geoprocessor/mesh.py +806 -790
- voxcity/geoprocessor/network.py +708 -679
- voxcity/geoprocessor/overlap.py +84 -0
- voxcity/geoprocessor/raster/__init__.py +82 -0
- voxcity/geoprocessor/raster/buildings.py +428 -0
- voxcity/geoprocessor/raster/canopy.py +258 -0
- voxcity/geoprocessor/raster/core.py +150 -0
- voxcity/geoprocessor/raster/export.py +93 -0
- voxcity/geoprocessor/raster/landcover.py +156 -0
- voxcity/geoprocessor/raster/raster.py +110 -0
- voxcity/geoprocessor/selection.py +85 -0
- voxcity/geoprocessor/utils.py +18 -14
- voxcity/models.py +113 -0
- voxcity/simulator/common/__init__.py +22 -0
- voxcity/simulator/common/geometry.py +98 -0
- voxcity/simulator/common/raytracing.py +450 -0
- voxcity/simulator/solar/__init__.py +43 -0
- voxcity/simulator/solar/integration.py +336 -0
- voxcity/simulator/solar/kernels.py +62 -0
- voxcity/simulator/solar/radiation.py +648 -0
- voxcity/simulator/solar/temporal.py +434 -0
- voxcity/simulator/view.py +36 -2286
- voxcity/simulator/visibility/__init__.py +29 -0
- voxcity/simulator/visibility/landmark.py +392 -0
- voxcity/simulator/visibility/view.py +508 -0
- voxcity/utils/logging.py +61 -0
- voxcity/utils/orientation.py +51 -0
- voxcity/utils/weather/__init__.py +26 -0
- voxcity/utils/weather/epw.py +146 -0
- voxcity/utils/weather/files.py +36 -0
- voxcity/utils/weather/onebuilding.py +486 -0
- voxcity/visualizer/__init__.py +24 -0
- voxcity/visualizer/builder.py +43 -0
- voxcity/visualizer/grids.py +141 -0
- voxcity/visualizer/maps.py +187 -0
- voxcity/visualizer/palette.py +228 -0
- voxcity/visualizer/renderer.py +928 -0
- {voxcity-0.6.26.dist-info → voxcity-0.7.0.dist-info}/METADATA +107 -34
- voxcity-0.7.0.dist-info/RECORD +77 -0
- voxcity/generator.py +0 -1302
- voxcity/geoprocessor/grid.py +0 -1739
- voxcity/geoprocessor/polygon.py +0 -1344
- voxcity/simulator/solar.py +0 -2339
- voxcity/utils/visualization.py +0 -2849
- voxcity/utils/weather.py +0 -1038
- voxcity-0.6.26.dist-info/RECORD +0 -38
- {voxcity-0.6.26.dist-info → voxcity-0.7.0.dist-info}/WHEEL +0 -0
- {voxcity-0.6.26.dist-info → voxcity-0.7.0.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-0.6.26.dist-info → voxcity-0.7.0.dist-info}/licenses/LICENSE +0 -0
voxcity/exporter/envimet.py
CHANGED
|
@@ -1,709 +1,729 @@
|
|
|
1
|
-
"""ENVI-met model file exporter module.
|
|
2
|
-
|
|
3
|
-
This module provides functionality to export voxel city data to ENVI-met INX format.
|
|
4
|
-
ENVI-met is a three-dimensional microclimate model designed to simulate surface-plant-air
|
|
5
|
-
interactions in urban environments.
|
|
6
|
-
|
|
7
|
-
Key Features:
|
|
8
|
-
- Converts voxel grids to ENVI-met compatible format
|
|
9
|
-
- Handles building heights, vegetation, materials, and terrain
|
|
10
|
-
- Supports telescoping grid for vertical mesh refinement
|
|
11
|
-
- Generates complete INX files with all required parameters
|
|
12
|
-
- Creates plant database (EDB) files for 3D vegetation
|
|
13
|
-
|
|
14
|
-
Main Functions:
|
|
15
|
-
- prepare_grids: Processes input grids for ENVI-met format
|
|
16
|
-
- create_xml_content: Generates INX file XML content
|
|
17
|
-
- export_inx: Main function to export model to INX format
|
|
18
|
-
- generate_edb_file: Creates plant database file
|
|
19
|
-
- array_to_string: Helper functions for grid formatting
|
|
20
|
-
|
|
21
|
-
Dependencies:
|
|
22
|
-
- numpy: For array operations
|
|
23
|
-
- datetime: For timestamp generation
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
import os
|
|
27
|
-
import numpy as np
|
|
28
|
-
import datetime
|
|
29
|
-
|
|
30
|
-
from ..geoprocessor.
|
|
31
|
-
from ..geoprocessor.utils import get_city_country_name_from_rectangle, get_timezone_info
|
|
32
|
-
from ..utils.lc import convert_land_cover
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
>>>
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
>>>
|
|
72
|
-
|
|
73
|
-
0,0
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
>>>
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
-
|
|
120
|
-
-
|
|
121
|
-
-
|
|
122
|
-
-
|
|
123
|
-
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
-
|
|
128
|
-
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
building_height_grid =
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
-
|
|
202
|
-
-
|
|
203
|
-
-
|
|
204
|
-
-
|
|
205
|
-
-
|
|
206
|
-
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
-
|
|
214
|
-
-
|
|
215
|
-
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
<
|
|
220
|
-
<
|
|
221
|
-
<
|
|
222
|
-
<
|
|
223
|
-
<
|
|
224
|
-
<
|
|
225
|
-
<
|
|
226
|
-
</
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
<
|
|
230
|
-
<
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
<grids-
|
|
235
|
-
<grids-
|
|
236
|
-
<
|
|
237
|
-
<
|
|
238
|
-
<
|
|
239
|
-
<
|
|
240
|
-
<
|
|
241
|
-
<
|
|
242
|
-
<
|
|
243
|
-
<
|
|
244
|
-
<
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
<
|
|
249
|
-
<
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
<
|
|
254
|
-
<
|
|
255
|
-
<
|
|
256
|
-
<
|
|
257
|
-
<
|
|
258
|
-
<
|
|
259
|
-
<
|
|
260
|
-
<
|
|
261
|
-
<
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
<
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
<
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
placeholders
|
|
326
|
-
|
|
327
|
-
"$
|
|
328
|
-
"$
|
|
329
|
-
"$
|
|
330
|
-
"$
|
|
331
|
-
"$
|
|
332
|
-
"$
|
|
333
|
-
"$
|
|
334
|
-
"$
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
"$grids-
|
|
411
|
-
"$grids-
|
|
412
|
-
"$
|
|
413
|
-
"$
|
|
414
|
-
"$
|
|
415
|
-
"$
|
|
416
|
-
"$
|
|
417
|
-
"$
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
xml_template = xml_template.replace("$
|
|
426
|
-
xml_template = xml_template.replace("$
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
<
|
|
446
|
-
<
|
|
447
|
-
<
|
|
448
|
-
<
|
|
449
|
-
<
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
xml_template = xml_template.replace("$
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
xml_template = xml_template.replace("$
|
|
459
|
-
xml_template = xml_template.replace("$
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
-
|
|
476
|
-
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
{
|
|
589
|
-
</
|
|
590
|
-
<
|
|
591
|
-
<
|
|
592
|
-
<
|
|
593
|
-
<
|
|
594
|
-
<
|
|
595
|
-
<
|
|
596
|
-
<
|
|
597
|
-
<
|
|
598
|
-
<
|
|
599
|
-
<
|
|
600
|
-
<
|
|
601
|
-
<
|
|
602
|
-
<
|
|
603
|
-
<
|
|
604
|
-
<
|
|
605
|
-
<
|
|
606
|
-
<
|
|
607
|
-
<
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
<
|
|
611
|
-
<
|
|
612
|
-
<
|
|
613
|
-
<
|
|
614
|
-
<
|
|
615
|
-
<
|
|
616
|
-
<
|
|
617
|
-
<
|
|
618
|
-
<
|
|
619
|
-
<
|
|
620
|
-
<
|
|
621
|
-
<
|
|
622
|
-
<
|
|
623
|
-
<
|
|
624
|
-
<
|
|
625
|
-
<
|
|
626
|
-
<
|
|
627
|
-
<
|
|
628
|
-
<
|
|
629
|
-
<
|
|
630
|
-
<
|
|
631
|
-
<
|
|
632
|
-
<
|
|
633
|
-
<
|
|
634
|
-
<
|
|
635
|
-
<
|
|
636
|
-
<
|
|
637
|
-
<
|
|
638
|
-
<
|
|
639
|
-
<
|
|
640
|
-
<
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
1
|
+
"""ENVI-met model file exporter module.
|
|
2
|
+
|
|
3
|
+
This module provides functionality to export voxel city data to ENVI-met INX format.
|
|
4
|
+
ENVI-met is a three-dimensional microclimate model designed to simulate surface-plant-air
|
|
5
|
+
interactions in urban environments.
|
|
6
|
+
|
|
7
|
+
Key Features:
|
|
8
|
+
- Converts voxel grids to ENVI-met compatible format
|
|
9
|
+
- Handles building heights, vegetation, materials, and terrain
|
|
10
|
+
- Supports telescoping grid for vertical mesh refinement
|
|
11
|
+
- Generates complete INX files with all required parameters
|
|
12
|
+
- Creates plant database (EDB) files for 3D vegetation
|
|
13
|
+
|
|
14
|
+
Main Functions:
|
|
15
|
+
- prepare_grids: Processes input grids for ENVI-met format
|
|
16
|
+
- create_xml_content: Generates INX file XML content
|
|
17
|
+
- export_inx: Main function to export model to INX format
|
|
18
|
+
- generate_edb_file: Creates plant database file
|
|
19
|
+
- array_to_string: Helper functions for grid formatting
|
|
20
|
+
|
|
21
|
+
Dependencies:
|
|
22
|
+
- numpy: For array operations
|
|
23
|
+
- datetime: For timestamp generation
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import os
|
|
27
|
+
import numpy as np
|
|
28
|
+
import datetime
|
|
29
|
+
|
|
30
|
+
from ..geoprocessor.raster import apply_operation, translate_array, group_and_label_cells, process_grid
|
|
31
|
+
from ..geoprocessor.utils import get_city_country_name_from_rectangle, get_timezone_info
|
|
32
|
+
from ..utils.lc import convert_land_cover
|
|
33
|
+
from ..models import VoxCity
|
|
34
|
+
|
|
35
|
+
def array_to_string(arr):
|
|
36
|
+
"""Convert a 2D numpy array to a string representation with comma-separated values.
|
|
37
|
+
|
|
38
|
+
This function formats array values for ENVI-met INX files, where each row must be:
|
|
39
|
+
1. Indented by 5 spaces
|
|
40
|
+
2. Values separated by commas
|
|
41
|
+
3. No trailing comma
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
arr (numpy.ndarray): 2D numpy array to convert
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
str: String representation with each row indented by 5 spaces and values comma-separated
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
>>> arr = np.array([[1, 2], [3, 4]])
|
|
51
|
+
>>> print(array_to_string(arr))
|
|
52
|
+
1,2
|
|
53
|
+
3,4
|
|
54
|
+
"""
|
|
55
|
+
return '\n'.join(' ' + ','.join(str(cell) for cell in row) for row in arr)
|
|
56
|
+
|
|
57
|
+
def array_to_string_with_value(arr, value):
|
|
58
|
+
"""Convert a 2D numpy array to a string representation, replacing all values with a constant.
|
|
59
|
+
|
|
60
|
+
This function is useful for creating uniform value grids in ENVI-met INX files,
|
|
61
|
+
such as for soil profiles or fixed height indicators.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
arr (numpy.ndarray): 2D numpy array to convert (only shape is used)
|
|
65
|
+
value (str or numeric): Value to use for all cells
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
str: String representation with each row indented by 5 spaces and constant value repeated
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
>>> arr = np.zeros((2, 2))
|
|
72
|
+
>>> print(array_to_string_with_value(arr, '0'))
|
|
73
|
+
0,0
|
|
74
|
+
0,0
|
|
75
|
+
"""
|
|
76
|
+
return '\n'.join(' ' + ','.join(str(value) for cell in row) for row in arr)
|
|
77
|
+
|
|
78
|
+
def array_to_string_int(arr):
|
|
79
|
+
"""Convert a 2D numpy array to a string representation of rounded integers.
|
|
80
|
+
|
|
81
|
+
This function is used for grids that must be represented as integers in ENVI-met,
|
|
82
|
+
such as building numbers or terrain heights. Values are rounded to nearest integer.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
arr (numpy.ndarray): 2D numpy array to convert
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
str: String representation with each row indented by 5 spaces and values rounded to integers
|
|
89
|
+
|
|
90
|
+
Example:
|
|
91
|
+
>>> arr = np.array([[1.6, 2.3], [3.7, 4.1]])
|
|
92
|
+
>>> print(array_to_string_int(arr))
|
|
93
|
+
2,2
|
|
94
|
+
4,4
|
|
95
|
+
"""
|
|
96
|
+
return '\n'.join(' ' + ','.join(str(int(cell+0.5)) for cell in row) for row in arr)
|
|
97
|
+
|
|
98
|
+
def prepare_grids(building_height_grid_ori, building_id_grid_ori, canopy_height_grid_ori, land_cover_grid_ori, dem_grid_ori, meshsize, land_cover_source):
|
|
99
|
+
"""Prepare and process input grids for ENVI-met model.
|
|
100
|
+
|
|
101
|
+
This function performs several key transformations on input grids:
|
|
102
|
+
1. Flips grids vertically to match ENVI-met coordinate system
|
|
103
|
+
2. Handles missing values and border conditions
|
|
104
|
+
3. Converts land cover classes to ENVI-met vegetation and material codes
|
|
105
|
+
4. Processes building IDs and heights
|
|
106
|
+
5. Adjusts DEM relative to minimum elevation
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
building_height_grid_ori (numpy.ndarray): Original building height grid (meters)
|
|
110
|
+
building_id_grid_ori (numpy.ndarray): Original building ID grid
|
|
111
|
+
canopy_height_grid_ori (numpy.ndarray): Original canopy height grid (meters)
|
|
112
|
+
land_cover_grid_ori (numpy.ndarray): Original land cover grid (class codes)
|
|
113
|
+
dem_grid_ori (numpy.ndarray): Original DEM grid (meters)
|
|
114
|
+
meshsize (float): Size of mesh cells in meters
|
|
115
|
+
land_cover_source (str): Source of land cover data for class conversion
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
tuple: Processed grids:
|
|
119
|
+
- building_height_grid (numpy.ndarray): Building heights
|
|
120
|
+
- building_id_grid (numpy.ndarray): Building IDs
|
|
121
|
+
- land_cover_veg_grid (numpy.ndarray): Vegetation codes
|
|
122
|
+
- land_cover_mat_grid (numpy.ndarray): Material codes
|
|
123
|
+
- canopy_height_grid (numpy.ndarray): Canopy heights
|
|
124
|
+
- dem_grid (numpy.ndarray): Processed DEM
|
|
125
|
+
|
|
126
|
+
Notes:
|
|
127
|
+
- Building heights at grid borders are set to 0
|
|
128
|
+
- DEM is normalized to minimum elevation
|
|
129
|
+
- Land cover is converted based on source-specific mapping
|
|
130
|
+
"""
|
|
131
|
+
# Flip building height grid vertically and replace NaN with 10m height
|
|
132
|
+
building_height_grid = np.flipud(np.nan_to_num(building_height_grid_ori, nan=10.0)).copy()
|
|
133
|
+
building_id_grid = np.flipud(building_id_grid_ori)
|
|
134
|
+
|
|
135
|
+
# Set border cells to 0 height
|
|
136
|
+
building_height_grid[0, :] = building_height_grid[-1, :] = building_height_grid[:, 0] = building_height_grid[:, -1] = 0
|
|
137
|
+
building_height_grid = apply_operation(building_height_grid, meshsize)
|
|
138
|
+
|
|
139
|
+
# Convert land cover if needed based on source
|
|
140
|
+
if (land_cover_source == 'OpenEarthMapJapan') or (land_cover_source == 'OpenStreetMap'):
|
|
141
|
+
land_cover_grid_converted = land_cover_grid_ori
|
|
142
|
+
else:
|
|
143
|
+
land_cover_grid_converted = convert_land_cover(land_cover_grid_ori, land_cover_source=land_cover_source)
|
|
144
|
+
|
|
145
|
+
land_cover_grid = np.flipud(land_cover_grid_converted).copy() + 1
|
|
146
|
+
|
|
147
|
+
# Dictionary mapping land cover types to vegetation codes
|
|
148
|
+
veg_translation_dict = {
|
|
149
|
+
1: '', # Bareland
|
|
150
|
+
2: '0200XX', # Rangeland
|
|
151
|
+
3: '0200H1', # Shrub
|
|
152
|
+
4: '0200XX', # Moss and lichen
|
|
153
|
+
5: '0200XX', # Agriculture land
|
|
154
|
+
6: '', # Tree
|
|
155
|
+
7: '0200XX', # Wet land
|
|
156
|
+
8: '' # Mangroves
|
|
157
|
+
}
|
|
158
|
+
land_cover_veg_grid = translate_array(land_cover_grid, veg_translation_dict)
|
|
159
|
+
|
|
160
|
+
# Dictionary mapping land cover types to material codes
|
|
161
|
+
mat_translation_dict = {
|
|
162
|
+
1: '000000', # Bareland
|
|
163
|
+
2: '000000', # Rangeland
|
|
164
|
+
3: '000000', # Shrub
|
|
165
|
+
4: '000000', # Moss and lichen
|
|
166
|
+
5: '000000', # Agriculture land
|
|
167
|
+
6: '000000', # Tree
|
|
168
|
+
7: '0200WW', # Wet land
|
|
169
|
+
8: '0200WW', # Mangroves
|
|
170
|
+
9: '0200WW', # Water
|
|
171
|
+
10: '000000', # Snow and ice
|
|
172
|
+
11: '0200PG', # Developed space
|
|
173
|
+
12: '0200ST', # Road
|
|
174
|
+
13: '000000', # Building
|
|
175
|
+
14: '000000', # No Data
|
|
176
|
+
}
|
|
177
|
+
land_cover_mat_grid = translate_array(land_cover_grid, mat_translation_dict)
|
|
178
|
+
|
|
179
|
+
# Process canopy and DEM grids
|
|
180
|
+
canopy_height_grid = canopy_height_grid_ori.copy()
|
|
181
|
+
dem_grid = np.flipud(dem_grid_ori).copy() - np.min(dem_grid_ori)
|
|
182
|
+
|
|
183
|
+
return building_height_grid, building_id_grid, land_cover_veg_grid, land_cover_mat_grid, canopy_height_grid, dem_grid
|
|
184
|
+
|
|
185
|
+
def create_xml_content(building_height_grid, building_id_grid, land_cover_veg_grid, land_cover_mat_grid, canopy_height_grid, dem_grid, meshsize, rectangle_vertices, **kwargs):
|
|
186
|
+
"""Create XML content for ENVI-met INX file.
|
|
187
|
+
|
|
188
|
+
This function generates the complete XML structure for an ENVI-met INX file,
|
|
189
|
+
including model metadata, geometry settings, and all required grid data.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
building_height_grid (numpy.ndarray): Processed building heights
|
|
193
|
+
building_id_grid (numpy.ndarray): Processed building IDs
|
|
194
|
+
land_cover_veg_grid (numpy.ndarray): Vegetation codes grid
|
|
195
|
+
land_cover_mat_grid (numpy.ndarray): Material codes grid
|
|
196
|
+
canopy_height_grid (numpy.ndarray): Processed canopy heights
|
|
197
|
+
dem_grid (numpy.ndarray): Processed DEM
|
|
198
|
+
meshsize (float): Size of mesh cells in meters
|
|
199
|
+
rectangle_vertices (list): Vertices defining model area as [(lon, lat), ...]
|
|
200
|
+
**kwargs: Additional keyword arguments:
|
|
201
|
+
- author_name (str): Name of model author
|
|
202
|
+
- model_description (str): Description of model
|
|
203
|
+
- domain_building_max_height_ratio (float): Ratio of domain height to max building height
|
|
204
|
+
- useTelescoping_grid (bool): Whether to use telescoping grid
|
|
205
|
+
- verticalStretch (float): Vertical stretch factor
|
|
206
|
+
- startStretch (float): Height to start stretching
|
|
207
|
+
- min_grids_Z (int): Minimum vertical grid cells
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
str: Complete XML content for INX file
|
|
211
|
+
|
|
212
|
+
Notes:
|
|
213
|
+
- Automatically determines location information from coordinates
|
|
214
|
+
- Handles both telescoping and uniform vertical grids
|
|
215
|
+
- Sets appropriate defaults for optional parameters
|
|
216
|
+
- Includes all required ENVI-met model settings
|
|
217
|
+
"""
|
|
218
|
+
# XML template defining the structure of an ENVI-met INX file
|
|
219
|
+
xml_template = """<ENVI-MET_Datafile>
|
|
220
|
+
<Header>
|
|
221
|
+
<filetype>INPX ENVI-met Area Input File</filetype>
|
|
222
|
+
<version>440</version>
|
|
223
|
+
<revisiondate>7/5/2024 5:44:52 PM</revisiondate>
|
|
224
|
+
<remark>Created with SPACES 5.6.1</remark>
|
|
225
|
+
<checksum>0</checksum>
|
|
226
|
+
<encryptionlevel>0</encryptionlevel>
|
|
227
|
+
</Header>
|
|
228
|
+
<baseData>
|
|
229
|
+
<modelDescription> $modelDescription$ </modelDescription>
|
|
230
|
+
<modelAuthor> $modelAuthor$ </modelAuthor>
|
|
231
|
+
<modelcopyright> The creator or distributor is responsible for following Copyright Laws </modelcopyright>
|
|
232
|
+
</baseData>
|
|
233
|
+
<modelGeometry>
|
|
234
|
+
<grids-I> $grids-I$ </grids-I>
|
|
235
|
+
<grids-J> $grids-J$ </grids-J>
|
|
236
|
+
<grids-Z> $grids-Z$ </grids-Z>
|
|
237
|
+
<dx> $dx$ </dx>
|
|
238
|
+
<dy> $dy$ </dy>
|
|
239
|
+
<dz-base> $dz-base$ </dz-base>
|
|
240
|
+
<useTelescoping_grid> $useTelescoping_grid$ </useTelescoping_grid>
|
|
241
|
+
<useSplitting> 1 </useSplitting>
|
|
242
|
+
<verticalStretch> $verticalStretch$ </verticalStretch>
|
|
243
|
+
<startStretch> $startStretch$ </startStretch>
|
|
244
|
+
<has3DModel> 0 </has3DModel>
|
|
245
|
+
<isFull3DDesign> 0 </isFull3DDesign>
|
|
246
|
+
</modelGeometry>
|
|
247
|
+
<nestingArea>
|
|
248
|
+
<numberNestinggrids> 0 </numberNestinggrids>
|
|
249
|
+
<soilProfileA> 000000 </soilProfileA>
|
|
250
|
+
<soilProfileB> 000000 </soilProfileB>
|
|
251
|
+
</nestingArea>
|
|
252
|
+
<locationData>
|
|
253
|
+
<modelRotation> $modelRotation$ </modelRotation>
|
|
254
|
+
<projectionSystem> $projectionSystem$ </projectionSystem>
|
|
255
|
+
<UTMZone> 0 </UTMZone>
|
|
256
|
+
<realworldLowerLeft_X> 0.00000 </realworldLowerLeft_X>
|
|
257
|
+
<realworldLowerLeft_Y> 0.00000 </realworldLowerLeft_Y>
|
|
258
|
+
<locationName> $locationName$ </locationName>
|
|
259
|
+
<location_Longitude> $location_Longitude$ </location_Longitude>
|
|
260
|
+
<location_Latitude> $location_Latitude$ </location_Latitude>
|
|
261
|
+
<locationTimeZone_Name> $locationTimeZone_Name$ </locationTimeZone_Name>
|
|
262
|
+
<locationTimeZone_Longitude> $locationTimeZone_Longitude$ </locationTimeZone_Longitude>
|
|
263
|
+
</locationData>
|
|
264
|
+
<defaultSettings>
|
|
265
|
+
<commonWallMaterial> 000000 </commonWallMaterial>
|
|
266
|
+
<commonRoofMaterial> 000000 </commonRoofMaterial>
|
|
267
|
+
</defaultSettings>
|
|
268
|
+
<buildings2D>
|
|
269
|
+
<zTop type="matrix-data" dataI="$grids-I$" dataJ="$grids-J$">
|
|
270
|
+
$zTop$
|
|
271
|
+
</zTop>
|
|
272
|
+
<zBottom type="matrix-data" dataI="$grids-I$" dataJ="$grids-J$">
|
|
273
|
+
$zBottom$
|
|
274
|
+
</zBottom>
|
|
275
|
+
<buildingNr type="matrix-data" dataI="$grids-I$" dataJ="$grids-J$">
|
|
276
|
+
$buildingNr$
|
|
277
|
+
</buildingNr>
|
|
278
|
+
<fixedheight type="matrix-data" dataI="$grids-I$" dataJ="$grids-J$">
|
|
279
|
+
$fixedheight$
|
|
280
|
+
</fixedheight>
|
|
281
|
+
</buildings2D>
|
|
282
|
+
<simpleplants2D>
|
|
283
|
+
<ID_plants1D type="matrix-data" dataI="$grids-I$" dataJ="$grids-J$">
|
|
284
|
+
$ID_plants1D$
|
|
285
|
+
</ID_plants1D>
|
|
286
|
+
</simpleplants2D>
|
|
287
|
+
$3Dplants$
|
|
288
|
+
<soils2D>
|
|
289
|
+
<ID_soilprofile type="matrix-data" dataI="$grids-I$" dataJ="$grids-J$">
|
|
290
|
+
$ID_soilprofile$
|
|
291
|
+
</ID_soilprofile>
|
|
292
|
+
</soils2D>
|
|
293
|
+
<dem>
|
|
294
|
+
<DEMReference> $DEMReference$ </DEMReference>
|
|
295
|
+
<terrainheight type="matrix-data" dataI="$grids-I$" dataJ="$grids-J$">
|
|
296
|
+
$terrainheight$
|
|
297
|
+
</terrainheight>
|
|
298
|
+
</dem>
|
|
299
|
+
<sources2D>
|
|
300
|
+
<ID_sources type="matrix-data" dataI="$grids-I$" dataJ="$grids-J$">
|
|
301
|
+
$ID_sources$
|
|
302
|
+
</ID_sources>
|
|
303
|
+
</sources2D>
|
|
304
|
+
</ENVI-MET_Datafile>"""
|
|
305
|
+
|
|
306
|
+
# Get location information based on rectangle vertices
|
|
307
|
+
city_country_name = get_city_country_name_from_rectangle(rectangle_vertices)
|
|
308
|
+
|
|
309
|
+
# Calculate center coordinates of the model area
|
|
310
|
+
longitudes = [coord[0] for coord in rectangle_vertices] # Changed order from lat to lon
|
|
311
|
+
latitudes = [coord[1] for coord in rectangle_vertices] # Changed order from lat to lon
|
|
312
|
+
center_lon = str(sum(longitudes) / len(longitudes)) # Changed order
|
|
313
|
+
center_lat = str(sum(latitudes) / len(latitudes)) # Changed order
|
|
314
|
+
|
|
315
|
+
timezone_info = get_timezone_info(rectangle_vertices)
|
|
316
|
+
|
|
317
|
+
# Set default values for optional parameters
|
|
318
|
+
author_name = kwargs.get('author_name')
|
|
319
|
+
if author_name is None:
|
|
320
|
+
author_name = "[Enter model author name]"
|
|
321
|
+
model_desctiption = kwargs.get('model_desctiption')
|
|
322
|
+
if model_desctiption is None:
|
|
323
|
+
model_desctiption = "[Enter model desctription]"
|
|
324
|
+
|
|
325
|
+
# Replace location-related placeholders in template
|
|
326
|
+
placeholders = {
|
|
327
|
+
"$modelDescription$": model_desctiption,
|
|
328
|
+
"$modelAuthor$": author_name,
|
|
329
|
+
"$modelRotation$": "0",
|
|
330
|
+
"$projectionSystem$": "GCS_WGS_1984",
|
|
331
|
+
"$locationName$": city_country_name,
|
|
332
|
+
"$location_Longitude$": center_lon,
|
|
333
|
+
"$location_Latitude$": center_lat,
|
|
334
|
+
"$locationTimeZone_Name$": timezone_info[0],
|
|
335
|
+
"$locationTimeZone_Longitude$": timezone_info[1],
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
# Ensure no None values are passed to replace()
|
|
339
|
+
for placeholder, value in placeholders.items():
|
|
340
|
+
if value is None:
|
|
341
|
+
print(f"Warning: {placeholder} is None, using fallback value")
|
|
342
|
+
if placeholder == "$locationName$":
|
|
343
|
+
value = "Unknown Location/ Unknown Country"
|
|
344
|
+
elif placeholder == "$locationTimeZone_Name$":
|
|
345
|
+
value = "UTC+00:00"
|
|
346
|
+
elif placeholder == "$locationTimeZone_Longitude$":
|
|
347
|
+
value = "0.00000"
|
|
348
|
+
elif placeholder == "$modelDescription$":
|
|
349
|
+
value = "[Enter model description]"
|
|
350
|
+
elif placeholder == "$modelAuthor$":
|
|
351
|
+
value = "[Enter model author name]"
|
|
352
|
+
else:
|
|
353
|
+
value = "Unknown"
|
|
354
|
+
xml_template = xml_template.replace(placeholder, str(value))
|
|
355
|
+
|
|
356
|
+
# Calculate building heights including terrain elevation
|
|
357
|
+
building_on_dem_grid = building_height_grid + dem_grid
|
|
358
|
+
|
|
359
|
+
# Configure vertical grid settings
|
|
360
|
+
domain_building_max_height_ratio = kwargs.get('domain_building_max_height_ratio')
|
|
361
|
+
if domain_building_max_height_ratio is None:
|
|
362
|
+
domain_building_max_height_ratio = 2
|
|
363
|
+
|
|
364
|
+
# Configure telescoping grid settings if enabled
|
|
365
|
+
useTelescoping_grid = kwargs.get('useTelescoping_grid')
|
|
366
|
+
if (useTelescoping_grid is None) or (useTelescoping_grid == False):
|
|
367
|
+
useTelescoping_grid = 0
|
|
368
|
+
verticalStretch = 0
|
|
369
|
+
startStretch = 0
|
|
370
|
+
else:
|
|
371
|
+
useTelescoping_grid = 1
|
|
372
|
+
verticalStretch = kwargs.get('verticalStretch')
|
|
373
|
+
if (verticalStretch is None):
|
|
374
|
+
verticalStretch = 20
|
|
375
|
+
startStretch = kwargs.get('startStretch')
|
|
376
|
+
if (startStretch is None):
|
|
377
|
+
startStretch = int(np.max(building_on_dem_grid)/meshsize + 0.5) * meshsize
|
|
378
|
+
|
|
379
|
+
# Set horizontal grid dimensions
|
|
380
|
+
grids_I, grids_J = building_height_grid.shape[1], building_height_grid.shape[0]
|
|
381
|
+
|
|
382
|
+
# Calculate vertical grid dimension based on building heights and telescoping settings
|
|
383
|
+
min_grids_Z = kwargs.get('min_grids_Z', 20)
|
|
384
|
+
if verticalStretch > 0:
|
|
385
|
+
# Calculate minimum number of cells needed to reach target height with telescoping
|
|
386
|
+
a = meshsize # First cell size
|
|
387
|
+
r = (100 + verticalStretch) / 100 # Growth ratio
|
|
388
|
+
S_target = (int(np.max(building_on_dem_grid)/meshsize + 0.5) * meshsize) * (domain_building_max_height_ratio - 1)
|
|
389
|
+
min_n = find_min_n(a, r, S_target, max_n=1000000)
|
|
390
|
+
if min_n is None:
|
|
391
|
+
# Fallback to non-telescoping grid if calculation fails
|
|
392
|
+
print("Warning: Telescoping grid calculation failed, using uniform grid")
|
|
393
|
+
grids_Z = max(int(np.max(building_on_dem_grid)/meshsize + 0.5) * domain_building_max_height_ratio, min_grids_Z)
|
|
394
|
+
else:
|
|
395
|
+
grids_Z_tent = int(np.max(building_on_dem_grid)/meshsize + 0.5) + min_n
|
|
396
|
+
if grids_Z_tent < min_grids_Z:
|
|
397
|
+
grids_Z = min_grids_Z
|
|
398
|
+
startStretch += (min_grids_Z - grids_Z)
|
|
399
|
+
else:
|
|
400
|
+
grids_Z = grids_Z_tent
|
|
401
|
+
else:
|
|
402
|
+
# Calculate vertical grid cells without telescoping
|
|
403
|
+
grids_Z = max(int(np.max(building_on_dem_grid)/meshsize + 0.5) * domain_building_max_height_ratio, min_grids_Z)
|
|
404
|
+
|
|
405
|
+
# Set grid cell sizes
|
|
406
|
+
dx, dy, dz_base = meshsize, meshsize, meshsize
|
|
407
|
+
|
|
408
|
+
# Replace grid-related placeholders
|
|
409
|
+
grid_placeholders = {
|
|
410
|
+
"$grids-I$": str(grids_I),
|
|
411
|
+
"$grids-J$": str(grids_J),
|
|
412
|
+
"$grids-Z$": str(grids_Z),
|
|
413
|
+
"$dx$": str(dx),
|
|
414
|
+
"$dy$": str(dy),
|
|
415
|
+
"$dz-base$": str(dz_base),
|
|
416
|
+
"$useTelescoping_grid$": str(useTelescoping_grid),
|
|
417
|
+
"$verticalStretch$": str(verticalStretch),
|
|
418
|
+
"$startStretch$": str(startStretch),
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
for placeholder, value in grid_placeholders.items():
|
|
422
|
+
xml_template = xml_template.replace(placeholder, value)
|
|
423
|
+
|
|
424
|
+
# Replace matrix data placeholders with actual grid data
|
|
425
|
+
xml_template = xml_template.replace("$zTop$", array_to_string(building_height_grid))
|
|
426
|
+
xml_template = xml_template.replace("$zBottom$", array_to_string_with_value(building_height_grid, '0'))
|
|
427
|
+
xml_template = xml_template.replace("$fixedheight$", array_to_string_with_value(building_height_grid, '0'))
|
|
428
|
+
|
|
429
|
+
# Process and add building numbers
|
|
430
|
+
building_nr_grid = group_and_label_cells(building_id_grid)
|
|
431
|
+
xml_template = xml_template.replace("$buildingNr$", array_to_string(building_nr_grid))
|
|
432
|
+
|
|
433
|
+
# Add vegetation data
|
|
434
|
+
xml_template = xml_template.replace("$ID_plants1D$", array_to_string(land_cover_veg_grid))
|
|
435
|
+
|
|
436
|
+
# Generate and add 3D plant data
|
|
437
|
+
tree_content = ""
|
|
438
|
+
for i in range(grids_I):
|
|
439
|
+
for j in range(grids_J):
|
|
440
|
+
canopy_height = int(canopy_height_grid[j, i] + 0.5)
|
|
441
|
+
# Only add trees where there are no buildings
|
|
442
|
+
if canopy_height_grid[j, i] > 0 and np.flipud(building_height_grid)[j, i]==0:
|
|
443
|
+
plantid = f'H{canopy_height:02d}W01'
|
|
444
|
+
tree_ij = f""" <3Dplants>
|
|
445
|
+
<rootcell_i> {i+1} </rootcell_i>
|
|
446
|
+
<rootcell_j> {j+1} </rootcell_j>
|
|
447
|
+
<rootcell_k> 0 </rootcell_k>
|
|
448
|
+
<plantID> {plantid} </plantID>
|
|
449
|
+
<name> .{plantid} </name>
|
|
450
|
+
<observe> 0 </observe>
|
|
451
|
+
</3Dplants>"""
|
|
452
|
+
tree_content += '\n' + tree_ij
|
|
453
|
+
|
|
454
|
+
# Add remaining data
|
|
455
|
+
xml_template = xml_template.replace("$3Dplants$", tree_content)
|
|
456
|
+
xml_template = xml_template.replace("$ID_soilprofile$", array_to_string(land_cover_mat_grid))
|
|
457
|
+
dem_grid = process_grid(building_nr_grid, dem_grid)
|
|
458
|
+
xml_template = xml_template.replace("$DEMReference$", '0')
|
|
459
|
+
xml_template = xml_template.replace("$terrainheight$", array_to_string_int(dem_grid))
|
|
460
|
+
xml_template = xml_template.replace("$ID_sources$", array_to_string_with_value(land_cover_mat_grid, ''))
|
|
461
|
+
|
|
462
|
+
return xml_template
|
|
463
|
+
|
|
464
|
+
def save_file(content, output_file_path):
|
|
465
|
+
"""Save content to a file with UTF-8 encoding.
|
|
466
|
+
|
|
467
|
+
This function ensures consistent file encoding and error handling when
|
|
468
|
+
saving ENVI-met files.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
content (str): String content to save
|
|
472
|
+
output_file_path (str): Path to save file to
|
|
473
|
+
|
|
474
|
+
Notes:
|
|
475
|
+
- Creates parent directories if they don't exist
|
|
476
|
+
- Uses UTF-8 encoding for compatibility
|
|
477
|
+
- Overwrites existing file if present
|
|
478
|
+
"""
|
|
479
|
+
with open(output_file_path, 'w', encoding='utf-8') as file:
|
|
480
|
+
file.write(content)
|
|
481
|
+
|
|
482
|
+
def export_inx(city: VoxCity, output_directory: str, file_basename: str = 'voxcity', land_cover_source: str | None = None, **kwargs):
|
|
483
|
+
"""Export model data to ENVI-met INX file format.
|
|
484
|
+
|
|
485
|
+
This is the main function for exporting voxel city data to ENVI-met format.
|
|
486
|
+
It coordinates the entire export process from grid preparation to file saving.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
city (VoxCity): VoxCity instance to export
|
|
490
|
+
output_directory (str): Directory to save output
|
|
491
|
+
file_basename (str): Base filename (without extension)
|
|
492
|
+
land_cover_source (str | None): Optional override for land cover source; defaults to city.extras
|
|
493
|
+
**kwargs: Additional keyword arguments passed to create_xml_content()
|
|
494
|
+
|
|
495
|
+
Notes:
|
|
496
|
+
- Creates output directory if it doesn't exist
|
|
497
|
+
- Handles grid preparation and transformation
|
|
498
|
+
- Generates complete INX file with all required data
|
|
499
|
+
- Uses standardized file naming convention
|
|
500
|
+
"""
|
|
501
|
+
# Resolve inputs from VoxCity
|
|
502
|
+
meshsize = float(city.voxels.meta.meshsize)
|
|
503
|
+
rectangle_vertices = city.extras.get("rectangle_vertices") or [(0.0, 0.0)] * 4
|
|
504
|
+
lc_source = land_cover_source or city.extras.get("land_cover_source", "Standard")
|
|
505
|
+
|
|
506
|
+
# Prepare grids
|
|
507
|
+
building_height_grid_inx, building_id_grid, land_cover_veg_grid_inx, land_cover_mat_grid_inx, canopy_height_grid_inx, dem_grid_inx = prepare_grids(
|
|
508
|
+
city.buildings.heights.copy(),
|
|
509
|
+
(city.buildings.ids if city.buildings.ids is not None else np.zeros_like(city.buildings.heights, dtype=int)).copy(),
|
|
510
|
+
(city.tree_canopy.top if city.tree_canopy is not None else np.zeros_like(city.land_cover.classes, dtype=float)).copy(),
|
|
511
|
+
city.land_cover.classes.copy(),
|
|
512
|
+
city.dem.elevation.copy(),
|
|
513
|
+
meshsize,
|
|
514
|
+
lc_source)
|
|
515
|
+
|
|
516
|
+
# Create XML content
|
|
517
|
+
xml_content = create_xml_content(building_height_grid_inx, building_id_grid, land_cover_veg_grid_inx, land_cover_mat_grid_inx, canopy_height_grid_inx, dem_grid_inx, meshsize, rectangle_vertices, **kwargs)
|
|
518
|
+
|
|
519
|
+
# Save the output
|
|
520
|
+
output_dir = output_directory or 'output'
|
|
521
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
522
|
+
output_file_path = os.path.join(output_dir, f"{file_basename}.INX")
|
|
523
|
+
save_file(xml_content, output_file_path)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
class EnvimetExporter:
|
|
527
|
+
"""Exporter adapter to write a VoxCity model to ENVI-met INX format."""
|
|
528
|
+
|
|
529
|
+
def export(self, obj, output_directory: str, base_filename: str, **kwargs):
|
|
530
|
+
if not isinstance(obj, VoxCity):
|
|
531
|
+
raise TypeError("EnvimetExporter expects a VoxCity instance")
|
|
532
|
+
city: VoxCity = obj
|
|
533
|
+
export_inx(
|
|
534
|
+
city,
|
|
535
|
+
output_directory=output_directory,
|
|
536
|
+
file_basename=base_filename,
|
|
537
|
+
**kwargs,
|
|
538
|
+
)
|
|
539
|
+
return os.path.join(output_directory, f"{base_filename}.INX")
|
|
540
|
+
|
|
541
|
+
def generate_edb_file(**kwargs):
|
|
542
|
+
"""Generate ENVI-met database file for 3D plants.
|
|
543
|
+
|
|
544
|
+
Creates a plant database file (EDB) containing definitions for trees of
|
|
545
|
+
different heights with customizable leaf area density profiles.
|
|
546
|
+
|
|
547
|
+
Args:
|
|
548
|
+
**kwargs: Keyword arguments:
|
|
549
|
+
- lad (float): Leaf area density in m²/m³ (default 1.0)
|
|
550
|
+
- trunk_height_ratio (float): Ratio of trunk height to total height
|
|
551
|
+
(default 11.76/19.98)
|
|
552
|
+
|
|
553
|
+
Notes:
|
|
554
|
+
- Generates plants for heights from 1-50m
|
|
555
|
+
- Uses standardized plant IDs in format 'HxxW01'
|
|
556
|
+
- Includes physical properties like wood density
|
|
557
|
+
- Sets seasonal variation profiles
|
|
558
|
+
- Creates complete ENVI-met plant database format
|
|
559
|
+
"""
|
|
560
|
+
|
|
561
|
+
lad = kwargs.get('lad')
|
|
562
|
+
if lad is None:
|
|
563
|
+
lad=1.0
|
|
564
|
+
|
|
565
|
+
trunk_height_ratio = kwargs.get("trunk_height_ratio")
|
|
566
|
+
if trunk_height_ratio is None:
|
|
567
|
+
trunk_height_ratio = 11.76 / 19.98
|
|
568
|
+
|
|
569
|
+
# Create header with current timestamp
|
|
570
|
+
header = f'''<ENVI-MET_Datafile>
|
|
571
|
+
<Header>
|
|
572
|
+
<filetype>DATA</filetype>
|
|
573
|
+
<version>1</version>
|
|
574
|
+
<revisiondate>{datetime.datetime.now().strftime("%m/%d/%Y %I:%M:%S %p")}</revisiondate>
|
|
575
|
+
<remark>Envi-Data</remark>
|
|
576
|
+
<checksum>0</checksum>
|
|
577
|
+
<encryptionlevel>1699612</encryptionlevel>
|
|
578
|
+
</Header>
|
|
579
|
+
'''
|
|
580
|
+
|
|
581
|
+
footer = '</ENVI-MET_Datafile>'
|
|
582
|
+
|
|
583
|
+
# Generate plant definitions for heights 1-50m
|
|
584
|
+
plant3d_objects = []
|
|
585
|
+
|
|
586
|
+
for height in range(1, 51):
|
|
587
|
+
plant3d = f''' <PLANT3D>
|
|
588
|
+
<ID> H{height:02d}W01 </ID>
|
|
589
|
+
<Description> H{height:02d}W01 </Description>
|
|
590
|
+
<AlternativeName> Albero nuovo </AlternativeName>
|
|
591
|
+
<Planttype> 0 </Planttype>
|
|
592
|
+
<Leaftype> 1 </Leaftype>
|
|
593
|
+
<Albedo> 0.18000 </Albedo>
|
|
594
|
+
<Eps> 0.00000 </Eps>
|
|
595
|
+
<Transmittance> 0.30000 </Transmittance>
|
|
596
|
+
<isoprene> 12.00000 </isoprene>
|
|
597
|
+
<leafweigth> 100.00000 </leafweigth>
|
|
598
|
+
<rs_min> 0.00000 </rs_min>
|
|
599
|
+
<Height> {height:.5f} </Height>
|
|
600
|
+
<Width> 1.00000 </Width>
|
|
601
|
+
<Depth> {height * trunk_height_ratio:.5f} </Depth>
|
|
602
|
+
<RootDiameter> 1.00000 </RootDiameter>
|
|
603
|
+
<cellsize> 1.00000 </cellsize>
|
|
604
|
+
<xy_cells> 1 </xy_cells>
|
|
605
|
+
<z_cells> {height} </z_cells>
|
|
606
|
+
<scalefactor> 0.00000 </scalefactor>
|
|
607
|
+
<LAD-Profile type="sparematrix-3D" dataI="1" dataJ="1" zlayers="{height}" defaultValue="0.00000">
|
|
608
|
+
{generate_lad_profile(height, trunk_height_ratio, lad=str(lad))}
|
|
609
|
+
</LAD-Profile>
|
|
610
|
+
<RAD-Profile> 0.10000,0.10000,0.10000,0.10000,0.10000,0.10000,0.10000,0.10000,0.10000,0.10000 </RAD-Profile>
|
|
611
|
+
<Root-Range-Profile> 1.00000,1.00000,1.00000,1.00000,1.00000,1.00000,1.00000,1.00000,1.00000,1.00000 </Root-Range-Profile>
|
|
612
|
+
<Season-Profile> 0.30000,0.30000,0.30000,0.40000,0.70000,1.00000,1.00000,1.00000,0.80000,0.60000,0.30000,0.30000 </Season-Profile>
|
|
613
|
+
<Blossom-Profile> 0.00000,0.00000,0.00000,0.00000,0.00000,0.00000,0.00000,0.00000,0.00000,0.00000,0.00000,0.00000 </Blossom-Profile>
|
|
614
|
+
<DensityWood> 690.00000 </DensityWood>
|
|
615
|
+
<YoungsModulus> 8770000896.00000 </YoungsModulus>
|
|
616
|
+
<YoungRatioRtoL> 0.12000 </YoungRatioRtoL>
|
|
617
|
+
<MORBranch> 65.00000 </MORBranch>
|
|
618
|
+
<MORConnection> 45.00000 </MORConnection>
|
|
619
|
+
<PlantGroup> 0 </PlantGroup>
|
|
620
|
+
<Color> 0 </Color>
|
|
621
|
+
<Group> </Group>
|
|
622
|
+
<Author> </Author>
|
|
623
|
+
<costs> 0.00000 </costs>
|
|
624
|
+
<ColorStem> 0 </ColorStem>
|
|
625
|
+
<ColorBlossom> 0 </ColorBlossom>
|
|
626
|
+
<BlossomRadius> 0.00000 </BlossomRadius>
|
|
627
|
+
<L-SystemBased> 0 </L-SystemBased>
|
|
628
|
+
<Axiom> V </Axiom>
|
|
629
|
+
<IterationDepth> 0 </IterationDepth>
|
|
630
|
+
<hasUserEdits> 0 </hasUserEdits>
|
|
631
|
+
<LADMatrix_generated> 0 </LADMatrix_generated>
|
|
632
|
+
<InitialSegmentLength> 0.00000 </InitialSegmentLength>
|
|
633
|
+
<SmallSegmentLength> 0.00000 </SmallSegmentLength>
|
|
634
|
+
<ChangeSegmentLength> 0.00000 </ChangeSegmentLength>
|
|
635
|
+
<SegmentResolution> 0.00000 </SegmentResolution>
|
|
636
|
+
<TurtleAngle> 0.00000 </TurtleAngle>
|
|
637
|
+
<RadiusOuterBranch> 0.00000 </RadiusOuterBranch>
|
|
638
|
+
<PipeFactor> 0.00000 </PipeFactor>
|
|
639
|
+
<LeafPosition> 0 </LeafPosition>
|
|
640
|
+
<LeafsPerNode> 0 </LeafsPerNode>
|
|
641
|
+
<LeafInternodeLength> 0.00000 </LeafInternodeLength>
|
|
642
|
+
<LeafMinSegmentOrder> 0 </LeafMinSegmentOrder>
|
|
643
|
+
<LeafWidth> 0.00000 </LeafWidth>
|
|
644
|
+
<LeafLength> 0.00000 </LeafLength>
|
|
645
|
+
<LeafSurface> 0.00000 </LeafSurface>
|
|
646
|
+
<PetioleAngle> 0.00000 </PetioleAngle>
|
|
647
|
+
<PetioleLength> 0.00000 </PetioleLength>
|
|
648
|
+
<LeafRotationalAngle> 0.00000 </LeafRotationalAngle>
|
|
649
|
+
<FactorHorizontal> 0.00000 </FactorHorizontal>
|
|
650
|
+
<TropismVector> 0.000000,0.000000,0.000000 </TropismVector>
|
|
651
|
+
<TropismElstaicity> 0.00000 </TropismElstaicity>
|
|
652
|
+
<SegmentRemovallist> </SegmentRemovallist>
|
|
653
|
+
<NrRules> 0 </NrRules>
|
|
654
|
+
<Rules_Variable> </Rules_Variable>
|
|
655
|
+
<Rules_Replacement> </Rules_Replacement>
|
|
656
|
+
<Rules_isConditional> </Rules_isConditional>
|
|
657
|
+
<Rules_Condition> </Rules_Condition>
|
|
658
|
+
<Rules_Remark> </Rules_Remark>
|
|
659
|
+
<TermLString> </TermLString>
|
|
660
|
+
<ApplyTermLString> 0 </ApplyTermLString>
|
|
661
|
+
</PLANT3D>
|
|
662
|
+
'''
|
|
663
|
+
plant3d_objects.append(plant3d)
|
|
664
|
+
|
|
665
|
+
content = header + ''.join(plant3d_objects) + footer
|
|
666
|
+
|
|
667
|
+
with open('projectdatabase.edb', 'w') as f:
|
|
668
|
+
f.write(content)
|
|
669
|
+
|
|
670
|
+
def generate_lad_profile(height, trunk_height_ratio, lad = '1.00000'):
|
|
671
|
+
"""Generate leaf area density profile for a plant.
|
|
672
|
+
|
|
673
|
+
Creates a vertical profile of leaf area density (LAD) values for ENVI-met
|
|
674
|
+
plant definitions, accounting for trunk space and crown distribution.
|
|
675
|
+
|
|
676
|
+
Args:
|
|
677
|
+
height (int): Total height of plant in meters
|
|
678
|
+
trunk_height_ratio (float): Ratio of trunk height to total height
|
|
679
|
+
lad (str): Leaf area density value as string (default '1.00000')
|
|
680
|
+
|
|
681
|
+
Returns:
|
|
682
|
+
str: LAD profile data formatted for ENVI-met EDB file
|
|
683
|
+
|
|
684
|
+
Notes:
|
|
685
|
+
- LAD values start above trunk height
|
|
686
|
+
- Uses 5-space indentation for ENVI-met format
|
|
687
|
+
- Profile follows format: "z-level,x,y,LAD"
|
|
688
|
+
"""
|
|
689
|
+
lad_profile = []
|
|
690
|
+
# Only add LAD values above trunk height
|
|
691
|
+
start = max(0, int(height * trunk_height_ratio))
|
|
692
|
+
for i in range(start, height):
|
|
693
|
+
lad_profile.append(f" 0,0,{i},{lad}")
|
|
694
|
+
return '\n'.join(lad_profile)
|
|
695
|
+
|
|
696
|
+
def find_min_n(a, r, S_target, max_n=1000000):
|
|
697
|
+
"""Find minimum number of terms needed in geometric series to exceed target sum.
|
|
698
|
+
|
|
699
|
+
Used for calculating telescoping grid parameters to achieve desired domain height.
|
|
700
|
+
Solves for n in the equation: a(1-r^n)/(1-r) > S_target
|
|
701
|
+
|
|
702
|
+
Args:
|
|
703
|
+
a (float): First term of series (base cell size)
|
|
704
|
+
r (float): Common ratio (stretch factor)
|
|
705
|
+
S_target (float): Target sum to exceed (desired height)
|
|
706
|
+
max_n (int): Maximum number of terms to try (default 1000000)
|
|
707
|
+
|
|
708
|
+
Returns:
|
|
709
|
+
int or None: Minimum number of terms needed, or None if not possible within max_n
|
|
710
|
+
|
|
711
|
+
Notes:
|
|
712
|
+
- Handles special case of r=1 (arithmetic series)
|
|
713
|
+
- Protects against overflow with large exponents
|
|
714
|
+
- Returns None if solution not found within max_n terms
|
|
715
|
+
"""
|
|
716
|
+
n = 1
|
|
717
|
+
while n <= max_n:
|
|
718
|
+
if r == 1:
|
|
719
|
+
S_n = a * n
|
|
720
|
+
else:
|
|
721
|
+
try:
|
|
722
|
+
S_n = a * (1 - r ** n) / (1 - r)
|
|
723
|
+
except OverflowError:
|
|
724
|
+
# Handle large exponents
|
|
725
|
+
S_n = float('inf') if r > 1 else 0
|
|
726
|
+
if (a > 0 and S_n > S_target) or (a < 0 and S_n < S_target):
|
|
727
|
+
return n
|
|
728
|
+
n += 1
|
|
709
729
|
return None # Not possible within max_n terms
|