voxcity 0.6.2__tar.gz → 0.6.4__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.

Files changed (54) hide show
  1. {voxcity-0.6.2 → voxcity-0.6.4}/PKG-INFO +3 -3
  2. {voxcity-0.6.2 → voxcity-0.6.4}/pyproject.toml +3 -3
  3. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity/downloader/gee.py +19 -14
  4. voxcity-0.6.4/src/voxcity/exporter/cityles.py +503 -0
  5. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity/generator.py +1073 -1073
  6. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity/geoprocessor/mesh.py +790 -790
  7. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity/geoprocessor/utils.py +820 -785
  8. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity/simulator/solar.py +101 -25
  9. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity/simulator/view.py +2238 -2238
  10. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity/utils/visualization.py +28 -2
  11. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity.egg-info/PKG-INFO +3 -3
  12. voxcity-0.6.2/src/voxcity/exporter/cityles.py +0 -368
  13. {voxcity-0.6.2 → voxcity-0.6.4}/AUTHORS.rst +0 -0
  14. {voxcity-0.6.2 → voxcity-0.6.4}/CONTRIBUTING.rst +0 -0
  15. {voxcity-0.6.2 → voxcity-0.6.4}/HISTORY.rst +0 -0
  16. {voxcity-0.6.2 → voxcity-0.6.4}/LICENSE +0 -0
  17. {voxcity-0.6.2 → voxcity-0.6.4}/MANIFEST.in +0 -0
  18. {voxcity-0.6.2 → voxcity-0.6.4}/README.md +0 -0
  19. {voxcity-0.6.2 → voxcity-0.6.4}/docs/Makefile +0 -0
  20. {voxcity-0.6.2 → voxcity-0.6.4}/docs/_static/logo.png +0 -0
  21. {voxcity-0.6.2 → voxcity-0.6.4}/docs/conf.py +0 -0
  22. {voxcity-0.6.2 → voxcity-0.6.4}/docs/logo.png +0 -0
  23. {voxcity-0.6.2 → voxcity-0.6.4}/docs/make.bat +0 -0
  24. {voxcity-0.6.2 → voxcity-0.6.4}/setup.cfg +0 -0
  25. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity/__init__.py +0 -0
  26. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity/downloader/__init__.py +0 -0
  27. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity/downloader/citygml.py +0 -0
  28. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity/downloader/eubucco.py +0 -0
  29. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity/downloader/mbfp.py +0 -0
  30. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity/downloader/oemj.py +0 -0
  31. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity/downloader/osm.py +0 -0
  32. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity/downloader/overture.py +0 -0
  33. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity/downloader/utils.py +0 -0
  34. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity/exporter/__init__.py +0 -0
  35. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity/exporter/envimet.py +0 -0
  36. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity/exporter/magicavoxel.py +0 -0
  37. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity/exporter/obj.py +0 -0
  38. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity/geoprocessor/__init__.py +0 -0
  39. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity/geoprocessor/draw.py +0 -0
  40. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity/geoprocessor/grid.py +0 -0
  41. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity/geoprocessor/network.py +0 -0
  42. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity/geoprocessor/polygon.py +0 -0
  43. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity/simulator/__init__.py +0 -0
  44. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity/simulator/utils.py +0 -0
  45. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity/utils/__init__.py +0 -0
  46. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity/utils/lc.py +0 -0
  47. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity/utils/material.py +0 -0
  48. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity/utils/weather.py +0 -0
  49. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity.egg-info/SOURCES.txt +0 -0
  50. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity.egg-info/dependency_links.txt +0 -0
  51. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity.egg-info/requires.txt +0 -0
  52. {voxcity-0.6.2 → voxcity-0.6.4}/src/voxcity.egg-info/top_level.txt +0 -0
  53. {voxcity-0.6.2 → voxcity-0.6.4}/tests/__init__.py +0 -0
  54. {voxcity-0.6.2 → voxcity-0.6.4}/tests/voxelcity.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voxcity
3
- Version: 0.6.2
3
+ Version: 0.6.4
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
- Author-email: Kunihiko Fujiwara <kunihiko@nus.edu.sg>
6
- Maintainer-email: Kunihiko Fujiwara <kunihiko@nus.edu.sg>
5
+ Author-email: Kunihiko Fujiwara <fujiwara.kunihiko@takenaka.co.jp>
6
+ Maintainer-email: Kunihiko Fujiwara <fujiwara.kunihiko@takenaka.co.jp>
7
7
  License: MIT
8
8
  Project-URL: bugs, https://github.com/kunifujiwara/voxcity/issues
9
9
  Project-URL: changelog, https://github.com/kunifujiwara/voxcity/blob/master/changelog.md
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "voxcity"
3
- version = "0.6.2"
3
+ version = "0.6.4"
4
4
  requires-python = ">=3.10,<3.13"
5
5
  classifiers = [
6
6
  "Programming Language :: Python :: 3.10",
@@ -10,10 +10,10 @@ classifiers = [
10
10
  description = "voxcity is an easy and one-stop tool to output 3d city models for microclimate simulation by integrating multiple geospatial open-data"
11
11
  readme = "README.md"
12
12
  authors = [
13
- {name = "Kunihiko Fujiwara", email = "kunihiko@nus.edu.sg"}
13
+ {name = "Kunihiko Fujiwara", email = "fujiwara.kunihiko@takenaka.co.jp"}
14
14
  ]
15
15
  maintainers = [
16
- {name = "Kunihiko Fujiwara", email = "kunihiko@nus.edu.sg"}
16
+ {name = "Kunihiko Fujiwara", email = "fujiwara.kunihiko@takenaka.co.jp"}
17
17
  ]
18
18
  license = {text = "MIT"}
19
19
  dependencies = [
@@ -26,16 +26,21 @@ import geemap
26
26
  # Local imports
27
27
  # from ..geo.utils import convert_format_lat_lon
28
28
 
29
- def initialize_earth_engine():
30
- """Initialize the Earth Engine API.
31
-
32
- This function must be called before using any other Earth Engine functionality.
33
- It assumes that Earth Engine authentication has been set up properly.
34
-
35
- Raises:
36
- ee.EEException: If authentication fails or Earth Engine is unavailable
29
+ def initialize_earth_engine(**initialize_kwargs):
30
+ """Initialize the Earth Engine API if not already initialized.
31
+
32
+ Uses a public-behavior check to determine whether Earth Engine is already
33
+ initialized by attempting to access asset roots. If that call fails, it will
34
+ initialize Earth Engine using the provided keyword arguments.
35
+
36
+ Arguments are passed through to ``ee.Initialize`` to support contexts such as
37
+ specifying a ``project`` or service account credentials.
37
38
  """
38
- ee.Initialize()
39
+ try:
40
+ # If this succeeds, EE is already initialized
41
+ ee.data.getAssetRoots()
42
+ except Exception:
43
+ ee.Initialize(**initialize_kwargs)
39
44
 
40
45
  def get_roi(input_coords):
41
46
  """Create an Earth Engine region of interest polygon from coordinates.
@@ -243,7 +248,7 @@ def save_geotiff_esa_land_cover(roi, geotiff_path):
243
248
  resolution of the ESA WorldCover dataset.
244
249
  """
245
250
  # Initialize Earth Engine
246
- ee.Initialize()
251
+ initialize_earth_engine()
247
252
 
248
253
  # Load and clip the ESA WorldCover dataset
249
254
  esa = ee.ImageCollection("ESA/WorldCover/v200").first()
@@ -307,7 +312,7 @@ def save_geotiff_dynamic_world_v1(roi, geotiff_path, date=None):
307
312
  actual date used.
308
313
  """
309
314
  # Initialize Earth Engine
310
- ee.Initialize()
315
+ initialize_earth_engine()
311
316
 
312
317
  # Load and filter Dynamic World dataset
313
318
  # Load the Dynamic World dataset and filter by ROI
@@ -416,7 +421,7 @@ def save_geotiff_esri_landcover(roi, geotiff_path, year=None):
416
421
  differ from the requested year if data is not available for that time.
417
422
  """
418
423
  # Initialize Earth Engine
419
- ee.Initialize()
424
+ initialize_earth_engine()
420
425
 
421
426
  # Load the ESRI Land Cover dataset and filter by ROI
422
427
  esri_lulc = ee.ImageCollection("projects/sat-io/open-datasets/landcover/ESRI_Global-LULC_10m_TS").filterBounds(roi)
@@ -508,7 +513,7 @@ def save_geotiff_open_buildings_temporal(aoi, geotiff_path):
508
513
  - Areas without buildings will have no-data values
509
514
  """
510
515
  # Initialize Earth Engine
511
- ee.Initialize()
516
+ initialize_earth_engine()
512
517
 
513
518
  # Load the dataset
514
519
  collection = ee.ImageCollection('GOOGLE/Research/open-buildings-temporal/v1')
@@ -554,7 +559,7 @@ def save_geotiff_dsm_minus_dtm(roi, geotiff_path, meshsize, source):
554
559
  - The function requires both DSM and DTM data to be available for the region
555
560
  """
556
561
  # Initialize Earth Engine
557
- ee.Initialize()
562
+ initialize_earth_engine()
558
563
 
559
564
  # Add buffer around ROI to ensure smooth interpolation at edges
560
565
  buffer_distance = 100
@@ -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