voxcity 0.6.2__py3-none-any.whl → 0.6.3__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.

Potentially problematic release.


This version of voxcity might be problematic. Click here for more details.

@@ -1,28 +1,14 @@
1
1
  """
2
- CityLES Exporter Module for VoxCity
3
-
4
- This module provides functionality to export VoxCity grid data to the CityLES input file format.
5
- CityLES is a large-eddy simulation (LES) model for urban environments, requiring specific input files
6
- describing land use, building geometry, vegetation, and terrain.
7
-
8
- Key Features:
9
- - Converts VoxCity grids to CityLES-compatible input files (topog.txt, landuse.txt, dem.txt, vmap.txt, lonlat.txt)
10
- - Handles land cover, building heights, canopy heights, and digital elevation models
11
- - Supports flexible mapping from land cover and building types to CityLES codes
12
- - Generates all required text files and metadata for CityLES runs
13
-
14
- Main Functions:
15
- - export_cityles: Main function to export all required CityLES input files
16
- - export_topog: Exports building geometry (topog.txt)
17
- - export_landuse: Exports land use grid (landuse.txt)
18
- - export_dem: Exports digital elevation model (dem.txt)
19
- - export_vmap: Exports vegetation map (vmap.txt)
20
- - export_lonlat: Exports longitude/latitude grid (lonlat.txt)
21
-
22
- Dependencies:
23
- - numpy: For array operations
24
- - pathlib: For file and directory management
25
- - os: For file system operations
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'.
26
12
  """
27
13
 
28
14
  import os
@@ -30,47 +16,134 @@ import numpy as np
30
16
  from pathlib import Path
31
17
 
32
18
 
33
- # Land cover to CityLES land use mapping
34
- # Based on common land cover classifications to CityLES codes
35
- LANDCOVER_TO_CITYLES_LANDUSE = {
36
- # Built-up areas
37
- 'building': 4, # Concrete building
38
- 'road': 2, # High reflective asphalt without AH
39
- 'parking': 2, # High reflective asphalt without AH
40
- 'pavement': 11, # Concrete (proxy of block)
41
-
42
- # Vegetation
43
- 'grass': 10, # Grassland
44
- 'forest': 16, # Deciduous broadleaf forest
45
- 'tree': 16, # Deciduous broadleaf forest
46
- 'agriculture': 7, # Dryland cropland and pasture
47
- 'cropland': 7, # Dryland cropland and pasture
48
- 'paddy': 6, # Paddy
49
-
50
- # Water and bare land
51
- 'water': 9, # Water
52
- 'bare_soil': 8, # Barren or sparsely vegetated
53
- 'sand': 8, # Barren or sparsely vegetated
54
-
55
- # Default
56
- 'default': 10 # Grassland as default
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
57
58
  }
58
59
 
59
- # Building material mapping
60
- # Maps building types to CityLES building attribute codes
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
61
130
  BUILDING_MATERIAL_MAPPING = {
62
- 'concrete': 104, # Concrete building
63
- 'residential': 105, # Slate roof (ordinal wooden house)
64
- 'commercial': 104, # Concrete building
65
- 'industrial': 104, # Concrete building
66
- 'default': 104 # Default to concrete building
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
67
138
  }
68
139
 
69
- # Tree type mapping
140
+ # Tree type mapping for vmap.txt
70
141
  TREE_TYPE_MAPPING = {
71
- 'deciduous': 101, # Leaf
72
- 'evergreen': 101, # Leaf (simplified)
73
- 'default': 101 # Default to leaf
142
+ 'deciduous': 101, # Leaf
143
+ 'evergreen': 101, # Leaf (simplified)
144
+ 'leaf': 101, # Leaf
145
+ 'shade': 102, # Shade
146
+ 'default': 101 # Default to leaf
74
147
  }
75
148
 
76
149
 
@@ -81,44 +154,39 @@ def create_cityles_directories(output_directory):
81
154
  return output_path
82
155
 
83
156
 
84
- def get_land_use_code(land_cover_value, land_cover_source=None):
85
- """
86
- Convert land cover value to CityLES land use code
87
-
88
- Parameters:
89
- -----------
90
- land_cover_value : int or str
91
- Land cover value from VoxCity
92
- land_cover_source : str, optional
93
- Source of land cover data (e.g., 'esri', 'esa', 'osm')
94
-
95
- Returns:
96
- --------
97
- int : CityLES land use code (1-17)
98
- """
99
- # If using numeric codes, you might need source-specific mappings
100
- # This is a simplified example
101
- if isinstance(land_cover_value, str):
102
- return LANDCOVER_TO_CITYLES_LANDUSE.get(land_cover_value.lower(),
103
- LANDCOVER_TO_CITYLES_LANDUSE['default'])
104
-
105
- # Example mapping for ESRI land cover (adjust based on actual data source)
106
- if land_cover_source == 'esri':
107
- esri_mapping = {
108
- 1: 9, # Water -> Water
109
- 2: 16, # Trees -> Deciduous broadleaf forest
110
- 4: 8, # Flooded vegetation -> Barren
111
- 5: 10, # Crops -> Grassland (simplified)
112
- 7: 4, # Built Area -> Concrete building
113
- 8: 8, # Bare ground -> Barren
114
- 9: 3, # Snow/Ice -> Concrete (proxy of jari)
115
- 10: 9, # Clouds -> Water (simplified)
116
- 11: 10 # Rangeland -> Grassland
117
- }
118
- return esri_mapping.get(land_cover_value, LANDCOVER_TO_CITYLES_LANDUSE['default'])
119
-
120
- # Default mapping
121
- return LANDCOVER_TO_CITYLES_LANDUSE['default']
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
122
190
 
123
191
 
124
192
  def export_topog(building_height_grid, building_id_grid, output_path,
@@ -139,26 +207,26 @@ def export_topog(building_height_grid, building_id_grid, output_path,
139
207
  """
140
208
  filename = output_path / 'topog.txt'
141
209
 
142
- # Get building positions (where height > 0)
143
- building_positions = np.argwhere(building_height_grid > 0)
144
- n_buildings = len(building_positions)
145
-
210
+ ny, nx = building_height_grid.shape
146
211
  material_code = BUILDING_MATERIAL_MAPPING.get(building_material,
147
212
  BUILDING_MATERIAL_MAPPING['default'])
148
213
 
214
+ # Write all grid cells including those without buildings
215
+ n_buildings = ny * nx
216
+
149
217
  with open(filename, 'w') as f:
150
218
  # Write number of buildings
151
219
  f.write(f"{n_buildings}\n")
152
220
 
153
- # Write building data
154
- for idx, (j, i) in enumerate(building_positions):
155
- # CityLES uses 1-based indexing
156
- i_1based = i + 1
157
- j_1based = j + 1
158
- height = building_height_grid[j, i]
159
-
160
- # Format: i j height material_code depth1 depth2 changed_material
161
- f.write(f"{i_1based} {j_1based} {height:.1f} {material_code} 0.0 0.0 102\n")
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")
162
230
 
163
231
 
164
232
  def export_landuse(land_cover_grid, output_path, land_cover_source=None):
@@ -168,7 +236,7 @@ def export_landuse(land_cover_grid, output_path, land_cover_source=None):
168
236
  Parameters:
169
237
  -----------
170
238
  land_cover_grid : numpy.ndarray
171
- 2D array of land cover values
239
+ 2D array of land cover values (may be raw or converted)
172
240
  output_path : Path
173
241
  Output directory path
174
242
  land_cover_source : str, optional
@@ -176,13 +244,38 @@ def export_landuse(land_cover_grid, output_path, land_cover_source=None):
176
244
  """
177
245
  filename = output_path / 'landuse.txt'
178
246
 
179
- # Flatten the grid and convert to CityLES codes
180
- flat_grid = land_cover_grid.flatten()
181
-
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
+
182
257
  with open(filename, 'w') as f:
183
- for value in flat_grid:
184
- cityles_code = get_land_use_code(value, land_cover_source)
185
- f.write(f"{cityles_code}\n")
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}%)")
186
279
 
187
280
 
188
281
  def export_dem(dem_grid, output_path):
@@ -228,27 +321,27 @@ def export_vmap(canopy_height_grid, output_path, tree_base_ratio=0.3, tree_type=
228
321
  """
229
322
  filename = output_path / 'vmap.txt'
230
323
 
231
- # Get tree positions (where canopy height > 0)
232
- tree_positions = np.argwhere(canopy_height_grid > 0)
233
- n_trees = len(tree_positions)
234
-
324
+ ny, nx = canopy_height_grid.shape
235
325
  tree_code = TREE_TYPE_MAPPING.get(tree_type, TREE_TYPE_MAPPING['default'])
236
326
 
327
+ # Write all grid cells including those without vegetation
328
+ n_trees = ny * nx
329
+
237
330
  with open(filename, 'w') as f:
238
331
  # Write number of trees
239
332
  f.write(f"{n_trees}\n")
240
333
 
241
- # Write tree data
242
- for idx, (j, i) in enumerate(tree_positions):
243
- # CityLES uses 1-based indexing
244
- i_1based = i + 1
245
- j_1based = j + 1
246
- total_height = canopy_height_grid[j, i]
247
- lower_height = total_height * tree_base_ratio
248
- upper_height = total_height
249
-
250
- # Format: i j lower_height upper_height tree_type
251
- f.write(f"{i_1based} {j_1based} {lower_height:.1f} {upper_height:.1f} {tree_code}\n")
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")
252
345
 
253
346
 
254
347
  def export_lonlat(rectangle_vertices, grid_shape, output_path):
@@ -287,6 +380,7 @@ def export_lonlat(rectangle_vertices, grid_shape, output_path):
287
380
  lon = lon_vals[i]
288
381
  lat = lat_vals[j]
289
382
 
383
+ # Note: Format is i j longitude latitude (not latitude longitude)
290
384
  f.write(f"{i_1based} {j_1based} {lon:.7f} {lat:.8f}\n")
291
385
 
292
386
 
@@ -307,13 +401,13 @@ def export_cityles(building_height_grid, building_id_grid, canopy_height_grid,
307
401
  canopy_height_grid : numpy.ndarray
308
402
  2D array of canopy heights
309
403
  land_cover_grid : numpy.ndarray
310
- 2D array of land cover values
404
+ 2D array of land cover values (may be raw or VoxCity standard)
311
405
  dem_grid : numpy.ndarray
312
406
  2D array of elevation values
313
407
  meshsize : float
314
408
  Grid cell size in meters
315
409
  land_cover_source : str
316
- Source of land cover data (e.g., 'esri', 'esa', 'osm')
410
+ Source of land cover data (e.g., 'ESRI 10m Annual Land Cover', 'ESA WorldCover')
317
411
  rectangle_vertices : list of tuples
318
412
  List of (lon, lat) vertices defining the area
319
413
  output_directory : str
@@ -335,21 +429,22 @@ def export_cityles(building_height_grid, building_id_grid, canopy_height_grid,
335
429
  output_path = create_cityles_directories(output_directory)
336
430
 
337
431
  print(f"Exporting CityLES files to: {output_path}")
432
+ print(f"Land cover source: {land_cover_source}")
338
433
 
339
434
  # Export individual files
340
- print("Exporting topog.txt...")
435
+ print("\nExporting topog.txt...")
341
436
  export_topog(building_height_grid, building_id_grid, output_path, building_material)
342
437
 
343
- print("Exporting landuse.txt...")
438
+ print("\nExporting landuse.txt...")
344
439
  export_landuse(land_cover_grid, output_path, land_cover_source)
345
440
 
346
- print("Exporting dem.txt...")
441
+ print("\nExporting dem.txt...")
347
442
  export_dem(dem_grid, output_path)
348
443
 
349
- print("Exporting vmap.txt...")
444
+ print("\nExporting vmap.txt...")
350
445
  export_vmap(canopy_height_grid, output_path, tree_base_ratio, tree_type)
351
446
 
352
- print("Exporting lonlat.txt...")
447
+ print("\nExporting lonlat.txt...")
353
448
  export_lonlat(rectangle_vertices, building_height_grid.shape, output_path)
354
449
 
355
450
  # Create metadata file for reference
@@ -357,12 +452,52 @@ def export_cityles(building_height_grid, building_id_grid, canopy_height_grid,
357
452
  with open(metadata_file, 'w') as f:
358
453
  f.write("CityLES Export Metadata\n")
359
454
  f.write("====================\n")
455
+ f.write(f"Export date: 2025/08/05\n")
360
456
  f.write(f"Grid shape: {building_height_grid.shape}\n")
361
457
  f.write(f"Mesh size: {meshsize} m\n")
362
458
  f.write(f"Land cover source: {land_cover_source}\n")
363
459
  f.write(f"Building material: {building_material}\n")
364
460
  f.write(f"Tree type: {tree_type}\n")
365
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")
366
469
 
367
- print(f"CityLES export completed successfully!")
368
- return str(output_path)
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