voxcity 0.6.1__tar.gz → 0.6.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of voxcity might be problematic. Click here for more details.
- {voxcity-0.6.1 → voxcity-0.6.3}/PKG-INFO +1 -1
- {voxcity-0.6.1 → voxcity-0.6.3}/pyproject.toml +1 -1
- voxcity-0.6.3/src/voxcity/exporter/cityles.py +503 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity/geoprocessor/grid.py +20 -39
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity/geoprocessor/mesh.py +790 -790
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity/simulator/view.py +2238 -2238
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity.egg-info/PKG-INFO +1 -1
- voxcity-0.6.1/src/voxcity/exporter/cityles.py +0 -368
- {voxcity-0.6.1 → voxcity-0.6.3}/AUTHORS.rst +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/CONTRIBUTING.rst +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/HISTORY.rst +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/LICENSE +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/MANIFEST.in +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/README.md +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/docs/Makefile +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/docs/_static/logo.png +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/docs/conf.py +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/docs/logo.png +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/docs/make.bat +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/setup.cfg +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity/__init__.py +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity/downloader/__init__.py +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity/downloader/citygml.py +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity/downloader/eubucco.py +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity/downloader/gee.py +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity/downloader/mbfp.py +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity/downloader/oemj.py +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity/downloader/osm.py +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity/downloader/overture.py +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity/downloader/utils.py +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity/exporter/__init__.py +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity/exporter/envimet.py +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity/exporter/magicavoxel.py +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity/exporter/obj.py +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity/generator.py +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity/geoprocessor/__init__.py +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity/geoprocessor/draw.py +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity/geoprocessor/network.py +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity/geoprocessor/polygon.py +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity/geoprocessor/utils.py +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity/simulator/__init__.py +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity/simulator/solar.py +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity/simulator/utils.py +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity/utils/__init__.py +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity/utils/lc.py +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity/utils/material.py +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity/utils/visualization.py +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity/utils/weather.py +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity.egg-info/SOURCES.txt +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity.egg-info/dependency_links.txt +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity.egg-info/requires.txt +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/src/voxcity.egg-info/top_level.txt +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/tests/__init__.py +0 -0
- {voxcity-0.6.1 → voxcity-0.6.3}/tests/voxelcity.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: voxcity
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.3
|
|
4
4
|
Summary: voxcity is an easy and one-stop tool to output 3d city models for microclimate simulation by integrating multiple geospatial open-data
|
|
5
5
|
Author-email: Kunihiko Fujiwara <kunihiko@nus.edu.sg>
|
|
6
6
|
Maintainer-email: Kunihiko Fujiwara <kunihiko@nus.edu.sg>
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CityLES export module for VoxCity
|
|
3
|
+
Exports VoxCity grid data to CityLES input file format
|
|
4
|
+
Updated 2025/08/05 with corrected land use and building material codes
|
|
5
|
+
Integrated with VoxCity land cover utilities
|
|
6
|
+
|
|
7
|
+
Notes:
|
|
8
|
+
- This module expects raw land cover grids as produced per-source by VoxCity, not
|
|
9
|
+
standardized/converted indices. Supported sources:
|
|
10
|
+
'OpenStreetMap', 'Urbanwatch', 'OpenEarthMapJapan', 'ESA WorldCover',
|
|
11
|
+
'ESRI 10m Annual Land Cover', 'Dynamic World V1'.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import numpy as np
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# VoxCity standard land cover classes after conversion
|
|
20
|
+
# Based on convert_land_cover function output
|
|
21
|
+
VOXCITY_STANDARD_CLASSES = {
|
|
22
|
+
0: 'Bareland',
|
|
23
|
+
1: 'Rangeland',
|
|
24
|
+
2: 'Shrub',
|
|
25
|
+
3: 'Agriculture land',
|
|
26
|
+
4: 'Tree',
|
|
27
|
+
5: 'Moss and lichen',
|
|
28
|
+
6: 'Wet land',
|
|
29
|
+
7: 'Mangrove',
|
|
30
|
+
8: 'Water',
|
|
31
|
+
9: 'Snow and ice',
|
|
32
|
+
10: 'Developed space',
|
|
33
|
+
11: 'Road',
|
|
34
|
+
12: 'Building',
|
|
35
|
+
13: 'No Data'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
## Source-specific class name to CityLES land use mappings
|
|
39
|
+
# CityLES land use codes: 1=Water, 2=Rice Paddy, 3=Crops, 4=Grassland, 5=Deciduous Broadleaf Forest,
|
|
40
|
+
# 9=Bare Land, 10=Building, 16=Asphalt (road), etc.
|
|
41
|
+
|
|
42
|
+
# OpenStreetMap / Standard
|
|
43
|
+
OSM_CLASS_TO_CITYLES = {
|
|
44
|
+
'Bareland': 9,
|
|
45
|
+
'Rangeland': 4,
|
|
46
|
+
'Shrub': 4,
|
|
47
|
+
'Moss and lichen': 4,
|
|
48
|
+
'Agriculture land': 3,
|
|
49
|
+
'Tree': 5,
|
|
50
|
+
'Wet land': 2,
|
|
51
|
+
'Mangroves': 5,
|
|
52
|
+
'Water': 1,
|
|
53
|
+
'Snow and ice': 9,
|
|
54
|
+
'Developed space': 10,
|
|
55
|
+
'Road': 16,
|
|
56
|
+
'Building': 10,
|
|
57
|
+
'No Data': 4
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# Urbanwatch
|
|
61
|
+
URBANWATCH_CLASS_TO_CITYLES = {
|
|
62
|
+
'Building': 10,
|
|
63
|
+
'Road': 16,
|
|
64
|
+
'Parking Lot': 16,
|
|
65
|
+
'Tree Canopy': 5,
|
|
66
|
+
'Grass/Shrub': 4,
|
|
67
|
+
'Agriculture': 3,
|
|
68
|
+
'Water': 1,
|
|
69
|
+
'Barren': 9,
|
|
70
|
+
'Unknown': 4,
|
|
71
|
+
'Sea': 1
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# OpenEarthMapJapan
|
|
75
|
+
OEMJ_CLASS_TO_CITYLES = {
|
|
76
|
+
'Bareland': 9,
|
|
77
|
+
'Rangeland': 4,
|
|
78
|
+
'Developed space': 10,
|
|
79
|
+
'Road': 16,
|
|
80
|
+
'Tree': 5,
|
|
81
|
+
'Water': 1,
|
|
82
|
+
'Agriculture land': 3,
|
|
83
|
+
'Building': 10
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# ESA WorldCover
|
|
87
|
+
ESA_CLASS_TO_CITYLES = {
|
|
88
|
+
'Trees': 5,
|
|
89
|
+
'Shrubland': 4,
|
|
90
|
+
'Grassland': 4,
|
|
91
|
+
'Cropland': 3,
|
|
92
|
+
'Built-up': 10,
|
|
93
|
+
'Barren / sparse vegetation': 9,
|
|
94
|
+
'Snow and ice': 9,
|
|
95
|
+
'Open water': 1,
|
|
96
|
+
'Herbaceous wetland': 2,
|
|
97
|
+
'Mangroves': 5,
|
|
98
|
+
'Moss and lichen': 9
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# ESRI 10m Annual Land Cover
|
|
102
|
+
ESRI_CLASS_TO_CITYLES = {
|
|
103
|
+
'No Data': 4,
|
|
104
|
+
'Water': 1,
|
|
105
|
+
'Trees': 5,
|
|
106
|
+
'Grass': 4,
|
|
107
|
+
'Flooded Vegetation': 2,
|
|
108
|
+
'Crops': 3,
|
|
109
|
+
'Scrub/Shrub': 4,
|
|
110
|
+
'Built Area': 10,
|
|
111
|
+
'Bare Ground': 9,
|
|
112
|
+
'Snow/Ice': 9,
|
|
113
|
+
'Clouds': 4
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# Dynamic World V1
|
|
117
|
+
DYNAMIC_WORLD_CLASS_TO_CITYLES = {
|
|
118
|
+
'Water': 1,
|
|
119
|
+
'Trees': 5,
|
|
120
|
+
'Grass': 4,
|
|
121
|
+
'Flooded Vegetation': 2,
|
|
122
|
+
'Crops': 3,
|
|
123
|
+
'Shrub and Scrub': 4,
|
|
124
|
+
'Built': 10,
|
|
125
|
+
'Bare': 9,
|
|
126
|
+
'Snow and Ice': 9
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# Building material mapping based on corrected documentation
|
|
130
|
+
BUILDING_MATERIAL_MAPPING = {
|
|
131
|
+
'building': 110, # Building (general)
|
|
132
|
+
'concrete': 110, # Building (concrete)
|
|
133
|
+
'residential': 111, # Old wooden house
|
|
134
|
+
'wooden': 111, # Old wooden house
|
|
135
|
+
'commercial': 110, # Building (commercial)
|
|
136
|
+
'industrial': 110, # Building (industrial)
|
|
137
|
+
'default': 110 # Default to general building
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# Tree type mapping for vmap.txt
|
|
141
|
+
TREE_TYPE_MAPPING = {
|
|
142
|
+
'deciduous': 101, # Leaf
|
|
143
|
+
'evergreen': 101, # Leaf (simplified)
|
|
144
|
+
'leaf': 101, # Leaf
|
|
145
|
+
'shade': 102, # Shade
|
|
146
|
+
'default': 101 # Default to leaf
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def create_cityles_directories(output_directory):
|
|
151
|
+
"""Create necessary directories for CityLES output"""
|
|
152
|
+
output_path = Path(output_directory)
|
|
153
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
154
|
+
return output_path
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _get_source_name_mapping(land_cover_source):
|
|
158
|
+
"""Return the class-name-to-CityLES mapping dictionary for the given source."""
|
|
159
|
+
if land_cover_source == 'OpenStreetMap' or land_cover_source == 'Standard':
|
|
160
|
+
return OSM_CLASS_TO_CITYLES
|
|
161
|
+
if land_cover_source == 'Urbanwatch':
|
|
162
|
+
return URBANWATCH_CLASS_TO_CITYLES
|
|
163
|
+
if land_cover_source == 'OpenEarthMapJapan':
|
|
164
|
+
return OEMJ_CLASS_TO_CITYLES
|
|
165
|
+
if land_cover_source == 'ESA WorldCover':
|
|
166
|
+
return ESA_CLASS_TO_CITYLES
|
|
167
|
+
if land_cover_source == 'ESRI 10m Annual Land Cover':
|
|
168
|
+
return ESRI_CLASS_TO_CITYLES
|
|
169
|
+
if land_cover_source == 'Dynamic World V1':
|
|
170
|
+
return DYNAMIC_WORLD_CLASS_TO_CITYLES
|
|
171
|
+
# Default fallback
|
|
172
|
+
return OSM_CLASS_TO_CITYLES
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _build_index_to_cityles_map(land_cover_source):
|
|
176
|
+
"""Build mapping: raw per-source index -> CityLES code, using source class order."""
|
|
177
|
+
try:
|
|
178
|
+
from voxcity.utils.lc import get_land_cover_classes
|
|
179
|
+
class_dict = get_land_cover_classes(land_cover_source)
|
|
180
|
+
class_names = list(class_dict.values())
|
|
181
|
+
except Exception:
|
|
182
|
+
# Fallback: no class list; return empty so default is used
|
|
183
|
+
class_names = []
|
|
184
|
+
|
|
185
|
+
name_to_code = _get_source_name_mapping(land_cover_source)
|
|
186
|
+
index_to_code = {}
|
|
187
|
+
for idx, class_name in enumerate(class_names):
|
|
188
|
+
index_to_code[idx] = name_to_code.get(class_name, 4)
|
|
189
|
+
return index_to_code, class_names
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def export_topog(building_height_grid, building_id_grid, output_path,
|
|
193
|
+
building_material='default'):
|
|
194
|
+
"""
|
|
195
|
+
Export topog.txt file for CityLES
|
|
196
|
+
|
|
197
|
+
Parameters:
|
|
198
|
+
-----------
|
|
199
|
+
building_height_grid : numpy.ndarray
|
|
200
|
+
2D array of building heights
|
|
201
|
+
building_id_grid : numpy.ndarray
|
|
202
|
+
2D array of building IDs
|
|
203
|
+
output_path : Path
|
|
204
|
+
Output directory path
|
|
205
|
+
building_material : str
|
|
206
|
+
Building material type for mapping
|
|
207
|
+
"""
|
|
208
|
+
filename = output_path / 'topog.txt'
|
|
209
|
+
|
|
210
|
+
ny, nx = building_height_grid.shape
|
|
211
|
+
material_code = BUILDING_MATERIAL_MAPPING.get(building_material,
|
|
212
|
+
BUILDING_MATERIAL_MAPPING['default'])
|
|
213
|
+
|
|
214
|
+
# Write all grid cells including those without buildings
|
|
215
|
+
n_buildings = ny * nx
|
|
216
|
+
|
|
217
|
+
with open(filename, 'w') as f:
|
|
218
|
+
# Write number of buildings
|
|
219
|
+
f.write(f"{n_buildings}\n")
|
|
220
|
+
|
|
221
|
+
# Write data for ALL grid points (buildings and non-buildings)
|
|
222
|
+
for j in range(ny):
|
|
223
|
+
for i in range(nx):
|
|
224
|
+
# CityLES uses 1-based indexing
|
|
225
|
+
i_1based = i + 1
|
|
226
|
+
j_1based = j + 1
|
|
227
|
+
height = float(building_height_grid[j, i])
|
|
228
|
+
# Format: i j height material_code depth1 depth2 changed_material
|
|
229
|
+
f.write(f"{i_1based} {j_1based} {height:.1f} {material_code} 0.0 0.0 102\n")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def export_landuse(land_cover_grid, output_path, land_cover_source=None):
|
|
233
|
+
"""
|
|
234
|
+
Export landuse.txt file for CityLES
|
|
235
|
+
|
|
236
|
+
Parameters:
|
|
237
|
+
-----------
|
|
238
|
+
land_cover_grid : numpy.ndarray
|
|
239
|
+
2D array of land cover values (may be raw or converted)
|
|
240
|
+
output_path : Path
|
|
241
|
+
Output directory path
|
|
242
|
+
land_cover_source : str, optional
|
|
243
|
+
Source of land cover data
|
|
244
|
+
"""
|
|
245
|
+
filename = output_path / 'landuse.txt'
|
|
246
|
+
|
|
247
|
+
ny, nx = land_cover_grid.shape
|
|
248
|
+
|
|
249
|
+
# Build per-source index mapping
|
|
250
|
+
index_to_code, class_names = _build_index_to_cityles_map(land_cover_source)
|
|
251
|
+
|
|
252
|
+
print(f"Land cover source: {land_cover_source} (raw indices)")
|
|
253
|
+
|
|
254
|
+
# Create mapping statistics
|
|
255
|
+
mapping_stats = {}
|
|
256
|
+
|
|
257
|
+
with open(filename, 'w') as f:
|
|
258
|
+
# Write in row-major order (j varies first, then i)
|
|
259
|
+
for j in range(ny):
|
|
260
|
+
for i in range(nx):
|
|
261
|
+
idx = int(land_cover_grid[j, i])
|
|
262
|
+
cityles_code = index_to_code.get(idx, 4)
|
|
263
|
+
f.write(f"{cityles_code}\n")
|
|
264
|
+
|
|
265
|
+
# Track mapping statistics
|
|
266
|
+
if idx not in mapping_stats:
|
|
267
|
+
mapping_stats[idx] = {'cityles_code': cityles_code, 'count': 0}
|
|
268
|
+
mapping_stats[idx]['count'] += 1
|
|
269
|
+
|
|
270
|
+
# Print mapping summary
|
|
271
|
+
print("\nLand cover mapping summary (by source class):")
|
|
272
|
+
total = ny * nx
|
|
273
|
+
for idx in sorted(mapping_stats.keys()):
|
|
274
|
+
stats = mapping_stats[idx]
|
|
275
|
+
percentage = (stats['count'] / total) * 100
|
|
276
|
+
class_name = class_names[idx] if 0 <= idx < len(class_names) else 'Unknown'
|
|
277
|
+
print(f" {idx}: {class_name} -> CityLES {stats['cityles_code']}: "
|
|
278
|
+
f"{stats['count']} cells ({percentage:.1f}%)")
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def export_dem(dem_grid, output_path):
|
|
282
|
+
"""
|
|
283
|
+
Export dem.txt file for CityLES
|
|
284
|
+
|
|
285
|
+
Parameters:
|
|
286
|
+
-----------
|
|
287
|
+
dem_grid : numpy.ndarray
|
|
288
|
+
2D array of elevation values
|
|
289
|
+
output_path : Path
|
|
290
|
+
Output directory path
|
|
291
|
+
"""
|
|
292
|
+
filename = output_path / 'dem.txt'
|
|
293
|
+
|
|
294
|
+
ny, nx = dem_grid.shape
|
|
295
|
+
|
|
296
|
+
with open(filename, 'w') as f:
|
|
297
|
+
for j in range(ny):
|
|
298
|
+
for i in range(nx):
|
|
299
|
+
# CityLES uses 1-based indexing
|
|
300
|
+
i_1based = i + 1
|
|
301
|
+
j_1based = j + 1
|
|
302
|
+
elevation = dem_grid[j, i]
|
|
303
|
+
|
|
304
|
+
f.write(f"{i_1based} {j_1based} {elevation:.1f}\n")
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def export_vmap(canopy_height_grid, output_path, tree_base_ratio=0.3, tree_type='default'):
|
|
308
|
+
"""
|
|
309
|
+
Export vmap.txt file for CityLES
|
|
310
|
+
|
|
311
|
+
Parameters:
|
|
312
|
+
-----------
|
|
313
|
+
canopy_height_grid : numpy.ndarray
|
|
314
|
+
2D array of canopy heights
|
|
315
|
+
output_path : Path
|
|
316
|
+
Output directory path
|
|
317
|
+
tree_base_ratio : float
|
|
318
|
+
Ratio of tree base height to total canopy height
|
|
319
|
+
tree_type : str
|
|
320
|
+
Tree type for mapping
|
|
321
|
+
"""
|
|
322
|
+
filename = output_path / 'vmap.txt'
|
|
323
|
+
|
|
324
|
+
ny, nx = canopy_height_grid.shape
|
|
325
|
+
tree_code = TREE_TYPE_MAPPING.get(tree_type, TREE_TYPE_MAPPING['default'])
|
|
326
|
+
|
|
327
|
+
# Write all grid cells including those without vegetation
|
|
328
|
+
n_trees = ny * nx
|
|
329
|
+
|
|
330
|
+
with open(filename, 'w') as f:
|
|
331
|
+
# Write number of trees
|
|
332
|
+
f.write(f"{n_trees}\n")
|
|
333
|
+
|
|
334
|
+
# Write data for ALL grid points (vegetation and non-vegetation)
|
|
335
|
+
for j in range(ny):
|
|
336
|
+
for i in range(nx):
|
|
337
|
+
# CityLES uses 1-based indexing
|
|
338
|
+
i_1based = i + 1
|
|
339
|
+
j_1based = j + 1
|
|
340
|
+
total_height = float(canopy_height_grid[j, i])
|
|
341
|
+
lower_height = total_height * tree_base_ratio
|
|
342
|
+
upper_height = total_height
|
|
343
|
+
# Format: i j lower_height upper_height tree_type
|
|
344
|
+
f.write(f"{i_1based} {j_1based} {lower_height:.1f} {upper_height:.1f} {tree_code}\n")
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def export_lonlat(rectangle_vertices, grid_shape, output_path):
|
|
348
|
+
"""
|
|
349
|
+
Export lonlat.txt file for CityLES
|
|
350
|
+
|
|
351
|
+
Parameters:
|
|
352
|
+
-----------
|
|
353
|
+
rectangle_vertices : list of tuples
|
|
354
|
+
List of (lon, lat) vertices defining the area
|
|
355
|
+
grid_shape : tuple
|
|
356
|
+
Shape of the grid (ny, nx)
|
|
357
|
+
output_path : Path
|
|
358
|
+
Output directory path
|
|
359
|
+
"""
|
|
360
|
+
filename = output_path / 'lonlat.txt'
|
|
361
|
+
|
|
362
|
+
ny, nx = grid_shape
|
|
363
|
+
|
|
364
|
+
# Extract bounds from vertices
|
|
365
|
+
lons = [v[0] for v in rectangle_vertices]
|
|
366
|
+
lats = [v[1] for v in rectangle_vertices]
|
|
367
|
+
min_lon, max_lon = min(lons), max(lons)
|
|
368
|
+
min_lat, max_lat = min(lats), max(lats)
|
|
369
|
+
|
|
370
|
+
# Create coordinate grids
|
|
371
|
+
lon_vals = np.linspace(min_lon, max_lon, nx)
|
|
372
|
+
lat_vals = np.linspace(min_lat, max_lat, ny)
|
|
373
|
+
|
|
374
|
+
with open(filename, 'w') as f:
|
|
375
|
+
for j in range(ny):
|
|
376
|
+
for i in range(nx):
|
|
377
|
+
# CityLES uses 1-based indexing
|
|
378
|
+
i_1based = i + 1
|
|
379
|
+
j_1based = j + 1
|
|
380
|
+
lon = lon_vals[i]
|
|
381
|
+
lat = lat_vals[j]
|
|
382
|
+
|
|
383
|
+
# Note: Format is i j longitude latitude (not latitude longitude)
|
|
384
|
+
f.write(f"{i_1based} {j_1based} {lon:.7f} {lat:.8f}\n")
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def export_cityles(building_height_grid, building_id_grid, canopy_height_grid,
|
|
388
|
+
land_cover_grid, dem_grid, meshsize, land_cover_source,
|
|
389
|
+
rectangle_vertices, output_directory="output/cityles",
|
|
390
|
+
building_material='default', tree_type='default',
|
|
391
|
+
tree_base_ratio=0.3, **kwargs):
|
|
392
|
+
"""
|
|
393
|
+
Export VoxCity data to CityLES format
|
|
394
|
+
|
|
395
|
+
Parameters:
|
|
396
|
+
-----------
|
|
397
|
+
building_height_grid : numpy.ndarray
|
|
398
|
+
2D array of building heights
|
|
399
|
+
building_id_grid : numpy.ndarray
|
|
400
|
+
2D array of building IDs
|
|
401
|
+
canopy_height_grid : numpy.ndarray
|
|
402
|
+
2D array of canopy heights
|
|
403
|
+
land_cover_grid : numpy.ndarray
|
|
404
|
+
2D array of land cover values (may be raw or VoxCity standard)
|
|
405
|
+
dem_grid : numpy.ndarray
|
|
406
|
+
2D array of elevation values
|
|
407
|
+
meshsize : float
|
|
408
|
+
Grid cell size in meters
|
|
409
|
+
land_cover_source : str
|
|
410
|
+
Source of land cover data (e.g., 'ESRI 10m Annual Land Cover', 'ESA WorldCover')
|
|
411
|
+
rectangle_vertices : list of tuples
|
|
412
|
+
List of (lon, lat) vertices defining the area
|
|
413
|
+
output_directory : str
|
|
414
|
+
Output directory path
|
|
415
|
+
building_material : str
|
|
416
|
+
Building material type for mapping
|
|
417
|
+
tree_type : str
|
|
418
|
+
Tree type for mapping
|
|
419
|
+
tree_base_ratio : float
|
|
420
|
+
Ratio of tree base height to total canopy height
|
|
421
|
+
**kwargs : dict
|
|
422
|
+
Additional parameters (for compatibility)
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
--------
|
|
426
|
+
str : Path to output directory
|
|
427
|
+
"""
|
|
428
|
+
# Create output directory
|
|
429
|
+
output_path = create_cityles_directories(output_directory)
|
|
430
|
+
|
|
431
|
+
print(f"Exporting CityLES files to: {output_path}")
|
|
432
|
+
print(f"Land cover source: {land_cover_source}")
|
|
433
|
+
|
|
434
|
+
# Export individual files
|
|
435
|
+
print("\nExporting topog.txt...")
|
|
436
|
+
export_topog(building_height_grid, building_id_grid, output_path, building_material)
|
|
437
|
+
|
|
438
|
+
print("\nExporting landuse.txt...")
|
|
439
|
+
export_landuse(land_cover_grid, output_path, land_cover_source)
|
|
440
|
+
|
|
441
|
+
print("\nExporting dem.txt...")
|
|
442
|
+
export_dem(dem_grid, output_path)
|
|
443
|
+
|
|
444
|
+
print("\nExporting vmap.txt...")
|
|
445
|
+
export_vmap(canopy_height_grid, output_path, tree_base_ratio, tree_type)
|
|
446
|
+
|
|
447
|
+
print("\nExporting lonlat.txt...")
|
|
448
|
+
export_lonlat(rectangle_vertices, building_height_grid.shape, output_path)
|
|
449
|
+
|
|
450
|
+
# Create metadata file for reference
|
|
451
|
+
metadata_file = output_path / 'cityles_metadata.txt'
|
|
452
|
+
with open(metadata_file, 'w') as f:
|
|
453
|
+
f.write("CityLES Export Metadata\n")
|
|
454
|
+
f.write("====================\n")
|
|
455
|
+
f.write(f"Export date: 2025/08/05\n")
|
|
456
|
+
f.write(f"Grid shape: {building_height_grid.shape}\n")
|
|
457
|
+
f.write(f"Mesh size: {meshsize} m\n")
|
|
458
|
+
f.write(f"Land cover source: {land_cover_source}\n")
|
|
459
|
+
f.write(f"Building material: {building_material}\n")
|
|
460
|
+
f.write(f"Tree type: {tree_type}\n")
|
|
461
|
+
f.write(f"Bounds: {rectangle_vertices}\n")
|
|
462
|
+
f.write(f"Buildings: {np.sum(building_height_grid > 0)}\n")
|
|
463
|
+
f.write(f"Trees: {np.sum(canopy_height_grid > 0)}\n")
|
|
464
|
+
|
|
465
|
+
# Add land use value ranges
|
|
466
|
+
f.write(f"\nLand cover value range: {land_cover_grid.min()} - {land_cover_grid.max()}\n")
|
|
467
|
+
unique_values = np.unique(land_cover_grid)
|
|
468
|
+
f.write(f"Unique land cover values: {unique_values}\n")
|
|
469
|
+
|
|
470
|
+
print(f"\nCityLES export completed successfully!")
|
|
471
|
+
return str(output_path)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
# Helper function to apply VoxCity's convert_land_cover if needed
|
|
475
|
+
def ensure_converted_land_cover(land_cover_grid, land_cover_source):
|
|
476
|
+
"""
|
|
477
|
+
Ensure land cover grid uses VoxCity standard indices
|
|
478
|
+
|
|
479
|
+
This function checks if the land cover data needs conversion and applies
|
|
480
|
+
VoxCity's convert_land_cover function if necessary.
|
|
481
|
+
|
|
482
|
+
Parameters:
|
|
483
|
+
-----------
|
|
484
|
+
land_cover_grid : numpy.ndarray
|
|
485
|
+
2D array of land cover values
|
|
486
|
+
land_cover_source : str
|
|
487
|
+
Source of land cover data
|
|
488
|
+
|
|
489
|
+
Returns:
|
|
490
|
+
--------
|
|
491
|
+
numpy.ndarray : Land cover grid with VoxCity standard indices (0-13)
|
|
492
|
+
"""
|
|
493
|
+
# Import VoxCity's convert function if available
|
|
494
|
+
try:
|
|
495
|
+
from voxcity.utils.lc import convert_land_cover
|
|
496
|
+
|
|
497
|
+
# Apply conversion
|
|
498
|
+
converted_grid = convert_land_cover(land_cover_grid, land_cover_source)
|
|
499
|
+
print(f"Applied VoxCity land cover conversion for {land_cover_source}")
|
|
500
|
+
return converted_grid
|
|
501
|
+
except ImportError:
|
|
502
|
+
print("Warning: Could not import VoxCity land cover utilities. Using direct mapping.")
|
|
503
|
+
return land_cover_grid
|
|
@@ -147,56 +147,37 @@ def group_and_label_cells(array):
|
|
|
147
147
|
|
|
148
148
|
def process_grid_optimized(grid_bi, dem_grid):
|
|
149
149
|
"""
|
|
150
|
-
Optimized version
|
|
151
|
-
|
|
150
|
+
Optimized version that computes per-building averages without allocating
|
|
151
|
+
huge arrays when building IDs are large and sparse.
|
|
152
152
|
"""
|
|
153
153
|
result = dem_grid.copy()
|
|
154
|
-
|
|
154
|
+
|
|
155
155
|
# Only process if there are non-zero values
|
|
156
156
|
if np.any(grid_bi != 0):
|
|
157
|
-
#
|
|
158
|
-
# First check if we have float values
|
|
157
|
+
# Convert to integer IDs (handle NaN for float arrays)
|
|
159
158
|
if grid_bi.dtype.kind == 'f':
|
|
160
|
-
# Convert to int, handling NaN values
|
|
161
159
|
grid_bi_int = np.nan_to_num(grid_bi, nan=0).astype(np.int64)
|
|
162
160
|
else:
|
|
163
161
|
grid_bi_int = grid_bi.astype(np.int64)
|
|
164
|
-
|
|
165
|
-
#
|
|
166
|
-
|
|
162
|
+
|
|
163
|
+
# Work only on non-zero cells
|
|
164
|
+
flat_ids = grid_bi_int.ravel()
|
|
167
165
|
flat_dem = dem_grid.ravel()
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
sums = np.bincount(ids, weights=values)
|
|
178
|
-
counts = np.bincount(ids)
|
|
179
|
-
|
|
180
|
-
# Avoid division by zero
|
|
166
|
+
nz_mask = flat_ids != 0
|
|
167
|
+
if np.any(nz_mask):
|
|
168
|
+
ids_nz = flat_ids[nz_mask]
|
|
169
|
+
vals_nz = flat_dem[nz_mask]
|
|
170
|
+
|
|
171
|
+
# Densify IDs via inverse indices to avoid np.bincount on large max(id)
|
|
172
|
+
unique_ids, inverse_idx = np.unique(ids_nz, return_inverse=True)
|
|
173
|
+
sums = np.bincount(inverse_idx, weights=vals_nz)
|
|
174
|
+
counts = np.bincount(inverse_idx)
|
|
181
175
|
counts[counts == 0] = 1
|
|
182
|
-
|
|
183
|
-
# Calculate means
|
|
184
176
|
means = sums / counts
|
|
185
|
-
|
|
186
|
-
#
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
# Ensure indices are within bounds
|
|
190
|
-
valid_ids = grid_bi_int[mask_nonzero]
|
|
191
|
-
valid_mask = valid_ids < len(means)
|
|
192
|
-
|
|
193
|
-
# Apply only to valid indices
|
|
194
|
-
result_mask = np.zeros_like(mask_nonzero)
|
|
195
|
-
result_mask[mask_nonzero] = valid_mask
|
|
196
|
-
|
|
197
|
-
# Set values
|
|
198
|
-
result[result_mask] = means[grid_bi_int[result_mask]]
|
|
199
|
-
|
|
177
|
+
|
|
178
|
+
# Scatter means back to result for non-zero cells
|
|
179
|
+
result.ravel()[nz_mask] = means[inverse_idx]
|
|
180
|
+
|
|
200
181
|
return result - np.min(result)
|
|
201
182
|
|
|
202
183
|
def process_grid(grid_bi, dem_grid):
|