voxcity 0.3.3__py3-none-any.whl → 0.3.5__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/geo/utils.py CHANGED
@@ -177,16 +177,15 @@ def transform_coords(transformer, lon, lat):
177
177
  def create_polygon(vertices):
178
178
  """
179
179
  Create a Shapely polygon from vertices.
180
- Converts from (lat,lon) format to (lon,lat) format required by Shapely.
180
+ Input vertices are already in (lon,lat) format required by Shapely.
181
181
 
182
182
  Args:
183
- vertices (list): List of (lat, lon) coordinate pairs
183
+ vertices (list): List of (lon, lat) coordinate pairs
184
184
 
185
185
  Returns:
186
186
  Polygon: Shapely polygon object
187
187
  """
188
- flipped_vertices = [(lon, lat) for lat, lon in vertices]
189
- return Polygon(flipped_vertices)
188
+ return Polygon(vertices)
190
189
 
191
190
  def create_geodataframe(polygon, crs=4326):
192
191
  """
@@ -202,14 +201,14 @@ def create_geodataframe(polygon, crs=4326):
202
201
  """
203
202
  return gpd.GeoDataFrame({'geometry': [polygon]}, crs=from_epsg(crs))
204
203
 
205
- def haversine_distance(lat1, lon1, lat2, lon2):
204
+ def haversine_distance(lon1, lat1, lon2, lat2):
206
205
  """
207
206
  Calculate great-circle distance between two points using Haversine formula.
208
207
  This is an approximation that treats the Earth as a perfect sphere.
209
208
 
210
209
  Args:
211
- lat1, lon1 (float): Coordinates of first point
212
- lat2, lon2 (float): Coordinates of second point
210
+ lon1, lat1 (float): Coordinates of first point
211
+ lon2, lat2 (float): Coordinates of second point
213
212
 
214
213
  Returns:
215
214
  float: Distance in kilometers
@@ -323,15 +322,16 @@ def merge_geotiffs(geotiff_files, output_dir):
323
322
  def convert_format_lat_lon(input_coords):
324
323
  """
325
324
  Convert coordinate format and close polygon.
325
+ Input coordinates are already in [lon, lat] format.
326
326
 
327
327
  Args:
328
328
  input_coords (list): List of [lon, lat] coordinates
329
329
 
330
330
  Returns:
331
- list: List of [lat, lon] coordinates with first point repeated at end
331
+ list: List of [lon, lat] coordinates with first point repeated at end
332
332
  """
333
- # Swap lon/lat to lat/lon format and create list
334
- output_coords = [[coord[1], coord[0]] for coord in input_coords]
333
+ # Create list with coordinates in same order
334
+ output_coords = input_coords.copy()
335
335
  # Close polygon by repeating first point at end
336
336
  output_coords.append(output_coords[0])
337
337
  return output_coords
@@ -344,7 +344,7 @@ def get_coordinates_from_cityname(place_name):
344
344
  place_name (str): Name of city to geocode
345
345
 
346
346
  Returns:
347
- tuple: (latitude, longitude) or None if geocoding fails
347
+ tuple: (longitude, latitude) or None if geocoding fails
348
348
  """
349
349
  # Initialize geocoder with user agent
350
350
  geolocator = Nominatim(user_agent="my_geocoding_script")
@@ -366,16 +366,16 @@ def get_city_country_name_from_rectangle(coordinates):
366
366
  Get city and country name from rectangle coordinates.
367
367
 
368
368
  Args:
369
- coordinates (list): List of (lat, lon) coordinates defining rectangle
369
+ coordinates (list): List of (lon, lat) coordinates defining rectangle
370
370
 
371
371
  Returns:
372
372
  str: String in format "city/ country" or None if lookup fails
373
373
  """
374
374
  # Calculate center point of rectangle
375
- latitudes = [coord[0] for coord in coordinates]
376
- longitudes = [coord[1] for coord in coordinates]
377
- center_lat = sum(latitudes) / len(latitudes)
375
+ longitudes = [coord[0] for coord in coordinates]
376
+ latitudes = [coord[1] for coord in coordinates]
378
377
  center_lon = sum(longitudes) / len(longitudes)
378
+ center_lat = sum(latitudes) / len(latitudes)
379
379
  center_coord = (center_lat, center_lon)
380
380
 
381
381
  # Initialize geocoder with rate limiting to avoid hitting API limits
@@ -401,17 +401,16 @@ def get_timezone_info(rectangle_coords):
401
401
  Get timezone and central meridian info for a location.
402
402
 
403
403
  Args:
404
- rectangle_coords (list): List of (lat, lon) coordinates defining rectangle
404
+ rectangle_coords (list): List of (lon, lat) coordinates defining rectangle
405
405
 
406
406
  Returns:
407
407
  tuple: (timezone string, central meridian longitude string)
408
408
  """
409
409
  # Calculate center point of rectangle
410
- latitudes = [coord[0] for coord in rectangle_coords]
411
- longitudes = [coord[1] for coord in rectangle_coords]
412
- center_lat = sum(latitudes) / len(latitudes)
410
+ longitudes = [coord[0] for coord in rectangle_coords]
411
+ latitudes = [coord[1] for coord in rectangle_coords]
413
412
  center_lon = sum(longitudes) / len(longitudes)
414
- center_coord = (center_lat, center_lon)
413
+ center_lat = sum(latitudes) / len(latitudes)
415
414
 
416
415
  # Find timezone at center coordinates
417
416
  tf = TimezoneFinder()
@@ -475,6 +474,7 @@ def create_building_polygons(filtered_buildings):
475
474
  """
476
475
  building_polygons = []
477
476
  idx = index.Index()
477
+ valid_count = 0
478
478
  count = 0
479
479
 
480
480
  # Find highest existing ID to avoid duplicates
@@ -487,63 +487,76 @@ def create_building_polygons(filtered_buildings):
487
487
  else:
488
488
  id_count = 1
489
489
 
490
- for i, building in enumerate(filtered_buildings):
491
- # Create polygon from coordinates
492
- polygon = Polygon(building['geometry']['coordinates'][0])
493
-
494
- # Extract height information from various possible property fields
495
- height = building['properties'].get('height')
496
- levels = building['properties'].get('levels')
497
- floors = building['properties'].get('num_floors')
498
- min_height = building['properties'].get('min_height')
499
- min_level = building['properties'].get('min_level')
500
- min_floor = building['properties'].get('min_floor')
501
-
502
- # Calculate height if not directly specified
503
- if (height is None) or (height<=0):
504
- if levels is not None:
505
- height = floor_height * levels
506
- elif floors is not None:
507
- height = floor_height * floors
508
- else:
509
- count += 1
510
- height = np.nan
511
-
512
- # Calculate minimum height if not directly specified
513
- if (min_height is None) or (min_height<=0):
514
- if min_level is not None:
515
- min_height = floor_height * float(min_level)
516
- elif min_floor is not None:
517
- min_height = floor_height * float(min_floor)
490
+ for building in filtered_buildings:
491
+ try:
492
+ # Handle potential nested coordinate tuples
493
+ coords = building['geometry']['coordinates'][0]
494
+ # Flatten coordinates if they're nested tuples
495
+ if isinstance(coords[0], tuple):
496
+ coords = [list(c) for c in coords]
497
+ elif isinstance(coords[0][0], tuple):
498
+ coords = [list(c[0]) for c in coords]
499
+
500
+ # Create polygon from coordinates
501
+ polygon = Polygon(coords)
502
+
503
+ # Skip invalid geometries
504
+ if not polygon.is_valid:
505
+ print(f"Warning: Skipping invalid polygon geometry")
506
+ continue
507
+
508
+ height = building['properties'].get('height')
509
+ levels = building['properties'].get('levels')
510
+ floors = building['properties'].get('num_floors')
511
+ min_height = building['properties'].get('min_height')
512
+ min_level = building['properties'].get('min_level')
513
+ min_floor = building['properties'].get('min_floor')
514
+
515
+ if (height is None) or (height<=0):
516
+ if levels is not None:
517
+ height = floor_height * levels
518
+ elif floors is not None:
519
+ height = floor_height * floors
520
+ else:
521
+ count += 1
522
+ height = np.nan
523
+
524
+ if (min_height is None) or (min_height<=0):
525
+ if min_level is not None:
526
+ min_height = floor_height * float(min_level)
527
+ elif min_floor is not None:
528
+ min_height = floor_height * float(min_floor)
529
+ else:
530
+ min_height = 0
531
+
532
+ if building['properties'].get('id') is not None:
533
+ feature_id = building['properties']['id']
518
534
  else:
519
- min_height = 0
535
+ feature_id = id_count
536
+ id_count += 1
520
537
 
521
- # Get or assign building ID
522
- if building['properties'].get('id') is not None:
523
- feature_id = building['properties']['id']
524
- else:
525
- feature_id = id_count
526
- id_count += 1
527
-
528
- # Check if building is inner part of another building
529
- if building['properties'].get('is_inner') is not None:
530
- is_inner = building['properties']['is_inner']
531
- else:
532
- is_inner = False
538
+ if building['properties'].get('is_inner') is not None:
539
+ is_inner = building['properties']['is_inner']
540
+ else:
541
+ is_inner = False
533
542
 
534
- # Store polygon with all properties and add to spatial index
535
- building_polygons.append((polygon, height, min_height, is_inner, feature_id))
536
- idx.insert(i, polygon.bounds)
543
+ building_polygons.append((polygon, height, min_height, is_inner, feature_id))
544
+ idx.insert(valid_count, polygon.bounds)
545
+ valid_count += 1
546
+
547
+ except Exception as e:
548
+ print(f"Warning: Skipping invalid building geometry: {e}")
549
+ continue
537
550
 
538
551
  return building_polygons, idx
539
552
 
540
- def get_country_name(lat, lon):
553
+ def get_country_name(lon, lat):
541
554
  """
542
555
  Get country name from coordinates using reverse geocoding.
543
556
 
544
557
  Args:
545
- lat (float): Latitude
546
558
  lon (float): Longitude
559
+ lat (float): Latitude
547
560
 
548
561
  Returns:
549
562
  str: Country name or None if lookup fails
voxcity/sim/solar.py CHANGED
@@ -374,7 +374,7 @@ def get_global_solar_irradiance_map(
374
374
 
375
375
  return global_map
376
376
 
377
- def get_solar_positions_astral(times, lat, lon):
377
+ def get_solar_positions_astral(times, lon, lat):
378
378
  """
379
379
  Compute solar azimuth and elevation using Astral for given times and location.
380
380
 
@@ -385,8 +385,8 @@ def get_solar_positions_astral(times, lat, lon):
385
385
 
386
386
  Args:
387
387
  times (DatetimeIndex): Array of timezone-aware datetime objects.
388
- lat (float): Latitude in degrees.
389
388
  lon (float): Longitude in degrees.
389
+ lat (float): Latitude in degrees.
390
390
 
391
391
  Returns:
392
392
  DataFrame: DataFrame with columns 'azimuth' and 'elevation' containing solar positions.
@@ -406,7 +406,7 @@ def get_solar_positions_astral(times, lat, lon):
406
406
  def get_cumulative_global_solar_irradiance(
407
407
  voxel_data,
408
408
  meshsize,
409
- df, lat, lon, tz,
409
+ df, lon, lat, tz,
410
410
  direct_normal_irradiance_scaling=1.0,
411
411
  diffuse_irradiance_scaling=1.0,
412
412
  **kwargs
@@ -424,8 +424,8 @@ def get_cumulative_global_solar_irradiance(
424
424
  voxel_data (ndarray): 3D array of voxel values.
425
425
  meshsize (float): Size of each voxel in meters.
426
426
  df (DataFrame): EPW weather data.
427
- lat (float): Latitude in degrees.
428
427
  lon (float): Longitude in degrees.
428
+ lat (float): Latitude in degrees.
429
429
  tz (float): Timezone offset in hours.
430
430
  direct_normal_irradiance_scaling (float): Scaling factor for direct normal irradiance.
431
431
  diffuse_irradiance_scaling (float): Scaling factor for diffuse horizontal irradiance.
@@ -498,7 +498,7 @@ def get_cumulative_global_solar_irradiance(
498
498
  df_period_utc = df_period_local.tz_convert(pytz.UTC)
499
499
 
500
500
  # Compute solar positions for period
501
- solar_positions = get_solar_positions_astral(df_period_utc.index, lat, lon)
501
+ solar_positions = get_solar_positions_astral(df_period_utc.index, lon, lat)
502
502
 
503
503
  # Create kwargs for diffuse calculation
504
504
  diffuse_kwargs = kwargs.copy()
voxcity/sim/view.py CHANGED
@@ -78,7 +78,7 @@ def calculate_transmittance(length, tree_k=0.6, tree_lad=1.0):
78
78
  def trace_ray_generic(voxel_data, origin, direction, hit_values, meshsize, tree_k, tree_lad, inclusion_mode=True):
79
79
  """Trace a ray through a voxel grid and check for hits with specified values.
80
80
 
81
- Uses DDA (Digital Differential Analyzer) algorithm for efficient ray traversal.
81
+ Uses DDA algorithm to efficiently traverse voxels along ray path.
82
82
  Handles tree transmittance using Beer-Lambert law.
83
83
 
84
84
  The DDA algorithm:
@@ -749,11 +749,11 @@ def get_landmark_visibility_map(voxcity_grid_ori, building_id_grid, building_geo
749
749
  return None
750
750
 
751
751
  # Calculate center point of rectangle
752
- lats = [coord[0] for coord in rectangle_vertices]
753
- lons = [coord[1] for coord in rectangle_vertices]
754
- center_lat = (min(lats) + max(lats)) / 2
752
+ lons = [coord[0] for coord in rectangle_vertices]
753
+ lats = [coord[1] for coord in rectangle_vertices]
755
754
  center_lon = (min(lons) + max(lons)) / 2
756
- target_point = (center_lat, center_lon)
755
+ center_lat = (min(lats) + max(lats)) / 2
756
+ target_point = (center_lon, center_lat)
757
757
 
758
758
  # Find buildings at center point
759
759
  landmark_ids = find_building_containing_point(features, target_point)
voxcity/utils/__init_.py CHANGED
@@ -1,3 +1,4 @@
1
1
  from .visualization import *
2
2
  from .lc import *
3
- from .weather import *
3
+ from .weather import *
4
+ from .material import *
@@ -0,0 +1,139 @@
1
+ import numpy as np
2
+
3
+ def get_material_dict():
4
+ """
5
+ Returns a dictionary mapping material names to their corresponding ID values.
6
+ """
7
+ return {
8
+ "unknown": -3,
9
+ "brick": -11,
10
+ "wood": -12,
11
+ "concrete": -13,
12
+ "metal": -14,
13
+ "stone": -15,
14
+ "glass": -16,
15
+ "plaster": -17,
16
+ }
17
+
18
+ def get_modulo_numbers(window_ratio):
19
+ """
20
+ Determines the appropriate modulo numbers for x, y, z based on window_ratio.
21
+
22
+ Parameters:
23
+ window_ratio: float between 0 and 1.0
24
+
25
+ Returns:
26
+ tuple (x_mod, y_mod, z_mod): modulo numbers for each dimension
27
+ """
28
+ if window_ratio <= 0.125 + 0.0625: # around 0.125
29
+ return (2, 2, 2)
30
+ elif window_ratio <= 0.25 + 0.125: # around 0.25
31
+ combinations = [(2, 2, 1), (2, 1, 2), (1, 2, 2)]
32
+ return combinations[hash(str(window_ratio)) % len(combinations)]
33
+ elif window_ratio <= 0.5 + 0.125: # around 0.5
34
+ combinations = [(2, 1, 1), (1, 2, 1), (1, 1, 2)]
35
+ return combinations[hash(str(window_ratio)) % len(combinations)]
36
+ elif window_ratio <= 0.75 + 0.125: # around 0.75
37
+ combinations = [(2, 1, 1), (1, 2, 1), (1, 1, 2)]
38
+ return combinations[hash(str(window_ratio)) % len(combinations)]
39
+ else: # above 0.875
40
+ return (1, 1, 1)
41
+
42
+ def set_building_material_by_id(voxelcity_grid, building_id_grid_ori, ids, mark, window_ratio=0.125, glass_id=-16):
43
+ """
44
+ Marks cells in voxelcity_grid based on building IDs and window ratio.
45
+ Never sets glass_id to cells with maximum z index.
46
+
47
+ 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)
59
+
60
+ Returns:
61
+ Modified voxelcity_grid
62
+ """
63
+ building_id_grid = np.flipud(building_id_grid_ori.copy())
64
+
65
+ # Get modulo numbers based on window_ratio
66
+ x_mod, y_mod, z_mod = get_modulo_numbers(window_ratio)
67
+
68
+ # Get positions where building IDs match
69
+ building_positions = np.where(np.isin(building_id_grid, ids))
70
+
71
+ # Loop through each position that matches building IDs
72
+ for i in range(len(building_positions[0])):
73
+ x, y = building_positions[0][i], building_positions[1][i]
74
+ z_mask = voxelcity_grid[x, y, :] == -3
75
+ voxelcity_grid[x, y, z_mask] = mark
76
+
77
+ # Check if x and y meet the modulo conditions
78
+ if x % x_mod == 0 and y % y_mod == 0:
79
+ z_mask = voxelcity_grid[x, y, :] == mark
80
+ if np.any(z_mask):
81
+ # Find the maximum z index where z_mask is True
82
+ z_indices = np.where(z_mask)[0]
83
+ max_z_index = np.max(z_indices)
84
+
85
+ # Create base mask excluding maximum z index
86
+ base_mask = z_mask.copy()
87
+ base_mask[max_z_index] = False
88
+
89
+ # Create pattern mask based on z modulo
90
+ pattern_mask = np.zeros_like(z_mask)
91
+ valid_z_indices = z_indices[z_indices != max_z_index] # Exclude max_z_index
92
+ if len(valid_z_indices) > 0:
93
+ pattern_mask[valid_z_indices[valid_z_indices % z_mod == 0]] = True
94
+
95
+ # For window_ratio around 0.75, add additional pattern
96
+ if 0.625 < window_ratio <= 0.875 and len(valid_z_indices) > 0:
97
+ additional_pattern = np.zeros_like(z_mask)
98
+ additional_pattern[valid_z_indices[valid_z_indices % (z_mod + 1) == 0]] = True
99
+ pattern_mask = np.logical_or(pattern_mask, additional_pattern)
100
+
101
+ # Final mask combines base_mask and pattern_mask
102
+ final_glass_mask = np.logical_and(base_mask, pattern_mask)
103
+
104
+ # Set glass_id for all positions in the final mask
105
+ voxelcity_grid[x, y, final_glass_mask] = glass_id
106
+
107
+ return voxelcity_grid
108
+
109
+ def set_building_material_by_gdf(voxelcity_grid_ori, building_id_grid, gdf_buildings, material_id_dict=None):
110
+ """
111
+ Sets building materials based on a GeoDataFrame containing building information.
112
+
113
+ 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)
119
+
120
+ Returns:
121
+ Modified voxelcity_grid
122
+ """
123
+ voxelcity_grid = voxelcity_grid_ori.copy()
124
+ if material_id_dict == None:
125
+ material_id_dict = get_material_dict()
126
+
127
+ for index, row in gdf_buildings.iterrows():
128
+ # Access properties
129
+ osmid = row['building_id']
130
+ surface_material = row['surface_material']
131
+ window_ratio = row['window_ratio']
132
+ if surface_material is None:
133
+ surface_material = 'unknown'
134
+ set_building_material_by_id(voxelcity_grid, building_id_grid, osmid,
135
+ material_id_dict[surface_material],
136
+ window_ratio=window_ratio,
137
+ glass_id=material_id_dict['glass'])
138
+
139
+ return voxelcity_grid