voxcity 0.3.4__py3-none-any.whl → 0.3.6__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/__init_.py CHANGED
@@ -1,3 +1,4 @@
1
1
  from .draw import *
2
2
  from .grid import *
3
- from .utils import *
3
+ from .utils import *
4
+ from .network import *
voxcity/geo/grid.py CHANGED
@@ -4,7 +4,7 @@ This module provides functions for creating and manipulating grids of building h
4
4
 
5
5
  import numpy as np
6
6
  import os
7
- from shapely.geometry import Polygon
7
+ from shapely.geometry import Polygon, box
8
8
  from scipy.ndimage import label, generate_binary_structure
9
9
  from pyproj import Geod, Transformer, CRS
10
10
  import rasterio
@@ -12,6 +12,7 @@ from affine import Affine
12
12
  from shapely.geometry import box
13
13
  from scipy.interpolate import griddata
14
14
  from shapely.errors import GEOSException
15
+ import geopandas as gpd
15
16
 
16
17
  from .utils import (
17
18
  initialize_geod,
@@ -748,4 +749,60 @@ def create_dem_grid_from_geotiff_polygon(tiff_path, mesh_size, rectangle_vertice
748
749
  # Use nearest neighbor interpolation for raw data
749
750
  grid = griddata(points, values, (xx, yy), method='nearest')
750
751
 
751
- return np.flipud(grid)
752
+ return np.flipud(grid)
753
+
754
+ def grid_to_geodataframe(grid_ori, rectangle_vertices, meshsize):
755
+ """Converts a 2D grid to a GeoDataFrame with cell polygons and values.
756
+
757
+ Args:
758
+ grid: 2D numpy array containing grid values
759
+ rectangle_vertices: List of [lon, lat] coordinates defining area corners
760
+ meshsize: Size of each grid cell in meters
761
+
762
+ Returns:
763
+ GeoDataFrame with columns:
764
+ - geometry: Polygon geometry of each grid cell
765
+ - value: Value from the grid
766
+ """
767
+ grid = np.flipud(grid_ori.copy())
768
+
769
+ # Extract bounds from rectangle vertices
770
+ min_lon = min(v[0] for v in rectangle_vertices)
771
+ max_lon = max(v[0] for v in rectangle_vertices)
772
+ min_lat = min(v[1] for v in rectangle_vertices)
773
+ max_lat = max(v[1] for v in rectangle_vertices)
774
+
775
+ rows, cols = grid.shape
776
+
777
+ # Calculate cell sizes in degrees (approximate)
778
+ # 111,111 meters = 1 degree at equator
779
+ cell_size_lon = meshsize / (111111 * np.cos(np.mean([min_lat, max_lat]) * np.pi / 180))
780
+ cell_size_lat = meshsize / 111111
781
+
782
+ # Create lists to store data
783
+ polygons = []
784
+ values = []
785
+
786
+ # Create grid cells
787
+ for i in range(rows):
788
+ for j in range(cols):
789
+ # Calculate cell bounds
790
+ cell_min_lon = min_lon + j * cell_size_lon
791
+ cell_max_lon = min_lon + (j + 1) * cell_size_lon
792
+ # Flip vertical axis since grid is stored with origin at top-left
793
+ cell_min_lat = max_lat - (i + 1) * cell_size_lat
794
+ cell_max_lat = max_lat - i * cell_size_lat
795
+
796
+ # Create polygon for cell
797
+ cell_poly = box(cell_min_lon, cell_min_lat, cell_max_lon, cell_max_lat)
798
+
799
+ polygons.append(cell_poly)
800
+ values.append(grid[i, j])
801
+
802
+ # Create GeoDataFrame
803
+ gdf = gpd.GeoDataFrame({
804
+ 'geometry': polygons,
805
+ 'value': values
806
+ }, crs=CRS.from_epsg(4326))
807
+
808
+ return gdf
voxcity/geo/network.py ADDED
@@ -0,0 +1,194 @@
1
+ import contextily as ctx
2
+ import matplotlib.pyplot as plt
3
+ import numpy as np
4
+ import pandas as pd
5
+ import geopandas as gpd
6
+ from shapely.geometry import LineString
7
+ import networkx as nx
8
+ import osmnx as ox
9
+
10
+ from .grid import grid_to_geodataframe
11
+
12
+ def calculate_edge_values(G, gdf, value_col='value'):
13
+ """
14
+ Calculate average values for graph edges based on intersection with polygons.
15
+
16
+ Parameters:
17
+ -----------
18
+ G : NetworkX Graph
19
+ Input graph with edges to analyze
20
+ gdf : GeoDataFrame
21
+ Grid containing polygons with values
22
+ value_col : str, default 'value'
23
+ Name of the column containing values in the grid
24
+
25
+ Returns:
26
+ --------
27
+ dict
28
+ Dictionary with edge identifiers (u,v,k) as keys and average values as values
29
+ """
30
+ edge_values = {}
31
+ for u, v, k, data in G.edges(data=True, keys=True):
32
+ if 'geometry' in data:
33
+ edge_line = data['geometry']
34
+ else:
35
+ start_node = G.nodes[u]
36
+ end_node = G.nodes[v]
37
+ edge_line = LineString([(start_node['x'], start_node['y']),
38
+ (end_node['x'], end_node['y'])])
39
+
40
+ intersecting_polys = gdf[gdf.geometry.intersects(edge_line)]
41
+
42
+ if len(intersecting_polys) > 0:
43
+ total_length = 0
44
+ weighted_sum = 0
45
+
46
+ for idx, poly in intersecting_polys.iterrows():
47
+ if pd.isna(poly[value_col]):
48
+ continue
49
+
50
+ intersection = edge_line.intersection(poly.geometry)
51
+ if not intersection.is_empty:
52
+ length = intersection.length
53
+ total_length += length
54
+ weighted_sum += length * poly[value_col]
55
+
56
+ if total_length > 0:
57
+ avg_value = weighted_sum / total_length
58
+ edge_values[(u, v, k)] = avg_value
59
+ else:
60
+ edge_values[(u, v, k)] = np.nan
61
+ else:
62
+ edge_values[(u, v, k)] = np.nan
63
+
64
+ return edge_values
65
+
66
+ def get_network_values(grid, rectangle_vertices, meshsize, value_name='value', **kwargs):
67
+ """
68
+ Analyze and visualize network values based on grid intersections.
69
+
70
+ Parameters:
71
+ -----------
72
+ grid : GeoDataFrame
73
+ Input grid with geometries and values
74
+ rectangle_vertices : list
75
+ List of coordinates defining the bounding box vertices
76
+ meshsize : float
77
+ Size of the mesh grid
78
+ value_name : str, default 'value'
79
+ Name of the column containing values in the grid
80
+ **kwargs : dict
81
+ Optional arguments including:
82
+ - network_type : str, default 'walk'
83
+ Type of network to download ('walk', 'drive', 'all', etc.)
84
+ - vis_graph : bool, default True
85
+ Whether to visualize the graph
86
+ - colormap : str, default 'viridis'
87
+ Matplotlib colormap name for visualization
88
+ - vmin : float, optional
89
+ Minimum value for color scaling
90
+ - vmax : float, optional
91
+ Maximum value for color scaling
92
+ - edge_width : float, default 1
93
+ Width of the edges in visualization
94
+ - fig_size : tuple, default (15,15)
95
+ Figure size for visualization
96
+ - zoom : int, default 16
97
+ Zoom level for the basemap
98
+ - basemap_style : ctx.providers, default CartoDB.Positron
99
+ Contextily basemap provider
100
+ - save_path : str, optional
101
+ Path to save the output GeoPackage
102
+
103
+ Returns:
104
+ --------
105
+ tuple : (NetworkX Graph, GeoDataFrame)
106
+ Returns the processed graph and edge GeoDataFrame
107
+ """
108
+ # Set default values for optional arguments
109
+ defaults = {
110
+ 'network_type': 'walk',
111
+ 'vis_graph': True,
112
+ 'colormap': 'viridis',
113
+ 'vmin': None,
114
+ 'vmax': None,
115
+ 'edge_width': 1,
116
+ 'fig_size': (15,15),
117
+ 'zoom': 16,
118
+ 'basemap_style': ctx.providers.CartoDB.Positron,
119
+ 'save_path': None
120
+ }
121
+
122
+ # Update defaults with provided kwargs
123
+ settings = defaults.copy()
124
+ settings.update(kwargs)
125
+
126
+ grid_gdf = grid_to_geodataframe(grid, rectangle_vertices, meshsize)
127
+
128
+ # Extract bounding box coordinates
129
+ north, south = rectangle_vertices[1][1], rectangle_vertices[0][1]
130
+ east, west = rectangle_vertices[2][0], rectangle_vertices[0][0]
131
+ bbox = (west, south, east, north)
132
+
133
+ # Download the road network
134
+ G = ox.graph.graph_from_bbox(bbox=bbox, network_type=settings['network_type'], simplify=True)
135
+
136
+ # Calculate edge values using the separate function
137
+ edge_values = calculate_edge_values(G, grid_gdf, "value")
138
+
139
+ # Add values to the graph
140
+ nx.set_edge_attributes(G, edge_values, value_name)
141
+
142
+ # Create GeoDataFrame from edges
143
+ edges_with_values = []
144
+ for u, v, k, data in G.edges(data=True, keys=True):
145
+ if 'geometry' in data:
146
+ edge_line = data['geometry']
147
+ else:
148
+ start_node = G.nodes[u]
149
+ end_node = G.nodes[v]
150
+ edge_line = LineString([(start_node['x'], start_node['y']),
151
+ (end_node['x'], end_node['y'])])
152
+
153
+ edges_with_values.append({
154
+ 'geometry': edge_line,
155
+ value_name: data.get(value_name, np.nan),
156
+ 'u': u,
157
+ 'v': v,
158
+ 'key': k
159
+ })
160
+
161
+ edge_gdf = gpd.GeoDataFrame(edges_with_values)
162
+
163
+ # Set CRS and save if requested
164
+ if edge_gdf.crs is None:
165
+ edge_gdf.set_crs(epsg=4326, inplace=True)
166
+
167
+ if settings['save_path']:
168
+ edge_gdf.to_file(settings['save_path'], driver="GPKG")
169
+
170
+ # Visualize if requested
171
+ if settings['vis_graph']:
172
+ edge_gdf_web = edge_gdf.to_crs(epsg=3857)
173
+
174
+ fig, ax = plt.subplots(figsize=settings['fig_size'])
175
+
176
+ plot = edge_gdf_web.plot(column=value_name,
177
+ ax=ax,
178
+ cmap=settings['colormap'],
179
+ legend=True,
180
+ vmin=settings['vmin'],
181
+ vmax=settings['vmax'],
182
+ linewidth=settings['edge_width'],
183
+ legend_kwds={'label': value_name,
184
+ 'shrink': 0.5}) # Make colorbar 50% smaller
185
+
186
+ ctx.add_basemap(ax,
187
+ source=settings['basemap_style'],
188
+ zoom=settings['zoom'])
189
+
190
+ ax.set_axis_off()
191
+ # plt.title(f'Network {value_name} Analysis', pad=20)
192
+ plt.show()
193
+
194
+ return G, edge_gdf
voxcity/sim/solar.py CHANGED
@@ -675,11 +675,11 @@ def get_global_solar_irradiance_using_epw(
675
675
  return None
676
676
  else:
677
677
  # Calculate center point of rectangle
678
- lats = [coord[0] for coord in rectangle_vertices]
679
- lons = [coord[1] for coord in rectangle_vertices]
680
- center_lat = (min(lats) + max(lats)) / 2
678
+ lons = [coord[0] for coord in rectangle_vertices]
679
+ lats = [coord[1] for coord in rectangle_vertices]
681
680
  center_lon = (min(lons) + max(lons)) / 2
682
- target_point = (center_lat, center_lon)
681
+ center_lat = (min(lats) + max(lats)) / 2
682
+ target_point = (center_lon, center_lat)
683
683
 
684
684
  # Optional: specify maximum distance in kilometers
685
685
  max_distance = 100 # None for no limit
@@ -687,8 +687,8 @@ def get_global_solar_irradiance_using_epw(
687
687
  output_dir = kwargs.get("output_dir", "output")
688
688
 
689
689
  epw_file_path, weather_data, metadata = get_nearest_epw_from_climate_onebuilding(
690
- latitude=center_lat,
691
690
  longitude=center_lon,
691
+ latitude=center_lat,
692
692
  output_dir=output_dir,
693
693
  max_distance=max_distance,
694
694
  extract_zip=True,
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
@@ -3,6 +3,7 @@ import matplotlib.pyplot as plt
3
3
  from mpl_toolkits.mplot3d import Axes3D
4
4
  from tqdm import tqdm
5
5
  import matplotlib.colors as mcolors
6
+ from matplotlib.colors import ListedColormap, BoundaryNorm
6
7
  import contextily as ctx
7
8
  from shapely.geometry import Polygon
8
9
  import plotly.graph_objects as go
@@ -21,7 +22,8 @@ from .lc import get_land_cover_classes
21
22
  from ..geo.grid import (
22
23
  calculate_grid_size,
23
24
  create_coordinate_mesh,
24
- create_cell_polygon
25
+ create_cell_polygon,
26
+ grid_to_geodataframe
25
27
  )
26
28
 
27
29
  from ..geo.utils import (
@@ -31,18 +33,7 @@ from ..geo.utils import (
31
33
  setup_transformer,
32
34
  transform_coords,
33
35
  )
34
-
35
- def get_material_dict():
36
- return {
37
- "unknown": -3,
38
- "brick": -11,
39
- "wood": -12,
40
- "concrete": -13,
41
- "metal": -14,
42
- "stone": -15,
43
- "glass": -16,
44
- "plaster": -17,
45
- }
36
+ from .material import get_material_dict
46
37
 
47
38
  def get_default_voxel_color_map():
48
39
  return {
@@ -251,119 +242,6 @@ def visualize_3d_voxel_plotly(voxel_grid, color_map = get_default_voxel_color_ma
251
242
  print("Visualization complete. Displaying plot...")
252
243
  fig.show()
253
244
 
254
- # def plot_grid(grid, origin, adjusted_meshsize, u_vec, v_vec, transformer, vertices, data_type, vmin=None, vmax=None, alpha=0.5, buf=0.2, edge=True, **kwargs):
255
- # fig, ax = plt.subplots(figsize=(12, 12))
256
-
257
- # if data_type == 'land_cover':
258
- # land_cover_classes = kwargs.get('land_cover_classes')
259
- # colors = [mcolors.to_rgb(f'#{r:02x}{g:02x}{b:02x}') for r, g, b in land_cover_classes.keys()]
260
- # cmap = mcolors.ListedColormap(colors)
261
- # norm = mcolors.BoundaryNorm(range(len(land_cover_classes)+1), cmap.N)
262
- # title = 'Grid Cells with Dominant Land Cover Classes'
263
- # label = 'Land Cover Class'
264
- # tick_labels = list(land_cover_classes.values())
265
- # elif data_type == 'building_height':
266
- # # Create a masked array to handle special values
267
- # masked_grid = np.ma.masked_array(grid, mask=(np.isnan(grid) | (grid == 0)))
268
-
269
- # # Set up colormap and normalization for positive values
270
- # cmap = plt.cm.viridis
271
- # if vmin is None:
272
- # vmin = np.nanmin(masked_grid[masked_grid > 0])
273
- # if vmax is None:
274
- # vmax = np.nanmax(masked_grid)
275
- # norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
276
-
277
- # title = 'Grid Cells with Building Heights'
278
- # label = 'Building Height (m)'
279
- # tick_labels = None
280
- # elif data_type == 'dem':
281
- # cmap = plt.cm.terrain
282
- # if vmin is None:
283
- # vmin = np.nanmin(grid)
284
- # if vmax is None:
285
- # vmax = np.nanmax(grid)
286
- # norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
287
- # title = 'DEM Grid Overlaid on Map'
288
- # label = 'Elevation (m)'
289
- # tick_labels = None
290
- # elif data_type == 'canopy_height':
291
- # cmap = plt.cm.Greens
292
- # if vmin is None:
293
- # vmin = np.nanmin(grid)
294
- # if vmax is None:
295
- # vmax = np.nanmax(grid)
296
- # norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
297
- # title = 'Canopy Height Grid Overlaid on Map'
298
- # label = 'Canopy Height (m)'
299
- # tick_labels = None
300
- # else:
301
- # raise ValueError("Invalid data_type. Choose 'land_cover', 'building_height', 'canopy_height', or 'dem'.")
302
-
303
- # # Ensure grid is in the correct orientation
304
- # grid = grid.T
305
-
306
- # for i in range(grid.shape[0]):
307
- # for j in range(grid.shape[1]):
308
- # cell = create_cell_polygon(origin, j, i, adjusted_meshsize, u_vec, v_vec) # Note the swap of i and j
309
- # x, y = cell.exterior.xy
310
- # x, y = zip(*[transformer.transform(lon, lat) for lat, lon in zip(x, y)])
311
-
312
- # value = grid[i, j]
313
-
314
- # if data_type == 'building_height':
315
- # if np.isnan(value):
316
- # # White fill for NaN values
317
- # ax.fill(x, y, alpha=alpha, fc='white', ec='black' if edge else None, linewidth=0.1)
318
- # elif value == 0:
319
- # # No fill for zero values, only edges if enabled
320
- # if edge:
321
- # ax.plot(x, y, color='black', linewidth=0.1)
322
- # elif value > 0:
323
- # # Viridis colormap for positive values
324
- # color = cmap(norm(value))
325
- # ax.fill(x, y, alpha=alpha, fc=color, ec='black' if edge else None, linewidth=0.1)
326
- # else:
327
- # color = cmap(norm(value))
328
- # if edge:
329
- # ax.fill(x, y, alpha=alpha, fc=color, ec='black', linewidth=0.1)
330
- # else:
331
- # ax.fill(x, y, alpha=alpha, fc=color, ec=None)
332
-
333
- # crs_epsg_3857 = CRS.from_epsg(3857)
334
- # ctx.add_basemap(ax, crs=crs_epsg_3857, source=ctx.providers.CartoDB.DarkMatter)
335
-
336
- # if data_type == 'building_height':
337
- # buildings = kwargs.get('buildings', [])
338
- # for building in buildings:
339
- # polygon = Polygon(building['geometry']['coordinates'][0])
340
- # x, y = polygon.exterior.xy
341
- # x, y = zip(*[transformer.transform(lon, lat) for lat, lon in zip(x, y)])
342
- # ax.plot(x, y, color='red', linewidth=1)
343
-
344
- # # Safe calculation of plot limits
345
- # all_coords = np.array(vertices)
346
- # x, y = zip(*[transformer.transform(lon, lat) for lat, lon in all_coords])
347
-
348
- # # Calculate limits safely
349
- # x_min, x_max = min(x), max(x)
350
- # y_min, y_max = min(y), max(y)
351
-
352
- # if x_min != x_max and y_min != y_max and buf != 0:
353
- # dist_x = x_max - x_min
354
- # dist_y = y_max - y_min
355
- # # Set limits with buffer
356
- # ax.set_xlim(x_min - buf * dist_x, x_max + buf * dist_x)
357
- # ax.set_ylim(y_min - buf * dist_y, y_max + buf * dist_y)
358
- # else:
359
- # # If coordinates are the same or buffer is 0, set limits without buffer
360
- # ax.set_xlim(x_min, x_max)
361
- # ax.set_ylim(y_min, y_max)
362
-
363
- # plt.axis('off')
364
- # plt.tight_layout()
365
- # plt.show()
366
-
367
245
  def plot_grid(grid, origin, adjusted_meshsize, u_vec, v_vec, transformer, vertices, data_type, vmin=None, vmax=None, color_map=None, alpha=0.5, buf=0.2, edge=True, basemap='CartoDB light', **kwargs):
368
246
  fig, ax = plt.subplots(figsize=(12, 12))
369
247
 
@@ -631,31 +509,6 @@ def visualize_numerical_grid_on_map(canopy_height_grid, rectangle_vertices, mesh
631
509
  # Plot the results
632
510
  plot_grid(canopy_height_grid, origin, adjusted_meshsize, u_vec, v_vec, transformer,
633
511
  rectangle_vertices, type, vmin=vmin, vmax=vmax, color_map=color_map, alpha=alpha, buf=buf, edge=edge, basemap=basemap)
634
-
635
- # def visualize_land_cover_grid(grid, mesh_size, color_map, land_cover_classes):
636
- # all_classes = list(land_cover_classes.values())# + ['No Data']
637
- # # for cls in all_classes:
638
- # # if cls not in color_map:
639
- # # color_map[cls] = [0.5, 0.5, 0.5]
640
-
641
- # sorted_classes = sorted(all_classes)
642
- # colors = [color_map[cls] for cls in sorted_classes]
643
- # cmap = mcolors.ListedColormap(colors)
644
-
645
- # bounds = np.arange(len(sorted_classes) + 1)
646
- # norm = mcolors.BoundaryNorm(bounds, cmap.N)
647
-
648
- # class_to_num = {cls: i for i, cls in enumerate(sorted_classes)}
649
- # numeric_grid = np.vectorize(class_to_num.get)(grid)
650
-
651
- # plt.figure(figsize=(10, 10))
652
- # im = plt.imshow(numeric_grid, cmap=cmap, norm=norm, interpolation='nearest')
653
- # cbar = plt.colorbar(im, ticks=bounds[:-1] + 0.5)
654
- # cbar.set_ticklabels(sorted_classes)
655
- # plt.title(f'Land Use/Land Cover Grid (Mesh Size: {mesh_size}m)')
656
- # plt.xlabel('Grid Cells (X)')
657
- # plt.ylabel('Grid Cells (Y)')
658
- # plt.show()
659
512
 
660
513
  def visualize_land_cover_grid(grid, mesh_size, color_map, land_cover_classes):
661
514
  all_classes = list(land_cover_classes.values())
@@ -688,204 +541,6 @@ def visualize_numerical_grid(grid, mesh_size, title, cmap='viridis', label='Valu
688
541
  plt.ylabel('Grid Cells (Y)')
689
542
  plt.show()
690
543
 
691
- def get_modulo_numbers(window_ratio):
692
- """
693
- Determines the appropriate modulo numbers for x, y, z based on window_ratio.
694
-
695
- Parameters:
696
- window_ratio: float between 0 and 1.0
697
-
698
- Returns:
699
- tuple (x_mod, y_mod, z_mod): modulo numbers for each dimension
700
- """
701
- if window_ratio <= 0.125 + 0.0625: # around 0.125
702
- return (2, 2, 2)
703
- elif window_ratio <= 0.25 + 0.125: # around 0.25
704
- combinations = [(2, 2, 1), (2, 1, 2), (1, 2, 2)]
705
- return combinations[hash(str(window_ratio)) % len(combinations)]
706
- elif window_ratio <= 0.5 + 0.125: # around 0.5
707
- combinations = [(2, 1, 1), (1, 2, 1), (1, 1, 2)]
708
- return combinations[hash(str(window_ratio)) % len(combinations)]
709
- elif window_ratio <= 0.75 + 0.125: # around 0.75
710
- combinations = [(2, 1, 1), (1, 2, 1), (1, 1, 2)]
711
- return combinations[hash(str(window_ratio)) % len(combinations)]
712
- else: # above 0.875
713
- return (1, 1, 1)
714
-
715
- def set_building_material_by_id(voxelcity_grid, building_id_grid_ori, ids, mark, window_ratio=0.125, glass_id=-10):
716
- """
717
- Marks cells in voxelcity_grid based on building IDs and window ratio.
718
- Never sets glass_id to cells with maximum z index.
719
-
720
- Parameters:
721
- voxelcity_grid: 3D numpy array
722
- building_id_grid_ori: 2D numpy array containing building IDs
723
- ids: list/array of building IDs to check
724
- mark: value to set for marked cells
725
- window_ratio: float between 0 and 1.0, determines window density:
726
- ~0.125: sparse windows (2,2,2)
727
- ~0.25: medium-sparse windows (2,2,1), (2,1,2), or (1,2,2)
728
- ~0.5: medium windows (2,1,1), (1,2,1), or (1,1,2)
729
- ~0.75: dense windows (2,1,1), (1,2,1), or (1,1,2)
730
- >0.875: maximum density (1,1,1)
731
- glass_id: value to set for glass cells (default: -10)
732
-
733
- Returns:
734
- Modified voxelcity_grid
735
- """
736
- building_id_grid = np.flipud(building_id_grid_ori.copy())
737
-
738
- # Get modulo numbers based on window_ratio
739
- x_mod, y_mod, z_mod = get_modulo_numbers(window_ratio)
740
-
741
- # Get positions where building IDs match
742
- building_positions = np.where(np.isin(building_id_grid, ids))
743
-
744
- # Loop through each position that matches building IDs
745
- for i in range(len(building_positions[0])):
746
- x, y = building_positions[0][i], building_positions[1][i]
747
- z_mask = voxelcity_grid[x, y, :] == -3
748
- voxelcity_grid[x, y, z_mask] = mark
749
-
750
- # Check if x and y meet the modulo conditions
751
- if x % x_mod == 0 and y % y_mod == 0:
752
- z_mask = voxelcity_grid[x, y, :] == mark
753
- if np.any(z_mask):
754
- # Find the maximum z index where z_mask is True
755
- z_indices = np.where(z_mask)[0]
756
- max_z_index = np.max(z_indices)
757
-
758
- # Create base mask excluding maximum z index
759
- base_mask = z_mask.copy()
760
- base_mask[max_z_index] = False
761
-
762
- # Create pattern mask based on z modulo
763
- pattern_mask = np.zeros_like(z_mask)
764
- valid_z_indices = z_indices[z_indices != max_z_index] # Exclude max_z_index
765
- if len(valid_z_indices) > 0:
766
- pattern_mask[valid_z_indices[valid_z_indices % z_mod == 0]] = True
767
-
768
- # For window_ratio around 0.75, add additional pattern
769
- if 0.625 < window_ratio <= 0.875 and len(valid_z_indices) > 0:
770
- additional_pattern = np.zeros_like(z_mask)
771
- additional_pattern[valid_z_indices[valid_z_indices % (z_mod + 1) == 0]] = True
772
- pattern_mask = np.logical_or(pattern_mask, additional_pattern)
773
-
774
- # Final mask combines base_mask and pattern_mask
775
- final_glass_mask = np.logical_and(base_mask, pattern_mask)
776
-
777
- # Set glass_id for all positions in the final mask
778
- voxelcity_grid[x, y, final_glass_mask] = glass_id
779
-
780
- return voxelcity_grid
781
-
782
- def set_building_material_by_gdf(voxelcity_grid_ori, building_id_grid, gdf_buildings, material_id_dict=None):
783
- voxelcity_grid = voxelcity_grid_ori.copy()
784
- if material_id_dict == None:
785
- material_id_dict = get_material_dict()
786
-
787
- for index, row in gdf_buildings.iterrows():
788
- # Access properties
789
- osmid = row['building_id']
790
- surface_material = row['surface_material']
791
- window_ratio = row['window_ratio']
792
- if surface_material is None:
793
- surface_material = 'unknown'
794
- set_building_material_by_id(voxelcity_grid, building_id_grid, osmid, material_id_dict[surface_material], window_ratio=window_ratio, glass_id=material_id_dict['glass'])
795
-
796
- return voxelcity_grid
797
-
798
- def get_modulo_numbers(window_ratio):
799
- """
800
- Determines the appropriate modulo numbers for x, y, z based on window_ratio.
801
-
802
- Parameters:
803
- window_ratio: float between 0 and 1.0
804
-
805
- Returns:
806
- tuple (x_mod, y_mod, z_mod): modulo numbers for each dimension
807
- """
808
- if window_ratio <= 0.125 + 0.0625: # around 0.125
809
- return (2, 2, 2)
810
- elif window_ratio <= 0.25 + 0.125: # around 0.25
811
- combinations = [(2, 2, 1), (2, 1, 2), (1, 2, 2)]
812
- return combinations[hash(str(window_ratio)) % len(combinations)]
813
- elif window_ratio <= 0.5 + 0.125: # around 0.5
814
- combinations = [(2, 1, 1), (1, 2, 1), (1, 1, 2)]
815
- return combinations[hash(str(window_ratio)) % len(combinations)]
816
- elif window_ratio <= 0.75 + 0.125: # around 0.75
817
- combinations = [(2, 1, 1), (1, 2, 1), (1, 1, 2)]
818
- return combinations[hash(str(window_ratio)) % len(combinations)]
819
- else: # above 0.875
820
- return (1, 1, 1)
821
-
822
- def set_building_material_by_id(voxelcity_grid, building_id_grid_ori, ids, mark, window_ratio=0.125, glass_id=-16):
823
- """
824
- Marks cells in voxelcity_grid based on building IDs and window ratio.
825
- Never sets glass_id to cells with maximum z index.
826
-
827
- Parameters:
828
- voxelcity_grid: 3D numpy array
829
- building_id_grid_ori: 2D numpy array containing building IDs
830
- ids: list/array of building IDs to check
831
- mark: value to set for marked cells
832
- window_ratio: float between 0 and 1.0, determines window density:
833
- ~0.125: sparse windows (2,2,2)
834
- ~0.25: medium-sparse windows (2,2,1), (2,1,2), or (1,2,2)
835
- ~0.5: medium windows (2,1,1), (1,2,1), or (1,1,2)
836
- ~0.75: dense windows (2,1,1), (1,2,1), or (1,1,2)
837
- >0.875: maximum density (1,1,1)
838
- glass_id: value to set for glass cells (default: -10)
839
-
840
- Returns:
841
- Modified voxelcity_grid
842
- """
843
- building_id_grid = np.flipud(building_id_grid_ori.copy())
844
-
845
- # Get modulo numbers based on window_ratio
846
- x_mod, y_mod, z_mod = get_modulo_numbers(window_ratio)
847
-
848
- # Get positions where building IDs match
849
- building_positions = np.where(np.isin(building_id_grid, ids))
850
-
851
- # Loop through each position that matches building IDs
852
- for i in range(len(building_positions[0])):
853
- x, y = building_positions[0][i], building_positions[1][i]
854
- z_mask = voxelcity_grid[x, y, :] == -3
855
- voxelcity_grid[x, y, z_mask] = mark
856
-
857
- # Check if x and y meet the modulo conditions
858
- if x % x_mod == 0 and y % y_mod == 0:
859
- z_mask = voxelcity_grid[x, y, :] == mark
860
- if np.any(z_mask):
861
- # Find the maximum z index where z_mask is True
862
- z_indices = np.where(z_mask)[0]
863
- max_z_index = np.max(z_indices)
864
-
865
- # Create base mask excluding maximum z index
866
- base_mask = z_mask.copy()
867
- base_mask[max_z_index] = False
868
-
869
- # Create pattern mask based on z modulo
870
- pattern_mask = np.zeros_like(z_mask)
871
- valid_z_indices = z_indices[z_indices != max_z_index] # Exclude max_z_index
872
- if len(valid_z_indices) > 0:
873
- pattern_mask[valid_z_indices[valid_z_indices % z_mod == 0]] = True
874
-
875
- # For window_ratio around 0.75, add additional pattern
876
- if 0.625 < window_ratio <= 0.875 and len(valid_z_indices) > 0:
877
- additional_pattern = np.zeros_like(z_mask)
878
- additional_pattern[valid_z_indices[valid_z_indices % (z_mod + 1) == 0]] = True
879
- pattern_mask = np.logical_or(pattern_mask, additional_pattern)
880
-
881
- # Final mask combines base_mask and pattern_mask
882
- final_glass_mask = np.logical_and(base_mask, pattern_mask)
883
-
884
- # Set glass_id for all positions in the final mask
885
- voxelcity_grid[x, y, final_glass_mask] = glass_id
886
-
887
- return voxelcity_grid
888
-
889
544
  def convert_coordinates(coords):
890
545
  return coords
891
546
 
@@ -904,10 +559,6 @@ def calculate_center(features):
904
559
  lons.append(lon)
905
560
  return sum(lats) / len(lats), sum(lons) / len(lons)
906
561
 
907
- # def format_building_id(id_num):
908
- # # Format ID to ensure it's at least 9 digits with leading zeros
909
- # return f"{id_num:09d}"
910
-
911
562
  def create_circle_polygon(center_lat, center_lon, radius_meters):
912
563
  """Create a circular polygon with given center and radius"""
913
564
  # Convert radius from meters to degrees (approximate)
@@ -1002,4 +653,127 @@ def display_builing_ids_on_map(building_geojson, rectangle_vertices):
1002
653
  ).add_to(m)
1003
654
 
1004
655
  # Save the map
1005
- return m
656
+ return m
657
+
658
+ def visualize_landcover_grid_on_basemap(landcover_grid, rectangle_vertices, meshsize, source='Standard', alpha=0.6, figsize=(12, 8),
659
+ basemap='CartoDB light', show_edge=False, edge_color='black', edge_width=0.5):
660
+ """Visualizes a land cover grid GeoDataFrame using predefined color schemes.
661
+
662
+ Args:
663
+ gdf: GeoDataFrame containing grid cells with 'geometry' and 'value' columns
664
+ source: Source of land cover classification (e.g., 'Standard', 'Urbanwatch', etc.)
665
+ title: Title for the plot (default: None)
666
+ alpha: Transparency of the grid overlay (default: 0.6)
667
+ figsize: Figure size in inches (default: (12, 8))
668
+ basemap: Basemap style (default: 'CartoDB light')
669
+ show_edge: Whether to show cell edges (default: True)
670
+ edge_color: Color of cell edges (default: 'black')
671
+ edge_width: Width of cell edges (default: 0.5)
672
+ """
673
+ # Get land cover classes and colors
674
+ land_cover_classes = get_land_cover_classes(source)
675
+
676
+ gdf = grid_to_geodataframe(landcover_grid, rectangle_vertices, meshsize)
677
+
678
+ # Convert RGB tuples to normalized RGB values
679
+ colors = [(r/255, g/255, b/255) for (r,g,b) in land_cover_classes.keys()]
680
+
681
+ # Create custom colormap
682
+ cmap = ListedColormap(colors)
683
+
684
+ # Create bounds for discrete colorbar
685
+ bounds = np.arange(len(colors) + 1)
686
+ norm = BoundaryNorm(bounds, cmap.N)
687
+
688
+ # Convert to Web Mercator
689
+ gdf_web = gdf.to_crs(epsg=3857)
690
+
691
+ # Create figure and axis
692
+ fig, ax = plt.subplots(figsize=figsize)
693
+
694
+ # Plot the GeoDataFrame
695
+ gdf_web.plot(column='value',
696
+ ax=ax,
697
+ alpha=alpha,
698
+ cmap=cmap,
699
+ norm=norm,
700
+ legend=True,
701
+ legend_kwds={
702
+ 'label': 'Land Cover Class',
703
+ 'ticks': bounds[:-1] + 0.5,
704
+ 'boundaries': bounds,
705
+ 'format': lambda x, p: list(land_cover_classes.values())[int(x)]
706
+ },
707
+ edgecolor=edge_color if show_edge else 'none',
708
+ linewidth=edge_width if show_edge else 0)
709
+
710
+ # Add basemap
711
+ basemaps = {
712
+ 'CartoDB dark': ctx.providers.CartoDB.DarkMatter,
713
+ 'CartoDB light': ctx.providers.CartoDB.Positron,
714
+ 'CartoDB voyager': ctx.providers.CartoDB.Voyager,
715
+ 'CartoDB light no labels': ctx.providers.CartoDB.PositronNoLabels,
716
+ 'CartoDB dark no labels': ctx.providers.CartoDB.DarkMatterNoLabels,
717
+ }
718
+ ctx.add_basemap(ax, source=basemaps[basemap])
719
+
720
+ # Set title and remove axes
721
+ ax.set_axis_off()
722
+
723
+ plt.tight_layout()
724
+ plt.show()
725
+
726
+ def visualize_numerical_grid_on_basemap(grid, rectangle_vertices, meshsize, value_name="value", cmap='viridis', vmin=None, vmax=None,
727
+ alpha=0.6, figsize=(12, 8), basemap='CartoDB light',
728
+ show_edge=False, edge_color='black', edge_width=0.5):
729
+ """Visualizes a numerical grid GeoDataFrame (e.g., heights) on a basemap.
730
+
731
+ Args:
732
+ gdf: GeoDataFrame containing grid cells with 'geometry' and 'value' columns
733
+ title: Title for the plot (default: None)
734
+ cmap: Colormap to use (default: 'viridis')
735
+ vmin: Minimum value for colormap scaling (default: None)
736
+ vmax: Maximum value for colormap scaling (default: None)
737
+ alpha: Transparency of the grid overlay (default: 0.6)
738
+ figsize: Figure size in inches (default: (12, 8))
739
+ basemap: Basemap style (default: 'CartoDB light')
740
+ show_edge: Whether to show cell edges (default: True)
741
+ edge_color: Color of cell edges (default: 'black')
742
+ edge_width: Width of cell edges (default: 0.5)
743
+ """
744
+
745
+ gdf = grid_to_geodataframe(grid, rectangle_vertices, meshsize)
746
+
747
+ # Convert to Web Mercator
748
+ gdf_web = gdf.to_crs(epsg=3857)
749
+
750
+ # Create figure and axis
751
+ fig, ax = plt.subplots(figsize=figsize)
752
+
753
+ # Plot the GeoDataFrame
754
+ gdf_web.plot(column='value',
755
+ ax=ax,
756
+ alpha=alpha,
757
+ cmap=cmap,
758
+ vmin=vmin,
759
+ vmax=vmax,
760
+ legend=True,
761
+ legend_kwds={'label': value_name},
762
+ edgecolor=edge_color if show_edge else 'none',
763
+ linewidth=edge_width if show_edge else 0)
764
+
765
+ # Add basemap
766
+ basemaps = {
767
+ 'CartoDB dark': ctx.providers.CartoDB.DarkMatter,
768
+ 'CartoDB light': ctx.providers.CartoDB.Positron,
769
+ 'CartoDB voyager': ctx.providers.CartoDB.Voyager,
770
+ 'CartoDB light no labels': ctx.providers.CartoDB.PositronNoLabels,
771
+ 'CartoDB dark no labels': ctx.providers.CartoDB.DarkMatterNoLabels,
772
+ }
773
+ ctx.add_basemap(ax, source=basemaps[basemap])
774
+
775
+ # Set title and remove axes
776
+ ax.set_axis_off()
777
+
778
+ plt.tight_layout()
779
+ plt.show()
voxcity/utils/weather.py CHANGED
@@ -512,8 +512,8 @@ def read_epw_for_solar_simulation(epw_file_path):
512
512
  month = int(vals[1])
513
513
  day = int(vals[2])
514
514
  hour = int(vals[3]) - 1
515
- dni = float(vals[13])
516
- dhi = float(vals[14])
515
+ dni = float(vals[14])
516
+ dhi = float(vals[15])
517
517
  timestamp = pd.Timestamp(year, month, day, hour)
518
518
  data.append([timestamp, dni, dhi])
519
519
 
voxcity/voxcity.py CHANGED
@@ -52,10 +52,12 @@ from .utils.visualization import (
52
52
  get_land_cover_classes,
53
53
  visualize_land_cover_grid,
54
54
  visualize_numerical_grid,
55
- visualize_land_cover_grid_on_map,
56
- visualize_numerical_grid_on_map,
57
- visualize_building_height_grid_on_map,
58
- visualize_3d_voxel
55
+ # visualize_land_cover_grid_on_map,
56
+ # visualize_numerical_grid_on_map,
57
+ # visualize_building_height_grid_on_map,
58
+ visualize_3d_voxel,
59
+ visualize_landcover_grid_on_basemap,
60
+ visualize_numerical_grid_on_basemap,
59
61
  )
60
62
 
61
63
  def get_land_cover_grid(rectangle_vertices, meshsize, source, output_dir, **kwargs):
@@ -593,10 +595,56 @@ def get_voxcity(rectangle_vertices, building_source, land_cover_source, canopy_h
593
595
  # Visualize 2D grids on map if requested
594
596
  mapvis = kwargs.get("mapvis")
595
597
  if mapvis:
596
- visualize_land_cover_grid_on_map(land_cover_grid, rectangle_vertices, meshsize, source = land_cover_source)
597
- visualize_building_height_grid_on_map(building_height_grid, building_geojson, rectangle_vertices, meshsize)
598
- visualize_numerical_grid_on_map(canopy_height_grid, rectangle_vertices, meshsize, "canopy_height")
599
- visualize_numerical_grid_on_map(dem_grid, rectangle_vertices, meshsize, "dem")
598
+ # Visualize land cover using the new function
599
+ visualize_landcover_grid_on_basemap(
600
+ land_cover_grid,
601
+ rectangle_vertices,
602
+ meshsize,
603
+ source=land_cover_source,
604
+ alpha=0.7,
605
+ figsize=(12, 8),
606
+ basemap='CartoDB light',
607
+ show_edge=False
608
+ )
609
+
610
+ # Visualize building heights using the new function
611
+ visualize_numerical_grid_on_basemap(
612
+ building_height_grid,
613
+ rectangle_vertices,
614
+ meshsize,
615
+ value_name="Building Heights (m)",
616
+ cmap='viridis',
617
+ alpha=0.7,
618
+ figsize=(12, 8),
619
+ basemap='CartoDB light',
620
+ show_edge=False
621
+ )
622
+
623
+ # Visualize canopy heights using the new function
624
+ visualize_numerical_grid_on_basemap(
625
+ canopy_height_grid,
626
+ rectangle_vertices,
627
+ meshsize,
628
+ value_name="Canopy Heights (m)",
629
+ cmap='Greens',
630
+ alpha=0.7,
631
+ figsize=(12, 8),
632
+ basemap='CartoDB light',
633
+ show_edge=False
634
+ )
635
+
636
+ # Visualize DEM using the new function
637
+ visualize_numerical_grid_on_basemap(
638
+ dem_grid,
639
+ rectangle_vertices,
640
+ meshsize,
641
+ value_name="Terrain Elevation (m)",
642
+ cmap='terrain',
643
+ alpha=0.7,
644
+ figsize=(12, 8),
645
+ basemap='CartoDB light',
646
+ show_edge=False
647
+ )
600
648
 
601
649
  # Generate 3D voxel grid
602
650
  voxcity_grid = create_3d_voxel(building_height_grid, building_min_height_grid, building_id_grid, land_cover_grid, dem_grid, canopy_height_grid, meshsize, land_cover_source)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: voxcity
3
- Version: 0.3.4
3
+ Version: 0.3.6
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>
@@ -48,6 +48,7 @@ Requires-Dist: overturemaps
48
48
  Requires-Dist: protobuf==3.20.3
49
49
  Requires-Dist: timezonefinder
50
50
  Requires-Dist: astral
51
+ Requires-Dist: osmnx
51
52
  Provides-Extra: dev
52
53
  Requires-Dist: coverage; extra == "dev"
53
54
  Requires-Dist: mypy; extra == "dev"
@@ -329,7 +330,7 @@ solar_kwargs = {
329
330
  }
330
331
 
331
332
  # Compute global solar irradiance map (direct + diffuse radiation)
332
- global_map = get_global_solar_irradiance_using_epw(
333
+ solar_grid = get_global_solar_irradiance_using_epw(
333
334
  voxcity_grid, # 3D voxel grid representing the urban environment
334
335
  meshsize, # Size of each voxel in meters
335
336
  calc_type='instantaneous', # Calculate instantaneous irradiance at specified time
@@ -344,7 +345,7 @@ solar_kwargs["end_time"] = "01-31 23:00:00" # End time for cumulative calculatio
344
345
  solar_kwargs["output_file_name"] = 'cummulative_solar_irradiance', # Base filename for outputs (without extension)
345
346
 
346
347
  # Calculate cumulative solar irradiance over the specified time period
347
- global_map = get_global_solar_irradiance_using_epw(
348
+ cum_solar_grid = get_global_solar_irradiance_using_epw(
348
349
  voxcity_grid, # 3D voxel grid representing the urban environment
349
350
  meshsize, # Size of each voxel in meters
350
351
  calc_type='cumulative', # Calculate cumulative irradiance over time period instead of instantaneous
@@ -417,6 +418,38 @@ landmark_vis_map = get_landmark_visibility_map(voxcity_grid, building_id_grid, b
417
418
  <em>Example Result Saved as OBJ and Rendered in Rhino</em>
418
419
  </p>
419
420
 
421
+ #### Network Analysis:
422
+
423
+ ```python
424
+ from voxcity.geo.network import get_network_values
425
+
426
+ network_kwargs = {
427
+ "network_type": "walk", # Type of network to download from OSM (walk, drive, all, etc.)
428
+ "colormap": "magma", # Matplotlib colormap for visualization
429
+ "vis_graph": True, # Whether to display the network visualization
430
+ "vmin": 0.0, # Minimum value for color scaling
431
+ "vmax": 600000, # Maximum value for color scaling
432
+ "edge_width": 2, # Width of network edges in visualization
433
+ "alpha": 0.8, # Transparency of network edges
434
+ "zoom": 16 # Zoom level for basemap
435
+ }
436
+
437
+ G, edge_gdf = get_network_values(
438
+ cum_solar_grid, # Grid of cumulative solar irradiance values
439
+ rectangle_vertices, # Coordinates defining simulation domain boundary
440
+ meshsize, # Size of each grid cell in meters
441
+ value_name='Cumulative Global Solar Irradiance (W/m²·hour)', # Label for values in visualization
442
+ **network_kwargs # Additional visualization and network parameters
443
+ )
444
+ ```
445
+
446
+ <p align="center">
447
+ <img src="https://raw.githubusercontent.com/kunifujiwara/VoxCity/main/images/network.png" alt="Example of Graph Output" width="500">
448
+ </p>
449
+ <p align="center">
450
+ <em>Example Result Saved as OBJ and Rendered in Rhino</em>
451
+ </p>
452
+
420
453
  ## References of Data Sources
421
454
 
422
455
  ### Building
@@ -1,5 +1,5 @@
1
1
  voxcity/__init__.py,sha256=HJM0D2Mv9qpk4JdVzt2SRAAk-hA1D_pCO0ezZH9F7KA,248
2
- voxcity/voxcity.py,sha256=ewwSxA_lMIkQ5yiLZutq4UCLfnUm0r5f2Jiy-q6cFm0,32256
2
+ voxcity/voxcity.py,sha256=aeM1OzW7nmbW4h4SFHceugvb0NhKdXlQz_QcVQrZrIk,33498
3
3
  voxcity/download/__init__.py,sha256=OgGcGxOXF4tjcEL6DhOnt13DYPTvOigUelp5xIpTqM0,171
4
4
  voxcity/download/eubucco.py,sha256=e1JXBuUfBptSDvNznSGckRs5Xgrj_SAFxk445J_o4KY,14854
5
5
  voxcity/download/gee.py,sha256=j7jmzp44T3M6j_4DwhU9Y8Y6gqbZo1zFIlduQPc0jvk,14339
@@ -14,21 +14,23 @@ voxcity/file/envimet.py,sha256=SPVoSyYTMNyDRDFWsI0YAsIsb6yt_SXZeDUlhyqlEqY,24282
14
14
  voxcity/file/geojson.py,sha256=G8jG5Ffh86uhNZBLmr_hgyU9FwGab_tJBePET5DUQYk,24188
15
15
  voxcity/file/magicavoxel.py,sha256=Fsv7yGRXeKmp82xcG3rOb0t_HtoqltNq2tHl08xVlqY,7500
16
16
  voxcity/file/obj.py,sha256=oW-kPoZj53nfmO9tXP3Wvizq6Kkjh-QQR8UBexRuMiI,21609
17
- voxcity/geo/__init_.py,sha256=rsj0OMzrTNACccdvEfmf632mb03BRUtKLuecppsxX40,62
17
+ voxcity/geo/__init_.py,sha256=AZYQxK1zY1M_mDT1HmgcdVI86OAtwK7CNo3AOScLHco,88
18
18
  voxcity/geo/draw.py,sha256=roljWXyqYdsWYkmb-5_WNxrJrfV5lnAt8uZblCCo_3Q,13555
19
- voxcity/geo/grid.py,sha256=YgAityV3KaBsng9R_aDQKRFkdEssv5Yzn5wKCIOJQOQ,34243
19
+ voxcity/geo/grid.py,sha256=_MzO-Cu2GhlP9nuCql6f1pfbU2_OAL27aQ_zCj1u_zk,36288
20
+ voxcity/geo/network.py,sha256=iBgvOaM4YPQKL5gnAU9rxe3ZlJLTjLIt7DoAIWzZRfs,6892
20
21
  voxcity/geo/utils.py,sha256=1BRHp-DDeOA8HG8jplY7Eo75G3oXkVGL6DGONL4BA8A,19815
21
22
  voxcity/sim/__init_.py,sha256=APdkcdaovj0v_RPOaA4SBvFUKT2RM7Hxuuz3Sux4gCo,65
22
- voxcity/sim/solar.py,sha256=7waUoUMzDBf_Van3qghSG019TrgHgNj-TcVRVf0StuU,31306
23
+ voxcity/sim/solar.py,sha256=GkL0PEjTr_bmtc75ixcyued5iwmiieLxTd-9e69x2R8,31306
23
24
  voxcity/sim/utils.py,sha256=sEYBB2-hLJxTiXQps1_-Fi7t1HN3-1OPOvBCWtgIisA,130
24
25
  voxcity/sim/view.py,sha256=oq6G-f0Tn-KT0vjYNJfucmOIrv1GNjljhA-zvU4nNoA,36668
25
- voxcity/utils/__init_.py,sha256=xjEadXQ9wXTw0lsx0JTbyTqASWw0GJLfT6eRr0CyQzw,71
26
+ voxcity/utils/__init_.py,sha256=nLYrj2huBbDBNMqfchCwexGP8Tlt9O_XluVDG7MoFkw,98
26
27
  voxcity/utils/lc.py,sha256=RwPd-VY3POV3gTrBhM7TubgGb9MCd3nVah_G8iUEF7k,11562
27
- voxcity/utils/visualization.py,sha256=GVERj0noHAvJtDT0fV3K6w7pTfuAUfwKez-UMuEakEg,42214
28
- voxcity/utils/weather.py,sha256=fJ2p5susoMgYSBlrmlTlZVUDe9kpQwmLuyv1TgcOnDM,21482
29
- voxcity-0.3.4.dist-info/AUTHORS.rst,sha256=m82vkI5QokEGdcHof2OxK39lf81w1P58kG9ZNNAKS9U,175
30
- voxcity-0.3.4.dist-info/LICENSE,sha256=-hGliOFiwUrUSoZiB5WF90xXGqinKyqiDI2t6hrnam8,1087
31
- voxcity-0.3.4.dist-info/METADATA,sha256=wi1ziMnMN8UpySzEQZpGN6SrUW2ZSkY-gOd4kQ9de0U,23608
32
- voxcity-0.3.4.dist-info/WHEEL,sha256=A3WOREP4zgxI0fKrHUG8DC8013e3dK3n7a6HDbcEIwE,91
33
- voxcity-0.3.4.dist-info/top_level.txt,sha256=00b2U-LKfDllt6RL1R33MXie5MvxzUFye0NGD96t_8I,8
34
- voxcity-0.3.4.dist-info/RECORD,,
28
+ voxcity/utils/material.py,sha256=Vt3IID5Ft54HNJcEC4zi31BCPqi_687X3CSp7rXaRVY,5907
29
+ voxcity/utils/visualization.py,sha256=FNBMN0V5IPuAdqvLHnqSGYqNS7jWesg0ZADEtsUtl0A,31925
30
+ voxcity/utils/weather.py,sha256=P6s1y_EstBL1OGP_MR_6u3vr-t6Uawg8uDckJnoI7FI,21482
31
+ voxcity-0.3.6.dist-info/AUTHORS.rst,sha256=m82vkI5QokEGdcHof2OxK39lf81w1P58kG9ZNNAKS9U,175
32
+ voxcity-0.3.6.dist-info/LICENSE,sha256=-hGliOFiwUrUSoZiB5WF90xXGqinKyqiDI2t6hrnam8,1087
33
+ voxcity-0.3.6.dist-info/METADATA,sha256=yYrGPuGGkWRSTCu7GwbO65Vgof05NhyUMxsiS3KbV5I,25071
34
+ voxcity-0.3.6.dist-info/WHEEL,sha256=A3WOREP4zgxI0fKrHUG8DC8013e3dK3n7a6HDbcEIwE,91
35
+ voxcity-0.3.6.dist-info/top_level.txt,sha256=00b2U-LKfDllt6RL1R33MXie5MvxzUFye0NGD96t_8I,8
36
+ voxcity-0.3.6.dist-info/RECORD,,