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 +2 -1
- voxcity/geo/grid.py +59 -2
- voxcity/geo/network.py +194 -0
- voxcity/sim/solar.py +5 -5
- voxcity/utils/__init_.py +2 -1
- voxcity/utils/material.py +139 -0
- voxcity/utils/visualization.py +128 -354
- voxcity/utils/weather.py +2 -2
- voxcity/voxcity.py +56 -8
- {voxcity-0.3.4.dist-info → voxcity-0.3.6.dist-info}/METADATA +36 -3
- {voxcity-0.3.4.dist-info → voxcity-0.3.6.dist-info}/RECORD +15 -13
- {voxcity-0.3.4.dist-info → voxcity-0.3.6.dist-info}/AUTHORS.rst +0 -0
- {voxcity-0.3.4.dist-info → voxcity-0.3.6.dist-info}/LICENSE +0 -0
- {voxcity-0.3.4.dist-info → voxcity-0.3.6.dist-info}/WHEEL +0 -0
- {voxcity-0.3.4.dist-info → voxcity-0.3.6.dist-info}/top_level.txt +0 -0
voxcity/geo/__init_.py
CHANGED
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
|
-
|
|
679
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
voxcity/utils/visualization.py
CHANGED
|
@@ -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[
|
|
516
|
-
dhi = float(vals[
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
26
|
+
voxcity/utils/__init_.py,sha256=nLYrj2huBbDBNMqfchCwexGP8Tlt9O_XluVDG7MoFkw,98
|
|
26
27
|
voxcity/utils/lc.py,sha256=RwPd-VY3POV3gTrBhM7TubgGb9MCd3nVah_G8iUEF7k,11562
|
|
27
|
-
voxcity/utils/
|
|
28
|
-
voxcity/utils/
|
|
29
|
-
voxcity
|
|
30
|
-
voxcity-0.3.
|
|
31
|
-
voxcity-0.3.
|
|
32
|
-
voxcity-0.3.
|
|
33
|
-
voxcity-0.3.
|
|
34
|
-
voxcity-0.3.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|