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

voxcity/utils/lc.py CHANGED
@@ -1,12 +1,40 @@
1
+ """
2
+ Land Cover Classification Utilities for VoxelCity
3
+
4
+ This module provides utilities for handling land cover data from various sources,
5
+ including color-based classification, data conversion between different land cover
6
+ classification systems, and spatial analysis of land cover polygons.
7
+
8
+ Supported land cover data sources:
9
+ - Urbanwatch
10
+ - OpenEarthMapJapan
11
+ - ESRI 10m Annual Land Cover
12
+ - ESA WorldCover
13
+ - Dynamic World V1
14
+ - OpenStreetMap
15
+ - Standard classification
16
+ """
17
+
1
18
  import numpy as np
2
19
  from shapely.geometry import Polygon
3
20
  from rtree import index
4
21
  from collections import Counter
5
22
 
6
23
  def rgb_distance(color1, color2):
24
+ """
25
+ Calculate the Euclidean distance between two RGB colors.
26
+
27
+ Args:
28
+ color1 (tuple): RGB values as (R, G, B) tuple
29
+ color2 (tuple): RGB values as (R, G, B) tuple
30
+
31
+ Returns:
32
+ float: Euclidean distance between the two colors
33
+ """
7
34
  return np.sqrt(np.sum((np.array(color1) - np.array(color2))**2))
8
35
 
9
36
 
37
+ # Legacy land cover classes mapping - kept for reference
10
38
  # land_cover_classes = {
11
39
  # (128, 0, 0): 'Bareland', 0
12
40
  # (0, 255, 36): 'Rangeland', 1
@@ -24,7 +52,27 @@ def rgb_distance(color1, color2):
24
52
  # }
25
53
 
26
54
  def get_land_cover_classes(source):
55
+ """
56
+ Get land cover classification mapping for a specific data source.
57
+
58
+ Each data source has its own color-to-class mapping system. This function
59
+ returns the appropriate RGB color to land cover class dictionary based on
60
+ the specified source.
61
+
62
+ Args:
63
+ source (str): Name of the land cover data source. Supported sources:
64
+ "Urbanwatch", "OpenEarthMapJapan", "ESRI 10m Annual Land Cover",
65
+ "ESA WorldCover", "Dynamic World V1", "Standard", "OpenStreetMap"
66
+
67
+ Returns:
68
+ dict: Dictionary mapping RGB tuples to land cover class names
69
+
70
+ Example:
71
+ >>> classes = get_land_cover_classes("Urbanwatch")
72
+ >>> print(classes[(255, 0, 0)]) # Returns 'Building'
73
+ """
27
74
  if source == "Urbanwatch":
75
+ # Urbanwatch color scheme - focused on urban features
28
76
  land_cover_classes = {
29
77
  (255, 0, 0): 'Building',
30
78
  (133, 133, 133): 'Road',
@@ -38,6 +86,7 @@ def get_land_cover_classes(source):
38
86
  (0, 0, 0): 'Sea'
39
87
  }
40
88
  elif (source == "OpenEarthMapJapan"):
89
+ # OpenEarthMap Japan specific classification
41
90
  land_cover_classes = {
42
91
  (128, 0, 0): 'Bareland',
43
92
  (0, 255, 36): 'Rangeland',
@@ -49,6 +98,7 @@ def get_land_cover_classes(source):
49
98
  (222, 31, 7): 'Building'
50
99
  }
51
100
  elif source == "ESRI 10m Annual Land Cover":
101
+ # ESRI's global 10-meter resolution land cover classification
52
102
  land_cover_classes = {
53
103
  (255, 255, 255): 'No Data',
54
104
  (26, 91, 171): 'Water',
@@ -63,6 +113,7 @@ def get_land_cover_classes(source):
63
113
  (200, 200, 200): 'Clouds'
64
114
  }
65
115
  elif source == "ESA WorldCover":
116
+ # European Space Agency WorldCover 10m classification
66
117
  land_cover_classes = {
67
118
  (0, 112, 0): 'Trees',
68
119
  (255, 224, 80): 'Shrubland',
@@ -77,6 +128,7 @@ def get_land_cover_classes(source):
77
128
  (255, 255, 0): 'Moss and lichen'
78
129
  }
79
130
  elif source == "Dynamic World V1":
131
+ # Google's Dynamic World near real-time land cover
80
132
  # Convert hex colors to RGB tuples
81
133
  land_cover_classes = {
82
134
  (65, 155, 223): 'Water', # #419bdf
@@ -90,6 +142,7 @@ def get_land_cover_classes(source):
90
142
  (179, 159, 225): 'Snow and Ice' # #b39fe1
91
143
  }
92
144
  elif (source == 'Standard') or (source == "OpenStreetMap"):
145
+ # Standard/OpenStreetMap classification - comprehensive land cover types
93
146
  land_cover_classes = {
94
147
  (128, 0, 0): 'Bareland',
95
148
  (0, 255, 36): 'Rangeland',
@@ -108,6 +161,7 @@ def get_land_cover_classes(source):
108
161
  }
109
162
  return land_cover_classes
110
163
 
164
+ # Legacy land cover classes with numeric indices - kept for reference
111
165
  # land_cover_classes = {
112
166
  # (128, 0, 0): 'Bareland', 0
113
167
  # (0, 255, 36): 'Rangeland', 1
@@ -128,6 +182,37 @@ def get_land_cover_classes(source):
128
182
 
129
183
 
130
184
  def convert_land_cover(input_array, land_cover_source='Urbanwatch'):
185
+ """
186
+ Convert land cover classification from source-specific indices to standardized indices.
187
+
188
+ This function maps land cover classes from various data sources to a standardized
189
+ classification system. Each source has different class definitions and indices,
190
+ so this conversion enables consistent processing across different data sources.
191
+
192
+ Args:
193
+ input_array (numpy.ndarray): Input array with source-specific land cover indices
194
+ land_cover_source (str): Name of the source land cover classification system
195
+ Default is 'Urbanwatch'
196
+
197
+ Returns:
198
+ numpy.ndarray: Array with standardized land cover indices
199
+
200
+ Standardized Classification System:
201
+ 0: Bareland
202
+ 1: Rangeland
203
+ 2: Shrub
204
+ 3: Agriculture land
205
+ 4: Tree
206
+ 5: Moss and lichen
207
+ 6: Wet land
208
+ 7: Mangrove
209
+ 8: Water
210
+ 9: Snow and ice
211
+ 10: Developed space
212
+ 11: Road
213
+ 12: Building
214
+ 13: No Data
215
+ """
131
216
 
132
217
  if land_cover_source == 'Urbanwatch':
133
218
  # Define the mapping from Urbanwatch to new standardized classes
@@ -144,6 +229,7 @@ def convert_land_cover(input_array, land_cover_source='Urbanwatch'):
144
229
  9: 8 # Sea -> Water
145
230
  }
146
231
  elif land_cover_source == 'ESA WorldCover':
232
+ # ESA WorldCover to standardized mapping
147
233
  convert_dict = {
148
234
  0: 4, # Trees -> Tree
149
235
  1: 2, # Shrubland -> Shrub
@@ -158,6 +244,7 @@ def convert_land_cover(input_array, land_cover_source='Urbanwatch'):
158
244
  10: 5 # Moss and lichen
159
245
  }
160
246
  elif land_cover_source == "ESRI 10m Annual Land Cover":
247
+ # ESRI 10m to standardized mapping
161
248
  convert_dict = {
162
249
  0: 13, # No Data
163
250
  1: 8, # Water
@@ -172,6 +259,7 @@ def convert_land_cover(input_array, land_cover_source='Urbanwatch'):
172
259
  10: 13 # Clouds -> No Data
173
260
  }
174
261
  elif land_cover_source == "Dynamic World V1":
262
+ # Dynamic World to standardized mapping
175
263
  convert_dict = {
176
264
  0: 8, # Water
177
265
  1: 4, # Trees -> Tree
@@ -184,6 +272,7 @@ def convert_land_cover(input_array, land_cover_source='Urbanwatch'):
184
272
  8: 9 # Snow and Ice
185
273
  }
186
274
  elif land_cover_source == "OpenEarthMapJapan":
275
+ # OpenEarthMapJapan to standardized mapping
187
276
  convert_dict = {
188
277
  0: 0, # Bareland
189
278
  1: 1, # Rangeland
@@ -204,6 +293,26 @@ def convert_land_cover(input_array, land_cover_source='Urbanwatch'):
204
293
  return converted_array
205
294
 
206
295
  def get_class_priority(source):
296
+ """
297
+ Get priority rankings for land cover classes to resolve conflicts during classification.
298
+
299
+ When multiple land cover classes are present in the same area, this priority system
300
+ determines which class should take precedence. Higher priority values indicate
301
+ classes that should override lower priority classes.
302
+
303
+ Args:
304
+ source (str): Name of the land cover data source
305
+
306
+ Returns:
307
+ dict: Dictionary mapping class names to priority values (higher = more priority)
308
+
309
+ Priority Logic for OpenStreetMap:
310
+ - Built Environment: Highest priority (most definitive structures)
311
+ - Water Bodies: High priority (clearly defined features)
312
+ - Vegetation: Medium priority (managed vs natural)
313
+ - Natural Non-Vegetation: Lower priority (often default classifications)
314
+ - Uncertain/No Data: Lowest priority
315
+ """
207
316
  if source == "OpenStreetMap":
208
317
  return {
209
318
  # Built Environment (highest priority as they're most definitively mapped)
@@ -230,6 +339,7 @@ def get_class_priority(source):
230
339
  # Uncertain
231
340
  'No Data': 14 # Lowest priority as it represents uncertainty
232
341
  }
342
+ # Legacy priority system - kept for reference
233
343
  # return {
234
344
  # 'Bareland': 4,
235
345
  # 'Rangeland': 6,
@@ -242,6 +352,26 @@ def get_class_priority(source):
242
352
  # }
243
353
 
244
354
  def create_land_cover_polygons(land_cover_geojson):
355
+ """
356
+ Create polygon geometries and spatial index from land cover GeoJSON data.
357
+
358
+ This function processes GeoJSON land cover data to create Shapely polygon
359
+ geometries and builds an R-tree spatial index for efficient spatial queries.
360
+
361
+ Args:
362
+ land_cover_geojson (list): List of GeoJSON feature dictionaries containing
363
+ land cover polygons with geometry and properties
364
+
365
+ Returns:
366
+ tuple: A tuple containing:
367
+ - land_cover_polygons (list): List of tuples (polygon, class_name)
368
+ - idx (rtree.index.Index): Spatial index for efficient polygon lookup
369
+
370
+ Note:
371
+ Each GeoJSON feature should have:
372
+ - geometry.coordinates[0]: List of coordinate pairs defining the polygon
373
+ - properties.class: String indicating the land cover class
374
+ """
245
375
  land_cover_polygons = []
246
376
  idx = index.Index()
247
377
  count = 0
@@ -262,19 +392,72 @@ def create_land_cover_polygons(land_cover_geojson):
262
392
  return land_cover_polygons, idx
263
393
 
264
394
  def get_nearest_class(pixel, land_cover_classes):
395
+ """
396
+ Find the nearest land cover class for a given pixel color using RGB distance.
397
+
398
+ This function determines the most appropriate land cover class for a pixel
399
+ by finding the class with the minimum RGB color distance to the pixel's color.
400
+
401
+ Args:
402
+ pixel (tuple): RGB color values as (R, G, B) tuple
403
+ land_cover_classes (dict): Dictionary mapping RGB tuples to class names
404
+
405
+ Returns:
406
+ str: Name of the nearest land cover class
407
+
408
+ Example:
409
+ >>> classes = {(255, 0, 0): 'Building', (0, 255, 0): 'Tree'}
410
+ >>> nearest = get_nearest_class((250, 5, 5), classes)
411
+ >>> print(nearest) # Returns 'Building'
412
+ """
265
413
  distances = {class_name: rgb_distance(pixel, color)
266
414
  for color, class_name in land_cover_classes.items()}
267
415
  return min(distances, key=distances.get)
268
416
 
269
417
  def get_dominant_class(cell_data, land_cover_classes):
418
+ """
419
+ Determine the dominant land cover class in a cell based on pixel majority.
420
+
421
+ This function analyzes all pixels within a cell, classifies each pixel to its
422
+ nearest land cover class, and returns the most frequently occurring class.
423
+
424
+ Args:
425
+ cell_data (numpy.ndarray): 3D array of RGB pixel data for the cell
426
+ land_cover_classes (dict): Dictionary mapping RGB tuples to class names
427
+
428
+ Returns:
429
+ str: Name of the dominant land cover class in the cell
430
+
431
+ Note:
432
+ If the cell contains no data, returns 'No Data'
433
+ """
270
434
  if cell_data.size == 0:
271
435
  return 'No Data'
436
+ # Classify each pixel in the cell to its nearest land cover class
272
437
  pixel_classes = [get_nearest_class(tuple(pixel), land_cover_classes)
273
438
  for pixel in cell_data.reshape(-1, 3)]
439
+ # Count occurrences of each class
274
440
  class_counts = Counter(pixel_classes)
441
+ # Return the most common class
275
442
  return class_counts.most_common(1)[0][0]
276
443
 
277
444
  def convert_land_cover_array(input_array, land_cover_classes):
445
+ """
446
+ Convert an array of land cover class names to integer indices.
447
+
448
+ This function maps string-based land cover class names to integer indices
449
+ for numerical processing and storage efficiency.
450
+
451
+ Args:
452
+ input_array (numpy.ndarray): Array containing land cover class names as strings
453
+ land_cover_classes (dict): Dictionary mapping RGB tuples to class names
454
+
455
+ Returns:
456
+ numpy.ndarray: Array with integer indices corresponding to land cover classes
457
+
458
+ Note:
459
+ Classes not found in the mapping are assigned index -1
460
+ """
278
461
  # Create a mapping of class names to integers
279
462
  class_to_int = {name: i for i, name in enumerate(land_cover_classes.values())}
280
463
 
voxcity/utils/material.py CHANGED
@@ -1,8 +1,27 @@
1
+ """
2
+ Material utilities for VoxelCity voxel grid processing.
3
+
4
+ This module provides functions for setting building materials and window patterns
5
+ in 3D voxel grids based on building IDs, material types, and window ratios.
6
+ The main functionality includes:
7
+ - Material ID mapping and retrieval
8
+ - Window pattern generation based on configurable ratios
9
+ - Building material assignment from GeoDataFrame data
10
+ """
11
+
1
12
  import numpy as np
2
13
 
3
14
  def get_material_dict():
4
15
  """
5
16
  Returns a dictionary mapping material names to their corresponding ID values.
17
+
18
+ The material IDs use negative values to distinguish them from other voxel types.
19
+ Each material has a unique negative ID that can be used for material-based
20
+ rendering and analysis.
21
+
22
+ Returns:
23
+ dict: Dictionary with material names as keys and negative integer IDs as values.
24
+ Available materials: unknown, brick, wood, concrete, metal, stone, glass, plaster
6
25
  """
7
26
  return {
8
27
  "unknown": -3,
@@ -19,23 +38,38 @@ def get_modulo_numbers(window_ratio):
19
38
  """
20
39
  Determines the appropriate modulo numbers for x, y, z based on window_ratio.
21
40
 
41
+ This function creates different window patterns by returning modulo values that
42
+ control the spacing of windows in the x, y, and z dimensions. Lower window_ratio
43
+ values result in sparser window patterns (higher modulo values), while higher
44
+ ratios create denser patterns.
45
+
46
+ The function uses hash-based selection for certain ratios to introduce variety
47
+ in window patterns for buildings with similar window ratios.
48
+
22
49
  Parameters:
23
- window_ratio: float between 0 and 1.0
50
+ window_ratio (float): Value between 0 and 1.0 representing window density
24
51
 
25
52
  Returns:
26
- tuple (x_mod, y_mod, z_mod): modulo numbers for each dimension
53
+ tuple: (x_mod, y_mod, z_mod) - modulo numbers for each dimension
54
+ Higher values = sparser windows, lower values = denser windows
27
55
  """
56
+ # Very sparse windows - every 2nd position in all dimensions
28
57
  if window_ratio <= 0.125 + 0.0625: # around 0.125
29
58
  return (2, 2, 2)
59
+ # Medium-sparse windows - vary pattern across dimensions
30
60
  elif window_ratio <= 0.25 + 0.125: # around 0.25
31
61
  combinations = [(2, 2, 1), (2, 1, 2), (1, 2, 2)]
62
+ # Use hash for consistent but varied selection
32
63
  return combinations[hash(str(window_ratio)) % len(combinations)]
64
+ # Medium density windows - two dimensions sparse, one dense
33
65
  elif window_ratio <= 0.5 + 0.125: # around 0.5
34
66
  combinations = [(2, 1, 1), (1, 2, 1), (1, 1, 2)]
35
67
  return combinations[hash(str(window_ratio)) % len(combinations)]
68
+ # Dense windows - similar pattern to medium density
36
69
  elif window_ratio <= 0.75 + 0.125: # around 0.75
37
70
  combinations = [(2, 1, 1), (1, 2, 1), (1, 1, 2)]
38
71
  return combinations[hash(str(window_ratio)) % len(combinations)]
72
+ # Maximum density - windows at every position
39
73
  else: # above 0.875
40
74
  return (1, 1, 1)
41
75
 
@@ -44,64 +78,82 @@ def set_building_material_by_id(voxelcity_grid, building_id_grid_ori, ids, mark,
44
78
  Marks cells in voxelcity_grid based on building IDs and window ratio.
45
79
  Never sets glass_id to cells with maximum z index.
46
80
 
81
+ This function processes buildings by:
82
+ 1. Finding all positions matching the specified building IDs
83
+ 2. Setting the base material for all building voxels
84
+ 3. Creating window patterns based on window_ratio and modulo calculations
85
+ 4. Ensuring the top floor (maximum z) never gets windows (glass_id)
86
+
87
+ The window pattern is determined by the modulo values returned from get_modulo_numbers(),
88
+ which creates different densities and arrangements of windows based on the window_ratio.
89
+
47
90
  Parameters:
48
- voxelcity_grid: 3D numpy array
49
- building_id_grid_ori: 2D numpy array containing building IDs
50
- ids: list/array of building IDs to check
51
- mark: value to set for marked cells
52
- window_ratio: float between 0 and 1.0, determines window density:
53
- ~0.125: sparse windows (2,2,2)
54
- ~0.25: medium-sparse windows (2,2,1), (2,1,2), or (1,2,2)
55
- ~0.5: medium windows (2,1,1), (1,2,1), or (1,1,2)
56
- ~0.75: dense windows (2,1,1), (1,2,1), or (1,1,2)
57
- >0.875: maximum density (1,1,1)
58
- glass_id: value to set for glass cells (default: -16)
91
+ voxelcity_grid (numpy.ndarray): 3D numpy array representing the voxel grid
92
+ building_id_grid_ori (numpy.ndarray): 2D numpy array containing building IDs
93
+ ids (list/array): Building IDs to process
94
+ mark (int): Material ID value to set for building cells
95
+ window_ratio (float): Value between 0 and 1.0 determining window density:
96
+ ~0.125: sparse windows (2,2,2)
97
+ ~0.25: medium-sparse windows (2,2,1), (2,1,2), or (1,2,2)
98
+ ~0.5: medium windows (2,1,1), (1,2,1), or (1,1,2)
99
+ ~0.75: dense windows (2,1,1), (1,2,1), or (1,1,2)
100
+ >0.875: maximum density (1,1,1)
101
+ glass_id (int): Material ID for glass/window cells (default: -16)
59
102
 
60
103
  Returns:
61
- Modified voxelcity_grid
104
+ numpy.ndarray: Modified voxelcity_grid with building materials and windows applied
62
105
  """
106
+ # Flip the building ID grid vertically to match coordinate system
63
107
  building_id_grid = np.flipud(building_id_grid_ori.copy())
64
108
 
65
- # Get modulo numbers based on window_ratio
109
+ # Get modulo numbers based on window_ratio for pattern generation
66
110
  x_mod, y_mod, z_mod = get_modulo_numbers(window_ratio)
67
111
 
68
- # Get positions where building IDs match
112
+ # Find all positions where building IDs match the specified IDs
69
113
  building_positions = np.where(np.isin(building_id_grid, ids))
70
114
 
71
- # Loop through each position that matches building IDs
115
+ # Process each building position
72
116
  for i in range(len(building_positions[0])):
73
117
  x, y = building_positions[0][i], building_positions[1][i]
118
+
119
+ # Set base building material for all voxels at this x,y position
120
+ # Only modify voxels that are currently marked as "unknown" (-3)
74
121
  z_mask = voxelcity_grid[x, y, :] == -3
75
122
  voxelcity_grid[x, y, z_mask] = mark
76
123
 
77
- # Check if x and y meet the modulo conditions
124
+ # Apply window pattern if position meets modulo conditions
78
125
  if x % x_mod == 0 and y % y_mod == 0:
126
+ # Find all z positions with the building material
79
127
  z_mask = voxelcity_grid[x, y, :] == mark
80
128
  if np.any(z_mask):
81
- # Find the maximum z index where z_mask is True
129
+ # Get z indices and find the maximum (top floor)
82
130
  z_indices = np.where(z_mask)[0]
83
131
  max_z_index = np.max(z_indices)
84
132
 
85
- # Create base mask excluding maximum z index
133
+ # Create base mask excluding the top floor
134
+ # This ensures the roof never gets windows
86
135
  base_mask = z_mask.copy()
87
136
  base_mask[max_z_index] = False
88
137
 
89
- # Create pattern mask based on z modulo
138
+ # Create window pattern based on z modulo
90
139
  pattern_mask = np.zeros_like(z_mask)
91
140
  valid_z_indices = z_indices[z_indices != max_z_index] # Exclude max_z_index
92
141
  if len(valid_z_indices) > 0:
142
+ # Apply z modulo pattern to create vertical window spacing
93
143
  pattern_mask[valid_z_indices[valid_z_indices % z_mod == 0]] = True
94
144
 
95
- # For window_ratio around 0.75, add additional pattern
145
+ # For higher window ratios, add additional window pattern
146
+ # This creates denser window arrangements for buildings with more windows
96
147
  if 0.625 < window_ratio <= 0.875 and len(valid_z_indices) > 0:
97
148
  additional_pattern = np.zeros_like(z_mask)
98
149
  additional_pattern[valid_z_indices[valid_z_indices % (z_mod + 1) == 0]] = True
150
+ # Combine patterns using logical OR to increase window density
99
151
  pattern_mask = np.logical_or(pattern_mask, additional_pattern)
100
152
 
101
- # Final mask combines base_mask and pattern_mask
153
+ # Combine base mask (excluding top floor) with pattern mask
102
154
  final_glass_mask = np.logical_and(base_mask, pattern_mask)
103
155
 
104
- # Set glass_id for all positions in the final mask
156
+ # Set glass material for all positions matching the final mask
105
157
  voxelcity_grid[x, y, final_glass_mask] = glass_id
106
158
 
107
159
  return voxelcity_grid
@@ -110,27 +162,42 @@ def set_building_material_by_gdf(voxelcity_grid_ori, building_id_grid, gdf_build
110
162
  """
111
163
  Sets building materials based on a GeoDataFrame containing building information.
112
164
 
165
+ This function iterates through a GeoDataFrame of building data and applies
166
+ materials and window patterns to the corresponding buildings in the voxel grid.
167
+ It handles missing material information by defaulting to 'unknown' material.
168
+
113
169
  Parameters:
114
- voxelcity_grid_ori: 3D numpy array of the original voxel grid
115
- building_id_grid: 2D numpy array containing building IDs
116
- gdf_buildings: GeoDataFrame containing building information with columns:
117
- 'building_id', 'surface_material', 'window_ratio'
118
- material_id_dict: Dictionary mapping material names to their IDs (optional)
170
+ voxelcity_grid_ori (numpy.ndarray): 3D numpy array of the original voxel grid
171
+ building_id_grid (numpy.ndarray): 2D numpy array containing building IDs
172
+ gdf_buildings (GeoDataFrame): Building information with required columns:
173
+ 'building_id': Unique identifier for each building
174
+ 'surface_material': Material type (brick, wood, concrete, etc.)
175
+ 'window_ratio': Float between 0-1 for window density
176
+ material_id_dict (dict, optional): Dictionary mapping material names to IDs.
177
+ If None, uses default from get_material_dict()
119
178
 
120
179
  Returns:
121
- Modified voxelcity_grid
180
+ numpy.ndarray: Modified voxelcity_grid with all building materials and windows applied
122
181
  """
182
+ # Create a copy to avoid modifying the original grid
123
183
  voxelcity_grid = voxelcity_grid_ori.copy()
184
+
185
+ # Use default material dictionary if none provided
124
186
  if material_id_dict == None:
125
187
  material_id_dict = get_material_dict()
126
188
 
189
+ # Process each building in the GeoDataFrame
127
190
  for index, row in gdf_buildings.iterrows():
128
- # Access properties
191
+ # Extract building properties from the current row
129
192
  osmid = row['building_id']
130
193
  surface_material = row['surface_material']
131
194
  window_ratio = row['window_ratio']
195
+
196
+ # Handle missing surface material data
132
197
  if surface_material is None:
133
- surface_material = 'unknown'
198
+ surface_material = 'unknown'
199
+
200
+ # Apply material and window pattern to this building
134
201
  set_building_material_by_id(voxelcity_grid, building_id_grid, osmid,
135
202
  material_id_dict[surface_material],
136
203
  window_ratio=window_ratio,