voxcity 0.3.2__py3-none-any.whl → 0.3.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of voxcity might be problematic. Click here for more details.
- voxcity/file/geojson.py +72 -1
- voxcity/geo/draw.py +112 -2
- voxcity/sim/solar.py +184 -50
- voxcity/sim/view.py +183 -31
- {voxcity-0.3.2.dist-info → voxcity-0.3.3.dist-info}/METADATA +57 -1
- {voxcity-0.3.2.dist-info → voxcity-0.3.3.dist-info}/RECORD +10 -10
- {voxcity-0.3.2.dist-info → voxcity-0.3.3.dist-info}/AUTHORS.rst +0 -0
- {voxcity-0.3.2.dist-info → voxcity-0.3.3.dist-info}/LICENSE +0 -0
- {voxcity-0.3.2.dist-info → voxcity-0.3.3.dist-info}/WHEEL +0 -0
- {voxcity-0.3.2.dist-info → voxcity-0.3.3.dist-info}/top_level.txt +0 -0
voxcity/file/geojson.py
CHANGED
|
@@ -525,4 +525,75 @@ def find_building_containing_point(features, target_point):
|
|
|
525
525
|
if polygon.contains(point):
|
|
526
526
|
id_list.append(feature['properties']['id'])
|
|
527
527
|
|
|
528
|
-
return id_list
|
|
528
|
+
return id_list
|
|
529
|
+
|
|
530
|
+
def get_buildings_in_drawn_polygon(building_geojson, drawn_polygon_vertices,
|
|
531
|
+
operation='within'):
|
|
532
|
+
"""
|
|
533
|
+
Given a list of building footprints (in Lat-Lon) and a set of drawn polygon
|
|
534
|
+
vertices (also in Lat-Lon), return the building IDs that fall within or
|
|
535
|
+
intersect the drawn polygon.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
building_geojson (list):
|
|
539
|
+
A list of GeoJSON features, each feature is a dict with:
|
|
540
|
+
{
|
|
541
|
+
"type": "Feature",
|
|
542
|
+
"geometry": {
|
|
543
|
+
"type": "Polygon",
|
|
544
|
+
"coordinates": [
|
|
545
|
+
[
|
|
546
|
+
[lat1, lon1], [lat2, lon2], ...
|
|
547
|
+
]
|
|
548
|
+
]
|
|
549
|
+
},
|
|
550
|
+
"properties": {
|
|
551
|
+
"id": ...
|
|
552
|
+
...
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
Note: These coordinates are in (lat, lon) order, not standard (lon, lat).
|
|
556
|
+
|
|
557
|
+
drawn_polygon_vertices (list):
|
|
558
|
+
A list of (lat, lon) tuples representing the polygon drawn by the user.
|
|
559
|
+
|
|
560
|
+
operation (str):
|
|
561
|
+
Determines how to include buildings.
|
|
562
|
+
Use "intersect" to include buildings that intersect the drawn polygon.
|
|
563
|
+
Use "within" to include buildings that lie entirely within the drawn polygon.
|
|
564
|
+
|
|
565
|
+
Returns:
|
|
566
|
+
list:
|
|
567
|
+
A list of building IDs (strings or ints) that satisfy the condition.
|
|
568
|
+
"""
|
|
569
|
+
# 1. Convert the user-drawn polygon vertices (lat, lon) into a Shapely Polygon.
|
|
570
|
+
# Shapely expects (x, y) = (longitude, latitude).
|
|
571
|
+
# So we'll do (lon, lat) for each vertex.
|
|
572
|
+
drawn_polygon_shapely = Polygon([(lon, lat) for (lat, lon) in drawn_polygon_vertices])
|
|
573
|
+
|
|
574
|
+
included_building_ids = []
|
|
575
|
+
|
|
576
|
+
# 2. Check each building in the GeoJSON
|
|
577
|
+
for feature in building_geojson:
|
|
578
|
+
# Skip any feature that is not Polygon
|
|
579
|
+
if feature['geometry']['type'] != 'Polygon':
|
|
580
|
+
continue
|
|
581
|
+
|
|
582
|
+
# Extract coordinates, which are in [ [lat, lon], [lat, lon], ... ]
|
|
583
|
+
coords = feature['geometry']['coordinates'][0]
|
|
584
|
+
|
|
585
|
+
# Create a Shapely polygon for the building
|
|
586
|
+
# Convert from (lat, lon) to (lon, lat)
|
|
587
|
+
building_polygon = Polygon([(lon, lat) for (lat, lon) in coords])
|
|
588
|
+
|
|
589
|
+
# 3. Depending on the operation, check the relationship
|
|
590
|
+
if operation == 'intersect':
|
|
591
|
+
if building_polygon.intersects(drawn_polygon_shapely):
|
|
592
|
+
included_building_ids.append(feature['properties'].get('id', None))
|
|
593
|
+
elif operation == 'within':
|
|
594
|
+
if building_polygon.within(drawn_polygon_shapely):
|
|
595
|
+
included_building_ids.append(feature['properties'].get('id', None))
|
|
596
|
+
else:
|
|
597
|
+
raise ValueError("operation must be 'intersect' or 'within'")
|
|
598
|
+
|
|
599
|
+
return included_building_ids
|
voxcity/geo/draw.py
CHANGED
|
@@ -4,10 +4,11 @@ This module provides functions for drawing and manipulating rectangles on maps.
|
|
|
4
4
|
|
|
5
5
|
import math
|
|
6
6
|
from pyproj import Proj, transform
|
|
7
|
-
from ipyleaflet import Map, DrawControl, Rectangle
|
|
7
|
+
from ipyleaflet import Map, DrawControl, Rectangle, Polygon as LeafletPolygon
|
|
8
8
|
import ipyleaflet
|
|
9
9
|
from geopy import distance
|
|
10
10
|
from .utils import get_coordinates_from_cityname
|
|
11
|
+
import shapely.geometry as geom
|
|
11
12
|
|
|
12
13
|
def rotate_rectangle(m, rectangle_vertices, angle):
|
|
13
14
|
"""
|
|
@@ -222,4 +223,113 @@ def center_location_map_cityname(cityname, east_west_length, north_south_length,
|
|
|
222
223
|
# Register event handler for drawing actions
|
|
223
224
|
draw_control.on_draw(handle_draw)
|
|
224
225
|
|
|
225
|
-
return m, rectangle_vertices
|
|
226
|
+
return m, rectangle_vertices
|
|
227
|
+
|
|
228
|
+
def display_buildings_and_draw_polygon(building_geojson, zoom=17):
|
|
229
|
+
"""
|
|
230
|
+
Displays building footprints (in Lat-Lon order) on an ipyleaflet map,
|
|
231
|
+
and allows the user to draw a polygon whose vertices are returned
|
|
232
|
+
in a Python list (also in Lat-Lon).
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
building_geojson (list): A list of GeoJSON features (Polygons),
|
|
236
|
+
BUT with coordinates in [lat, lon] order.
|
|
237
|
+
zoom (int): Initial zoom level for the map. Default=17.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
(map_object, drawn_polygon_vertices)
|
|
241
|
+
- map_object: ipyleaflet Map instance
|
|
242
|
+
- drawn_polygon_vertices: a Python list that gets updated whenever
|
|
243
|
+
a new polygon is created. The list is in (lat, lon) order.
|
|
244
|
+
"""
|
|
245
|
+
# ---------------------------------------------------------
|
|
246
|
+
# 1. Determine a suitable map center via bounding box logic
|
|
247
|
+
# ---------------------------------------------------------
|
|
248
|
+
all_lats = []
|
|
249
|
+
all_lons = []
|
|
250
|
+
for feature in building_geojson:
|
|
251
|
+
# Handle only Polygons here; skip MultiPolygon if present
|
|
252
|
+
if feature['geometry']['type'] == 'Polygon':
|
|
253
|
+
# Coordinates in this data are [ [lat, lon], [lat, lon], ... ]
|
|
254
|
+
coords = feature['geometry']['coordinates'][0] # outer ring
|
|
255
|
+
all_lats.extend(pt[0] for pt in coords)
|
|
256
|
+
all_lons.extend(pt[1] for pt in coords)
|
|
257
|
+
|
|
258
|
+
if not all_lats or not all_lons:
|
|
259
|
+
# Fallback: If no footprints or invalid data, pick a default
|
|
260
|
+
center_lat, center_lon = 40.0, -100.0
|
|
261
|
+
else:
|
|
262
|
+
min_lat, max_lat = min(all_lats), max(all_lats)
|
|
263
|
+
min_lon, max_lon = min(all_lons), max(all_lons)
|
|
264
|
+
center_lat = (min_lat + max_lat) / 2
|
|
265
|
+
center_lon = (min_lon + max_lon) / 2
|
|
266
|
+
|
|
267
|
+
# Create the ipyleaflet map
|
|
268
|
+
m = Map(center=(center_lat, center_lon), zoom=zoom, scroll_wheel_zoom=True)
|
|
269
|
+
|
|
270
|
+
# -----------------------------------------
|
|
271
|
+
# 2. Add each building footprint to the map
|
|
272
|
+
# -----------------------------------------
|
|
273
|
+
for feature in building_geojson:
|
|
274
|
+
# Only handle simple Polygons
|
|
275
|
+
if feature['geometry']['type'] == 'Polygon':
|
|
276
|
+
coords = feature['geometry']['coordinates'][0]
|
|
277
|
+
# Because your data is already lat-lon, we do NOT swap:
|
|
278
|
+
lat_lon_coords = [(c[0], c[1]) for c in coords]
|
|
279
|
+
|
|
280
|
+
# Create the polygon layer
|
|
281
|
+
bldg_layer = LeafletPolygon(
|
|
282
|
+
locations=lat_lon_coords,
|
|
283
|
+
color="blue",
|
|
284
|
+
fill_color="blue",
|
|
285
|
+
fill_opacity=0.2,
|
|
286
|
+
weight=2
|
|
287
|
+
)
|
|
288
|
+
m.add_layer(bldg_layer)
|
|
289
|
+
|
|
290
|
+
# -----------------------------------------------------------------
|
|
291
|
+
# 3. Enable drawing of polygons, capturing the vertices in Lat-Lon
|
|
292
|
+
# -----------------------------------------------------------------
|
|
293
|
+
drawn_polygon_vertices = [] # We'll store the newly drawn polygon's vertices here (lat, lon).
|
|
294
|
+
|
|
295
|
+
draw_control = DrawControl(
|
|
296
|
+
polygon={
|
|
297
|
+
"shapeOptions": {
|
|
298
|
+
"color": "#6bc2e5",
|
|
299
|
+
"fillColor": "#6bc2e5",
|
|
300
|
+
"fillOpacity": 0.2
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
rectangle={}, # Disable rectangles (or enable if needed)
|
|
304
|
+
circle={}, # Disable circles
|
|
305
|
+
circlemarker={}, # Disable circlemarkers
|
|
306
|
+
polyline={}, # Disable polylines
|
|
307
|
+
marker={} # Disable markers
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
def handle_draw(self, action, geo_json):
|
|
311
|
+
"""
|
|
312
|
+
Callback for whenever a shape is created or edited.
|
|
313
|
+
ipyleaflet's DrawControl returns standard GeoJSON (lon, lat).
|
|
314
|
+
We'll convert them to (lat, lon).
|
|
315
|
+
"""
|
|
316
|
+
# Clear any previously stored vertices
|
|
317
|
+
drawn_polygon_vertices.clear()
|
|
318
|
+
|
|
319
|
+
if action == 'created' and geo_json['geometry']['type'] == 'Polygon':
|
|
320
|
+
# The polygon’s first ring
|
|
321
|
+
coordinates = geo_json['geometry']['coordinates'][0]
|
|
322
|
+
print("Vertices of the drawn polygon (Lat-Lon):")
|
|
323
|
+
|
|
324
|
+
# By default, drawn polygon coords are [ [lon, lat], [lon, lat], ... ]
|
|
325
|
+
# The last coordinate repeats the first -> skip it with [:-1]
|
|
326
|
+
for coord in coordinates[:-1]:
|
|
327
|
+
lon = coord[0]
|
|
328
|
+
lat = coord[1]
|
|
329
|
+
drawn_polygon_vertices.append((lat, lon))
|
|
330
|
+
print(f" - (lat, lon) = ({lat}, {lon})")
|
|
331
|
+
|
|
332
|
+
draw_control.on_draw(handle_draw)
|
|
333
|
+
m.add_control(draw_control)
|
|
334
|
+
|
|
335
|
+
return m, drawn_polygon_vertices
|
voxcity/sim/solar.py
CHANGED
|
@@ -16,10 +16,16 @@ def compute_direct_solar_irradiance_map_binary(voxel_data, sun_direction, view_p
|
|
|
16
16
|
"""
|
|
17
17
|
Compute a map of direct solar irradiation accounting for tree transmittance.
|
|
18
18
|
|
|
19
|
+
The function:
|
|
20
|
+
1. Places observers at valid locations (empty voxels above ground)
|
|
21
|
+
2. Casts rays from each observer in the sun direction
|
|
22
|
+
3. Computes transmittance through trees using Beer-Lambert law
|
|
23
|
+
4. Returns a 2D map of transmittance values
|
|
24
|
+
|
|
19
25
|
Args:
|
|
20
26
|
voxel_data (ndarray): 3D array of voxel values.
|
|
21
27
|
sun_direction (tuple): Direction vector of the sun.
|
|
22
|
-
|
|
28
|
+
view_point_height (float): Observer height in meters.
|
|
23
29
|
hit_values (tuple): Values considered non-obstacles if inclusion_mode=False.
|
|
24
30
|
meshsize (float): Size of each voxel in meters.
|
|
25
31
|
tree_k (float): Tree extinction coefficient.
|
|
@@ -27,7 +33,7 @@ def compute_direct_solar_irradiance_map_binary(voxel_data, sun_direction, view_p
|
|
|
27
33
|
inclusion_mode (bool): False here, meaning any voxel not in hit_values is an obstacle.
|
|
28
34
|
|
|
29
35
|
Returns:
|
|
30
|
-
ndarray: 2D array of transmittance values (0.0-1.0), NaN = invalid observer.
|
|
36
|
+
ndarray: 2D array of transmittance values (0.0-1.0), NaN = invalid observer position.
|
|
31
37
|
"""
|
|
32
38
|
|
|
33
39
|
view_height_voxel = int(view_point_height / meshsize)
|
|
@@ -35,18 +41,22 @@ def compute_direct_solar_irradiance_map_binary(voxel_data, sun_direction, view_p
|
|
|
35
41
|
nx, ny, nz = voxel_data.shape
|
|
36
42
|
irradiance_map = np.full((nx, ny), np.nan, dtype=np.float64)
|
|
37
43
|
|
|
38
|
-
# Normalize sun direction
|
|
44
|
+
# Normalize sun direction vector for ray tracing
|
|
39
45
|
sd = np.array(sun_direction, dtype=np.float64)
|
|
40
46
|
sd_len = np.sqrt(sd[0]**2 + sd[1]**2 + sd[2]**2)
|
|
41
47
|
if sd_len == 0.0:
|
|
42
48
|
return np.flipud(irradiance_map)
|
|
43
49
|
sd /= sd_len
|
|
44
50
|
|
|
51
|
+
# Process each x,y position in parallel
|
|
45
52
|
for x in prange(nx):
|
|
46
53
|
for y in range(ny):
|
|
47
54
|
found_observer = False
|
|
55
|
+
# Search upward for valid observer position
|
|
48
56
|
for z in range(1, nz):
|
|
57
|
+
# Check if current voxel is empty/tree and voxel below is solid
|
|
49
58
|
if voxel_data[x, y, z] in (0, -2) and voxel_data[x, y, z - 1] not in (0, -2):
|
|
59
|
+
# Skip if standing on building/vegetation/water
|
|
50
60
|
if voxel_data[x, y, z - 1] in (-30, -3, -2):
|
|
51
61
|
irradiance_map[x, y] = np.nan
|
|
52
62
|
found_observer = True
|
|
@@ -62,12 +72,43 @@ def compute_direct_solar_irradiance_map_binary(voxel_data, sun_direction, view_p
|
|
|
62
72
|
if not found_observer:
|
|
63
73
|
irradiance_map[x, y] = np.nan
|
|
64
74
|
|
|
75
|
+
# Flip map vertically to match visualization conventions
|
|
65
76
|
return np.flipud(irradiance_map)
|
|
66
77
|
|
|
67
78
|
def get_direct_solar_irradiance_map(voxel_data, meshsize, azimuth_degrees_ori, elevation_degrees,
|
|
68
79
|
direct_normal_irradiance, show_plot=False, **kwargs):
|
|
69
80
|
"""
|
|
70
81
|
Compute direct solar irradiance map with tree transmittance.
|
|
82
|
+
|
|
83
|
+
The function:
|
|
84
|
+
1. Converts sun angles to direction vector
|
|
85
|
+
2. Computes binary transmittance map
|
|
86
|
+
3. Scales by direct normal irradiance and sun elevation
|
|
87
|
+
4. Optionally visualizes and exports results
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
voxel_data (ndarray): 3D array of voxel values.
|
|
91
|
+
meshsize (float): Size of each voxel in meters.
|
|
92
|
+
azimuth_degrees_ori (float): Sun azimuth angle in degrees (0° = North, 90° = East).
|
|
93
|
+
elevation_degrees (float): Sun elevation angle in degrees above horizon.
|
|
94
|
+
direct_normal_irradiance (float): Direct normal irradiance in W/m².
|
|
95
|
+
show_plot (bool): Whether to display visualization.
|
|
96
|
+
**kwargs: Additional arguments including:
|
|
97
|
+
- view_point_height (float): Observer height in meters (default: 1.5)
|
|
98
|
+
- colormap (str): Matplotlib colormap name (default: 'magma')
|
|
99
|
+
- vmin (float): Minimum value for colormap
|
|
100
|
+
- vmax (float): Maximum value for colormap
|
|
101
|
+
- tree_k (float): Tree extinction coefficient (default: 0.6)
|
|
102
|
+
- tree_lad (float): Leaf area density in m^-1 (default: 1.0)
|
|
103
|
+
- obj_export (bool): Whether to export as OBJ file
|
|
104
|
+
- output_directory (str): Directory for OBJ export
|
|
105
|
+
- output_file_name (str): Filename for OBJ export
|
|
106
|
+
- dem_grid (ndarray): DEM grid for OBJ export
|
|
107
|
+
- num_colors (int): Number of colors for OBJ export
|
|
108
|
+
- alpha (float): Alpha value for OBJ export
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
ndarray: 2D array of direct solar irradiance values (W/m²).
|
|
71
112
|
"""
|
|
72
113
|
view_point_height = kwargs.get("view_point_height", 1.5)
|
|
73
114
|
colormap = kwargs.get("colormap", 'magma')
|
|
@@ -78,7 +119,8 @@ def get_direct_solar_irradiance_map(voxel_data, meshsize, azimuth_degrees_ori, e
|
|
|
78
119
|
tree_k = kwargs.get("tree_k", 0.6)
|
|
79
120
|
tree_lad = kwargs.get("tree_lad", 1.0)
|
|
80
121
|
|
|
81
|
-
# Convert angles to direction
|
|
122
|
+
# Convert sun angles to direction vector
|
|
123
|
+
# Note: azimuth is adjusted by 180° to match coordinate system
|
|
82
124
|
azimuth_degrees = 180 - azimuth_degrees_ori
|
|
83
125
|
azimuth_radians = np.deg2rad(azimuth_degrees)
|
|
84
126
|
elevation_radians = np.deg2rad(elevation_degrees)
|
|
@@ -91,14 +133,17 @@ def get_direct_solar_irradiance_map(voxel_data, meshsize, azimuth_degrees_ori, e
|
|
|
91
133
|
hit_values = (0,)
|
|
92
134
|
inclusion_mode = False
|
|
93
135
|
|
|
136
|
+
# Compute transmittance map
|
|
94
137
|
transmittance_map = compute_direct_solar_irradiance_map_binary(
|
|
95
138
|
voxel_data, sun_direction, view_point_height, hit_values,
|
|
96
139
|
meshsize, tree_k, tree_lad, inclusion_mode
|
|
97
140
|
)
|
|
98
141
|
|
|
142
|
+
# Scale by direct normal irradiance and sun elevation
|
|
99
143
|
sin_elev = dz
|
|
100
144
|
direct_map = transmittance_map * direct_normal_irradiance * sin_elev
|
|
101
145
|
|
|
146
|
+
# Optional visualization
|
|
102
147
|
if show_plot:
|
|
103
148
|
cmap = plt.cm.get_cmap(colormap).copy()
|
|
104
149
|
cmap.set_bad(color='lightgray')
|
|
@@ -135,6 +180,33 @@ def get_direct_solar_irradiance_map(voxel_data, meshsize, azimuth_degrees_ori, e
|
|
|
135
180
|
def get_diffuse_solar_irradiance_map(voxel_data, meshsize, diffuse_irradiance=1.0, show_plot=False, **kwargs):
|
|
136
181
|
"""
|
|
137
182
|
Compute diffuse solar irradiance map using the Sky View Factor (SVF) with tree transmittance.
|
|
183
|
+
|
|
184
|
+
The function:
|
|
185
|
+
1. Computes SVF map accounting for tree transmittance
|
|
186
|
+
2. Scales SVF by diffuse horizontal irradiance
|
|
187
|
+
3. Optionally visualizes and exports results
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
voxel_data (ndarray): 3D array of voxel values.
|
|
191
|
+
meshsize (float): Size of each voxel in meters.
|
|
192
|
+
diffuse_irradiance (float): Diffuse horizontal irradiance in W/m².
|
|
193
|
+
show_plot (bool): Whether to display visualization.
|
|
194
|
+
**kwargs: Additional arguments including:
|
|
195
|
+
- view_point_height (float): Observer height in meters (default: 1.5)
|
|
196
|
+
- colormap (str): Matplotlib colormap name (default: 'magma')
|
|
197
|
+
- vmin (float): Minimum value for colormap
|
|
198
|
+
- vmax (float): Maximum value for colormap
|
|
199
|
+
- tree_k (float): Tree extinction coefficient
|
|
200
|
+
- tree_lad (float): Leaf area density in m^-1
|
|
201
|
+
- obj_export (bool): Whether to export as OBJ file
|
|
202
|
+
- output_directory (str): Directory for OBJ export
|
|
203
|
+
- output_file_name (str): Filename for OBJ export
|
|
204
|
+
- dem_grid (ndarray): DEM grid for OBJ export
|
|
205
|
+
- num_colors (int): Number of colors for OBJ export
|
|
206
|
+
- alpha (float): Alpha value for OBJ export
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
ndarray: 2D array of diffuse solar irradiance values (W/m²).
|
|
138
210
|
"""
|
|
139
211
|
|
|
140
212
|
view_point_height = kwargs.get("view_point_height", 1.5)
|
|
@@ -152,6 +224,7 @@ def get_diffuse_solar_irradiance_map(voxel_data, meshsize, diffuse_irradiance=1.
|
|
|
152
224
|
SVF_map = get_sky_view_factor_map(voxel_data, meshsize, **svf_kwargs)
|
|
153
225
|
diffuse_map = SVF_map * diffuse_irradiance
|
|
154
226
|
|
|
227
|
+
# Optional visualization
|
|
155
228
|
if show_plot:
|
|
156
229
|
vmin = kwargs.get("vmin", 0.0)
|
|
157
230
|
vmax = kwargs.get("vmax", diffuse_irradiance)
|
|
@@ -201,18 +274,35 @@ def get_global_solar_irradiance_map(
|
|
|
201
274
|
"""
|
|
202
275
|
Compute global solar irradiance (direct + diffuse) on a horizontal plane at each valid observer location.
|
|
203
276
|
|
|
204
|
-
|
|
277
|
+
The function:
|
|
278
|
+
1. Computes direct solar irradiance map
|
|
279
|
+
2. Computes diffuse solar irradiance map
|
|
280
|
+
3. Combines maps and optionally visualizes/exports results
|
|
205
281
|
|
|
206
282
|
Args:
|
|
207
283
|
voxel_data (ndarray): 3D voxel array.
|
|
208
284
|
meshsize (float): Voxel size in meters.
|
|
209
|
-
azimuth_degrees (float): Sun azimuth angle in degrees.
|
|
210
|
-
elevation_degrees (float): Sun elevation angle in degrees.
|
|
211
|
-
direct_normal_irradiance (float):
|
|
212
|
-
diffuse_irradiance (float): Diffuse irradiance in W/m².
|
|
285
|
+
azimuth_degrees (float): Sun azimuth angle in degrees (0° = North, 90° = East).
|
|
286
|
+
elevation_degrees (float): Sun elevation angle in degrees above horizon.
|
|
287
|
+
direct_normal_irradiance (float): Direct normal irradiance in W/m².
|
|
288
|
+
diffuse_irradiance (float): Diffuse horizontal irradiance in W/m².
|
|
289
|
+
show_plot (bool): Whether to display visualization.
|
|
290
|
+
**kwargs: Additional arguments including:
|
|
291
|
+
- view_point_height (float): Observer height in meters (default: 1.5)
|
|
292
|
+
- colormap (str): Matplotlib colormap name (default: 'magma')
|
|
293
|
+
- vmin (float): Minimum value for colormap
|
|
294
|
+
- vmax (float): Maximum value for colormap
|
|
295
|
+
- tree_k (float): Tree extinction coefficient
|
|
296
|
+
- tree_lad (float): Leaf area density in m^-1
|
|
297
|
+
- obj_export (bool): Whether to export as OBJ file
|
|
298
|
+
- output_directory (str): Directory for OBJ export
|
|
299
|
+
- output_file_name (str): Filename for OBJ export
|
|
300
|
+
- dem_grid (ndarray): DEM grid for OBJ export
|
|
301
|
+
- num_colors (int): Number of colors for OBJ export
|
|
302
|
+
- alpha (float): Alpha value for OBJ export
|
|
213
303
|
|
|
214
304
|
Returns:
|
|
215
|
-
ndarray: 2D array of global solar irradiance (W/m²).
|
|
305
|
+
ndarray: 2D array of global solar irradiance values (W/m²).
|
|
216
306
|
"""
|
|
217
307
|
|
|
218
308
|
colormap = kwargs.get("colormap", 'magma')
|
|
@@ -242,12 +332,13 @@ def get_global_solar_irradiance_map(
|
|
|
242
332
|
**direct_diffuse_kwargs
|
|
243
333
|
)
|
|
244
334
|
|
|
245
|
-
# Sum the two
|
|
335
|
+
# Sum the two components
|
|
246
336
|
global_map = direct_map + diffuse_map
|
|
247
337
|
|
|
248
338
|
vmin = kwargs.get("vmin", np.nanmin(global_map))
|
|
249
339
|
vmax = kwargs.get("vmax", np.nanmax(global_map))
|
|
250
340
|
|
|
341
|
+
# Optional visualization
|
|
251
342
|
if show_plot:
|
|
252
343
|
cmap = plt.cm.get_cmap(colormap).copy()
|
|
253
344
|
cmap.set_bad(color='lightgray')
|
|
@@ -286,7 +377,19 @@ def get_global_solar_irradiance_map(
|
|
|
286
377
|
def get_solar_positions_astral(times, lat, lon):
|
|
287
378
|
"""
|
|
288
379
|
Compute solar azimuth and elevation using Astral for given times and location.
|
|
289
|
-
|
|
380
|
+
|
|
381
|
+
The function:
|
|
382
|
+
1. Creates an Astral observer at the specified location
|
|
383
|
+
2. Computes sun position for each timestamp
|
|
384
|
+
3. Returns DataFrame with azimuth and elevation angles
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
times (DatetimeIndex): Array of timezone-aware datetime objects.
|
|
388
|
+
lat (float): Latitude in degrees.
|
|
389
|
+
lon (float): Longitude in degrees.
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
DataFrame: DataFrame with columns 'azimuth' and 'elevation' containing solar positions.
|
|
290
393
|
"""
|
|
291
394
|
observer = Observer(latitude=lat, longitude=lon)
|
|
292
395
|
df_pos = pd.DataFrame(index=times, columns=['azimuth', 'elevation'], dtype=float)
|
|
@@ -309,27 +412,43 @@ def get_cumulative_global_solar_irradiance(
|
|
|
309
412
|
**kwargs
|
|
310
413
|
):
|
|
311
414
|
"""
|
|
312
|
-
Compute cumulative global solar irradiance over a specified period using data from an EPW file
|
|
313
|
-
|
|
415
|
+
Compute cumulative global solar irradiance over a specified period using data from an EPW file.
|
|
416
|
+
|
|
417
|
+
The function:
|
|
418
|
+
1. Filters EPW data for specified time period
|
|
419
|
+
2. Computes sun positions for each timestep
|
|
420
|
+
3. Calculates and accumulates global irradiance maps
|
|
421
|
+
4. Handles tree transmittance and visualization
|
|
314
422
|
|
|
315
423
|
Args:
|
|
316
424
|
voxel_data (ndarray): 3D array of voxel values.
|
|
317
425
|
meshsize (float): Size of each voxel in meters.
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
426
|
+
df (DataFrame): EPW weather data.
|
|
427
|
+
lat (float): Latitude in degrees.
|
|
428
|
+
lon (float): Longitude in degrees.
|
|
429
|
+
tz (float): Timezone offset in hours.
|
|
430
|
+
direct_normal_irradiance_scaling (float): Scaling factor for direct normal irradiance.
|
|
431
|
+
diffuse_irradiance_scaling (float): Scaling factor for diffuse horizontal irradiance.
|
|
322
432
|
**kwargs: Additional arguments including:
|
|
323
|
-
- view_point_height (float): Observer height in meters
|
|
324
|
-
-
|
|
325
|
-
-
|
|
326
|
-
-
|
|
327
|
-
-
|
|
433
|
+
- view_point_height (float): Observer height in meters (default: 1.5)
|
|
434
|
+
- start_time (str): Start time in format 'MM-DD HH:MM:SS'
|
|
435
|
+
- end_time (str): End time in format 'MM-DD HH:MM:SS'
|
|
436
|
+
- tree_k (float): Tree extinction coefficient
|
|
437
|
+
- tree_lad (float): Leaf area density in m^-1
|
|
328
438
|
- show_plot (bool): Whether to show final plot
|
|
329
439
|
- show_each_timestep (bool): Whether to show plots for each timestep
|
|
440
|
+
- colormap (str): Matplotlib colormap name
|
|
441
|
+
- vmin (float): Minimum value for colormap
|
|
442
|
+
- vmax (float): Maximum value for colormap
|
|
443
|
+
- obj_export (bool): Whether to export as OBJ file
|
|
444
|
+
- output_directory (str): Directory for OBJ export
|
|
445
|
+
- output_file_name (str): Filename for OBJ export
|
|
446
|
+
- dem_grid (ndarray): DEM grid for OBJ export
|
|
447
|
+
- num_colors (int): Number of colors for OBJ export
|
|
448
|
+
- alpha (float): Alpha value for OBJ export
|
|
330
449
|
|
|
331
450
|
Returns:
|
|
332
|
-
ndarray: 2D array of cumulative global solar irradiance (W/m²·hour).
|
|
451
|
+
ndarray: 2D array of cumulative global solar irradiance values (W/m²·hour).
|
|
333
452
|
"""
|
|
334
453
|
view_point_height = kwargs.get("view_point_height", 1.5)
|
|
335
454
|
colormap = kwargs.get("colormap", 'magma')
|
|
@@ -346,20 +465,23 @@ def get_cumulative_global_solar_irradiance(
|
|
|
346
465
|
except ValueError as ve:
|
|
347
466
|
raise ValueError("start_time and end_time must be in format 'MM-DD HH:MM:SS'") from ve
|
|
348
467
|
|
|
349
|
-
# Add hour of year column and filter data
|
|
468
|
+
# Add hour of year column and filter data
|
|
350
469
|
df['hour_of_year'] = (df.index.dayofyear - 1) * 24 + df.index.hour + 1
|
|
351
470
|
|
|
471
|
+
# Convert dates to day of year and hour
|
|
352
472
|
start_doy = datetime(2000, start_dt.month, start_dt.day).timetuple().tm_yday
|
|
353
473
|
end_doy = datetime(2000, end_dt.month, end_dt.day).timetuple().tm_yday
|
|
354
474
|
|
|
355
475
|
start_hour = (start_doy - 1) * 24 + start_dt.hour + 1
|
|
356
476
|
end_hour = (end_doy - 1) * 24 + end_dt.hour + 1
|
|
357
477
|
|
|
478
|
+
# Handle period crossing year boundary
|
|
358
479
|
if start_hour <= end_hour:
|
|
359
480
|
df_period = df[(df['hour_of_year'] >= start_hour) & (df['hour_of_year'] <= end_hour)]
|
|
360
481
|
else:
|
|
361
482
|
df_period = df[(df['hour_of_year'] >= start_hour) | (df['hour_of_year'] <= end_hour)]
|
|
362
483
|
|
|
484
|
+
# Filter by minutes within start/end hours
|
|
363
485
|
df_period = df_period[
|
|
364
486
|
((df_period.index.hour != start_dt.hour) | (df_period.index.minute >= start_dt.minute)) &
|
|
365
487
|
((df_period.index.hour != end_dt.hour) | (df_period.index.minute <= end_dt.minute))
|
|
@@ -368,14 +490,14 @@ def get_cumulative_global_solar_irradiance(
|
|
|
368
490
|
if df_period.empty:
|
|
369
491
|
raise ValueError("No EPW data in the specified period.")
|
|
370
492
|
|
|
371
|
-
#
|
|
493
|
+
# Handle timezone conversion
|
|
372
494
|
offset_minutes = int(tz * 60)
|
|
373
495
|
local_tz = pytz.FixedOffset(offset_minutes)
|
|
374
496
|
df_period_local = df_period.copy()
|
|
375
497
|
df_period_local.index = df_period_local.index.tz_localize(local_tz)
|
|
376
498
|
df_period_utc = df_period_local.tz_convert(pytz.UTC)
|
|
377
499
|
|
|
378
|
-
# Compute solar positions
|
|
500
|
+
# Compute solar positions for period
|
|
379
501
|
solar_positions = get_solar_positions_astral(df_period_utc.index, lat, lon)
|
|
380
502
|
|
|
381
503
|
# Create kwargs for diffuse calculation
|
|
@@ -393,7 +515,7 @@ def get_cumulative_global_solar_irradiance(
|
|
|
393
515
|
**diffuse_kwargs
|
|
394
516
|
)
|
|
395
517
|
|
|
396
|
-
# Initialize maps
|
|
518
|
+
# Initialize accumulation maps
|
|
397
519
|
cumulative_map = np.zeros((voxel_data.shape[0], voxel_data.shape[1]))
|
|
398
520
|
mask_map = np.ones((voxel_data.shape[0], voxel_data.shape[1]), dtype=bool)
|
|
399
521
|
|
|
@@ -405,13 +527,14 @@ def get_cumulative_global_solar_irradiance(
|
|
|
405
527
|
'obj_export': False
|
|
406
528
|
})
|
|
407
529
|
|
|
408
|
-
#
|
|
530
|
+
# Process each timestep
|
|
409
531
|
for idx, (time_utc, row) in enumerate(df_period_utc.iterrows()):
|
|
532
|
+
# Get scaled irradiance values
|
|
410
533
|
DNI = row['DNI'] * direct_normal_irradiance_scaling
|
|
411
534
|
DHI = row['DHI'] * diffuse_irradiance_scaling
|
|
412
535
|
time_local = df_period_local.index[idx]
|
|
413
536
|
|
|
414
|
-
# Get solar position
|
|
537
|
+
# Get solar position for timestep
|
|
415
538
|
solpos = solar_positions.loc[time_utc]
|
|
416
539
|
azimuth_degrees = solpos['azimuth']
|
|
417
540
|
elevation_degrees = solpos['elevation']
|
|
@@ -426,13 +549,13 @@ def get_cumulative_global_solar_irradiance(
|
|
|
426
549
|
**direct_kwargs
|
|
427
550
|
)
|
|
428
551
|
|
|
429
|
-
# Scale
|
|
552
|
+
# Scale base diffuse map by actual DHI
|
|
430
553
|
diffuse_map = base_diffuse_map * DHI
|
|
431
554
|
|
|
432
|
-
# Combine direct and diffuse
|
|
555
|
+
# Combine direct and diffuse components
|
|
433
556
|
global_map = direct_map + diffuse_map
|
|
434
557
|
|
|
435
|
-
# Update
|
|
558
|
+
# Update valid pixel mask
|
|
436
559
|
mask_map &= ~np.isnan(global_map)
|
|
437
560
|
|
|
438
561
|
# Replace NaN with 0 for accumulation
|
|
@@ -453,7 +576,7 @@ def get_cumulative_global_solar_irradiance(
|
|
|
453
576
|
plt.colorbar(label='Global Solar Irradiance (W/m²)')
|
|
454
577
|
plt.show()
|
|
455
578
|
|
|
456
|
-
# Apply mask
|
|
579
|
+
# Apply mask to final result
|
|
457
580
|
cumulative_map[~mask_map] = np.nan
|
|
458
581
|
|
|
459
582
|
# Final visualization
|
|
@@ -506,27 +629,38 @@ def get_global_solar_irradiance_using_epw(
|
|
|
506
629
|
**kwargs
|
|
507
630
|
):
|
|
508
631
|
"""
|
|
509
|
-
Compute
|
|
510
|
-
accounting for tree transmittance.
|
|
632
|
+
Compute global solar irradiance using EPW weather data, either for a single time or cumulatively over a period.
|
|
511
633
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
)
|
|
521
|
-
|
|
522
|
-
|
|
634
|
+
The function:
|
|
635
|
+
1. Optionally downloads and reads EPW weather data
|
|
636
|
+
2. Handles timezone conversions and solar position calculations
|
|
637
|
+
3. Computes either instantaneous or cumulative irradiance maps
|
|
638
|
+
4. Supports visualization and export options
|
|
639
|
+
|
|
640
|
+
Args:
|
|
641
|
+
voxel_data (ndarray): 3D array of voxel values.
|
|
642
|
+
meshsize (float): Size of each voxel in meters.
|
|
643
|
+
calc_type (str): 'instantaneous' or 'cumulative'.
|
|
644
|
+
direct_normal_irradiance_scaling (float): Scaling factor for direct normal irradiance.
|
|
645
|
+
diffuse_irradiance_scaling (float): Scaling factor for diffuse horizontal irradiance.
|
|
646
|
+
**kwargs: Additional arguments including:
|
|
523
647
|
- download_nearest_epw (bool): Whether to download nearest EPW file
|
|
524
648
|
- epw_file_path (str): Path to EPW file
|
|
525
|
-
-
|
|
526
|
-
-
|
|
649
|
+
- rectangle_vertices (list): List of (lat,lon) coordinates for EPW download
|
|
650
|
+
- output_dir (str): Directory for EPW download
|
|
651
|
+
- calc_time (str): Time for instantaneous calculation ('MM-DD HH:MM:SS')
|
|
652
|
+
- start_time (str): Start time for cumulative calculation
|
|
653
|
+
- end_time (str): End time for cumulative calculation
|
|
654
|
+
- view_point_height (float): Observer height in meters
|
|
655
|
+
- tree_k (float): Tree extinction coefficient
|
|
656
|
+
- tree_lad (float): Leaf area density in m^-1
|
|
657
|
+
- show_plot (bool): Whether to show visualization
|
|
658
|
+
- show_each_timestep (bool): Whether to show timestep plots
|
|
659
|
+
- colormap (str): Matplotlib colormap name
|
|
660
|
+
- obj_export (bool): Whether to export as OBJ file
|
|
527
661
|
|
|
528
662
|
Returns:
|
|
529
|
-
ndarray: 2D array of
|
|
663
|
+
ndarray: 2D array of solar irradiance values (W/m²).
|
|
530
664
|
"""
|
|
531
665
|
view_point_height = kwargs.get("view_point_height", 1.5)
|
|
532
666
|
colormap = kwargs.get("colormap", 'magma')
|
voxcity/sim/view.py
CHANGED
|
@@ -2,15 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
This module provides functionality to compute and visualize:
|
|
4
4
|
- Green View Index (GVI): Measures visibility of green elements like trees and vegetation
|
|
5
|
-
- Sky View Index (SVI): Measures visibility of open sky from street level
|
|
5
|
+
- Sky View Index (SVI): Measures visibility of open sky from street level
|
|
6
|
+
- Sky View Factor (SVF): Measures the ratio of visible sky hemisphere to total hemisphere
|
|
6
7
|
- Landmark Visibility: Measures visibility of specified landmark buildings from different locations
|
|
7
8
|
|
|
8
9
|
The module uses optimized ray tracing techniques with Numba JIT compilation for efficient computation.
|
|
9
10
|
Key features:
|
|
10
11
|
- Generic ray tracing framework that can be customized for different view indices
|
|
11
12
|
- Parallel processing for fast computation of view maps
|
|
13
|
+
- Tree transmittance modeling using Beer-Lambert law
|
|
12
14
|
- Visualization tools including matplotlib plots and OBJ exports
|
|
13
15
|
- Support for both inclusion and exclusion based visibility checks
|
|
16
|
+
|
|
17
|
+
The module provides several key functions:
|
|
18
|
+
- trace_ray_generic(): Core ray tracing function that handles tree transmittance
|
|
19
|
+
- compute_vi_generic(): Computes view indices by casting rays in specified directions
|
|
20
|
+
- compute_vi_map_generic(): Generates 2D maps of view indices
|
|
21
|
+
- get_view_index(): High-level function to compute various view indices
|
|
22
|
+
- compute_landmark_visibility(): Computes visibility of landmark buildings
|
|
23
|
+
- get_sky_view_factor_map(): Computes sky view factor maps
|
|
24
|
+
|
|
25
|
+
The module uses a voxel-based representation where:
|
|
26
|
+
- Empty space is represented by 0
|
|
27
|
+
- Trees are represented by -2
|
|
28
|
+
- Buildings are represented by -3
|
|
29
|
+
- Other values can be used for different features
|
|
30
|
+
|
|
31
|
+
Tree transmittance is modeled using the Beer-Lambert law with configurable parameters:
|
|
32
|
+
- tree_k: Static extinction coefficient (default 0.6)
|
|
33
|
+
- tree_lad: Leaf area density in m^-1 (default 1.0)
|
|
34
|
+
|
|
35
|
+
Additional implementation details:
|
|
36
|
+
- Uses DDA (Digital Differential Analyzer) algorithm for efficient ray traversal
|
|
37
|
+
- Handles edge cases like zero-length rays and division by zero
|
|
38
|
+
- Supports early exit optimizations for performance
|
|
39
|
+
- Provides flexible observer placement rules
|
|
40
|
+
- Includes comprehensive error checking and validation
|
|
41
|
+
- Allows customization of visualization parameters
|
|
14
42
|
"""
|
|
15
43
|
|
|
16
44
|
import numpy as np
|
|
@@ -18,20 +46,31 @@ import matplotlib.pyplot as plt
|
|
|
18
46
|
import matplotlib.patches as mpatches
|
|
19
47
|
from numba import njit, prange
|
|
20
48
|
|
|
21
|
-
from ..file.geojson import find_building_containing_point
|
|
49
|
+
from ..file.geojson import find_building_containing_point, get_buildings_in_drawn_polygon
|
|
22
50
|
from ..file.obj import grid_to_obj, export_obj
|
|
23
51
|
|
|
24
52
|
@njit
|
|
25
53
|
def calculate_transmittance(length, tree_k=0.6, tree_lad=1.0):
|
|
26
54
|
"""Calculate tree transmittance using the Beer-Lambert law.
|
|
27
55
|
|
|
56
|
+
Uses the Beer-Lambert law to model light attenuation through tree canopy:
|
|
57
|
+
transmittance = exp(-k * LAD * L)
|
|
58
|
+
where:
|
|
59
|
+
- k is the extinction coefficient
|
|
60
|
+
- LAD is the leaf area density
|
|
61
|
+
- L is the path length through the canopy
|
|
62
|
+
|
|
28
63
|
Args:
|
|
29
64
|
length (float): Path length through tree voxel in meters
|
|
30
|
-
tree_k (float): Static extinction coefficient (default: 0.
|
|
65
|
+
tree_k (float): Static extinction coefficient (default: 0.6)
|
|
66
|
+
Controls overall light attenuation strength
|
|
31
67
|
tree_lad (float): Leaf area density in m^-1 (default: 1.0)
|
|
68
|
+
Higher values = denser foliage = more attenuation
|
|
32
69
|
|
|
33
70
|
Returns:
|
|
34
71
|
float: Transmittance value between 0 and 1
|
|
72
|
+
1.0 = fully transparent
|
|
73
|
+
0.0 = fully opaque
|
|
35
74
|
"""
|
|
36
75
|
return np.exp(-tree_k * tree_lad * length)
|
|
37
76
|
|
|
@@ -39,9 +78,34 @@ def calculate_transmittance(length, tree_k=0.6, tree_lad=1.0):
|
|
|
39
78
|
def trace_ray_generic(voxel_data, origin, direction, hit_values, meshsize, tree_k, tree_lad, inclusion_mode=True):
|
|
40
79
|
"""Trace a ray through a voxel grid and check for hits with specified values.
|
|
41
80
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
81
|
+
Uses DDA (Digital Differential Analyzer) algorithm for efficient ray traversal.
|
|
82
|
+
Handles tree transmittance using Beer-Lambert law.
|
|
83
|
+
|
|
84
|
+
The DDA algorithm:
|
|
85
|
+
1. Initializes ray at origin voxel
|
|
86
|
+
2. Calculates distances to next voxel boundaries in each direction
|
|
87
|
+
3. Steps to next voxel by choosing smallest distance
|
|
88
|
+
4. Repeats until hit or out of bounds
|
|
89
|
+
|
|
90
|
+
Tree transmittance:
|
|
91
|
+
- When ray passes through tree voxels (-2), transmittance is accumulated
|
|
92
|
+
- Uses Beer-Lambert law with configurable extinction coefficient and leaf area density
|
|
93
|
+
- Ray is considered blocked if cumulative transmittance falls below 0.01
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
voxel_data (ndarray): 3D array of voxel values
|
|
97
|
+
origin (ndarray): Starting point (x,y,z) of ray in voxel coordinates
|
|
98
|
+
direction (ndarray): Direction vector of ray (will be normalized)
|
|
99
|
+
hit_values (tuple): Values to check for hits
|
|
100
|
+
meshsize (float): Size of each voxel in meters
|
|
101
|
+
tree_k (float): Tree extinction coefficient
|
|
102
|
+
tree_lad (float): Leaf area density in m^-1
|
|
103
|
+
inclusion_mode (bool): If True, hit_values are hits. If False, hit_values are allowed values.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
tuple: (hit_detected, transmittance_value)
|
|
107
|
+
hit_detected (bool): Whether ray hit a target voxel
|
|
108
|
+
transmittance_value (float): Cumulative transmittance through trees
|
|
45
109
|
"""
|
|
46
110
|
nx, ny, nz = voxel_data.shape
|
|
47
111
|
x0, y0, z0 = origin
|
|
@@ -151,9 +215,28 @@ def trace_ray_generic(voxel_data, origin, direction, hit_values, meshsize, tree_
|
|
|
151
215
|
def compute_vi_generic(observer_location, voxel_data, ray_directions, hit_values, meshsize, tree_k, tree_lad, inclusion_mode=True):
|
|
152
216
|
"""Compute view index accounting for tree transmittance.
|
|
153
217
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
-
|
|
218
|
+
Casts rays in specified directions and computes visibility index based on hits and transmittance.
|
|
219
|
+
The view index is the ratio of visible rays to total rays cast, where:
|
|
220
|
+
- For inclusion mode: Counts hits with target values
|
|
221
|
+
- For exclusion mode: Counts rays that don't hit obstacles
|
|
222
|
+
Tree transmittance is handled specially:
|
|
223
|
+
- In inclusion mode with trees as targets: Uses (1 - transmittance) as contribution
|
|
224
|
+
- In exclusion mode: Uses transmittance value directly
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
observer_location (ndarray): Observer position (x,y,z) in voxel coordinates
|
|
228
|
+
voxel_data (ndarray): 3D array of voxel values
|
|
229
|
+
ray_directions (ndarray): Array of direction vectors for rays
|
|
230
|
+
hit_values (tuple): Values to check for hits
|
|
231
|
+
meshsize (float): Size of each voxel in meters
|
|
232
|
+
tree_k (float): Tree extinction coefficient
|
|
233
|
+
tree_lad (float): Leaf area density in m^-1
|
|
234
|
+
inclusion_mode (bool): If True, hit_values are hits. If False, hit_values are allowed values.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
float: View index value between 0 and 1
|
|
238
|
+
0.0 = no visibility in any direction
|
|
239
|
+
1.0 = full visibility in all directions
|
|
157
240
|
"""
|
|
158
241
|
total_rays = ray_directions.shape[0]
|
|
159
242
|
visibility_sum = 0.0
|
|
@@ -180,7 +263,31 @@ def compute_vi_generic(observer_location, voxel_data, ray_directions, hit_values
|
|
|
180
263
|
@njit(parallel=True)
|
|
181
264
|
def compute_vi_map_generic(voxel_data, ray_directions, view_height_voxel, hit_values,
|
|
182
265
|
meshsize, tree_k, tree_lad, inclusion_mode=True):
|
|
183
|
-
"""Compute view index map incorporating tree transmittance.
|
|
266
|
+
"""Compute view index map incorporating tree transmittance.
|
|
267
|
+
|
|
268
|
+
Places observers at valid locations and computes view index for each position.
|
|
269
|
+
Valid observer locations are:
|
|
270
|
+
- Empty voxels (0) or tree voxels (-2)
|
|
271
|
+
- Above non-empty, non-tree voxels
|
|
272
|
+
- Not above water (7,8,9) or negative values
|
|
273
|
+
|
|
274
|
+
The function processes each x,y position in parallel for efficiency.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
voxel_data (ndarray): 3D array of voxel values
|
|
278
|
+
ray_directions (ndarray): Array of direction vectors for rays
|
|
279
|
+
view_height_voxel (int): Observer height in voxel units
|
|
280
|
+
hit_values (tuple): Values to check for hits
|
|
281
|
+
meshsize (float): Size of each voxel in meters
|
|
282
|
+
tree_k (float): Tree extinction coefficient
|
|
283
|
+
tree_lad (float): Leaf area density in m^-1
|
|
284
|
+
inclusion_mode (bool): If True, hit_values are hits. If False, hit_values are allowed values.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
ndarray: 2D array of view index values
|
|
288
|
+
NaN = invalid observer location
|
|
289
|
+
0.0-1.0 = view index value
|
|
290
|
+
"""
|
|
184
291
|
nx, ny, nz = voxel_data.shape
|
|
185
292
|
vi_map = np.full((nx, ny), np.nan)
|
|
186
293
|
|
|
@@ -188,12 +295,15 @@ def compute_vi_map_generic(voxel_data, ray_directions, view_height_voxel, hit_va
|
|
|
188
295
|
for y in range(ny):
|
|
189
296
|
found_observer = False
|
|
190
297
|
for z in range(1, nz):
|
|
298
|
+
# Check for valid observer location
|
|
191
299
|
if voxel_data[x, y, z] in (0, -2) and voxel_data[x, y, z - 1] not in (0, -2):
|
|
300
|
+
# Skip invalid ground types
|
|
192
301
|
if (voxel_data[x, y, z - 1] in (7, 8, 9)) or (voxel_data[x, y, z - 1] < 0):
|
|
193
302
|
vi_map[x, y] = np.nan
|
|
194
303
|
found_observer = True
|
|
195
304
|
break
|
|
196
305
|
else:
|
|
306
|
+
# Place observer and compute view index
|
|
197
307
|
observer_location = np.array([x, y, z + view_height_voxel], dtype=np.float64)
|
|
198
308
|
vi_value = compute_vi_generic(observer_location, voxel_data, ray_directions,
|
|
199
309
|
hit_values, meshsize, tree_k, tree_lad, inclusion_mode)
|
|
@@ -208,6 +318,14 @@ def compute_vi_map_generic(voxel_data, ray_directions, view_height_voxel, hit_va
|
|
|
208
318
|
def get_view_index(voxel_data, meshsize, mode=None, hit_values=None, inclusion_mode=True, **kwargs):
|
|
209
319
|
"""Calculate and visualize a generic view index for a voxel city model.
|
|
210
320
|
|
|
321
|
+
This is a high-level function that provides a flexible interface for computing
|
|
322
|
+
various view indices. It handles:
|
|
323
|
+
- Mode presets for common indices (green, sky)
|
|
324
|
+
- Ray direction generation
|
|
325
|
+
- Tree transmittance parameters
|
|
326
|
+
- Visualization
|
|
327
|
+
- Optional OBJ export
|
|
328
|
+
|
|
211
329
|
Args:
|
|
212
330
|
voxel_data (ndarray): 3D array of voxel values.
|
|
213
331
|
meshsize (float): Size of each voxel in meters.
|
|
@@ -321,15 +439,21 @@ def get_view_index(voxel_data, meshsize, mode=None, hit_values=None, inclusion_m
|
|
|
321
439
|
|
|
322
440
|
return vi_map
|
|
323
441
|
|
|
324
|
-
def mark_building_by_id(
|
|
442
|
+
def mark_building_by_id(voxcity_grid_ori, building_id_grid_ori, ids, mark):
|
|
325
443
|
"""Mark specific buildings in the voxel grid with a given value.
|
|
326
444
|
|
|
445
|
+
Used to identify landmark buildings for visibility analysis.
|
|
446
|
+
Flips building ID grid vertically to match voxel grid orientation.
|
|
447
|
+
|
|
327
448
|
Args:
|
|
328
449
|
voxcity_grid (ndarray): 3D array of voxel values
|
|
329
450
|
building_id_grid_ori (ndarray): 2D array of building IDs
|
|
330
451
|
ids (list): List of building IDs to mark
|
|
331
452
|
mark (int): Value to mark the buildings with
|
|
332
453
|
"""
|
|
454
|
+
|
|
455
|
+
voxcity_grid = voxcity_grid_ori.copy()
|
|
456
|
+
|
|
333
457
|
# Flip building ID grid vertically to match voxel grid orientation
|
|
334
458
|
building_id_grid = np.flipud(building_id_grid_ori.copy())
|
|
335
459
|
|
|
@@ -342,17 +466,20 @@ def mark_building_by_id(voxcity_grid, building_id_grid_ori, ids, mark):
|
|
|
342
466
|
# Replace building voxels (-3) with mark value at this x,y position
|
|
343
467
|
z_mask = voxcity_grid[x, y, :] == -3
|
|
344
468
|
voxcity_grid[x, y, z_mask] = mark
|
|
469
|
+
|
|
470
|
+
return voxcity_grid
|
|
345
471
|
|
|
346
472
|
@njit
|
|
347
473
|
def trace_ray_to_target(voxel_data, origin, target, opaque_values):
|
|
348
474
|
"""Trace a ray from origin to target through voxel data.
|
|
349
475
|
|
|
350
476
|
Uses DDA algorithm to efficiently traverse voxels along ray path.
|
|
477
|
+
Checks for any opaque voxels blocking the line of sight.
|
|
351
478
|
|
|
352
479
|
Args:
|
|
353
480
|
voxel_data (ndarray): 3D array of voxel values
|
|
354
|
-
origin (tuple): Starting point (x,y,z)
|
|
355
|
-
target (tuple): End point (x,y,z)
|
|
481
|
+
origin (tuple): Starting point (x,y,z) in voxel coordinates
|
|
482
|
+
target (tuple): End point (x,y,z) in voxel coordinates
|
|
356
483
|
opaque_values (ndarray): Array of voxel values that block the ray
|
|
357
484
|
|
|
358
485
|
Returns:
|
|
@@ -443,8 +570,11 @@ def trace_ray_to_target(voxel_data, origin, target, opaque_values):
|
|
|
443
570
|
def compute_visibility_to_all_landmarks(observer_location, landmark_positions, voxel_data, opaque_values):
|
|
444
571
|
"""Check if any landmark is visible from the observer location.
|
|
445
572
|
|
|
573
|
+
Traces rays to each landmark position until finding one that's visible.
|
|
574
|
+
Uses optimized ray tracing with early exit on first visible landmark.
|
|
575
|
+
|
|
446
576
|
Args:
|
|
447
|
-
observer_location (ndarray): Observer position (x,y,z)
|
|
577
|
+
observer_location (ndarray): Observer position (x,y,z) in voxel coordinates
|
|
448
578
|
landmark_positions (ndarray): Array of landmark positions
|
|
449
579
|
voxel_data (ndarray): 3D array of voxel values
|
|
450
580
|
opaque_values (ndarray): Array of voxel values that block visibility
|
|
@@ -467,6 +597,12 @@ def compute_visibility_map(voxel_data, landmark_positions, opaque_values, view_h
|
|
|
467
597
|
Places observers at valid locations (empty voxels above ground, excluding building
|
|
468
598
|
roofs and vegetation) and checks visibility to any landmark.
|
|
469
599
|
|
|
600
|
+
The function processes each x,y position in parallel for efficiency.
|
|
601
|
+
Valid observer locations are:
|
|
602
|
+
- Empty voxels (0) or tree voxels (-2)
|
|
603
|
+
- Above non-empty, non-tree voxels
|
|
604
|
+
- Not above water (7,8,9) or negative values
|
|
605
|
+
|
|
470
606
|
Args:
|
|
471
607
|
voxel_data (ndarray): 3D array of voxel values
|
|
472
608
|
landmark_positions (ndarray): Array of landmark positions
|
|
@@ -474,7 +610,10 @@ def compute_visibility_map(voxel_data, landmark_positions, opaque_values, view_h
|
|
|
474
610
|
view_height_voxel (int): Height offset for observer in voxels
|
|
475
611
|
|
|
476
612
|
Returns:
|
|
477
|
-
ndarray: 2D array of visibility values
|
|
613
|
+
ndarray: 2D array of visibility values
|
|
614
|
+
NaN = invalid observer location
|
|
615
|
+
0 = no landmarks visible
|
|
616
|
+
1 = at least one landmark visible
|
|
478
617
|
"""
|
|
479
618
|
nx, ny, nz = voxel_data.shape
|
|
480
619
|
visibility_map = np.full((nx, ny), np.nan)
|
|
@@ -509,6 +648,12 @@ def compute_landmark_visibility(voxel_data, target_value=-30, view_height_voxel=
|
|
|
509
648
|
Places observers at valid locations and checks visibility to any landmark voxel.
|
|
510
649
|
Generates a binary visibility map and visualization.
|
|
511
650
|
|
|
651
|
+
The function:
|
|
652
|
+
1. Identifies all landmark voxels (target_value)
|
|
653
|
+
2. Determines which voxel values block visibility
|
|
654
|
+
3. Computes visibility from each valid observer location
|
|
655
|
+
4. Generates visualization with legend
|
|
656
|
+
|
|
512
657
|
Args:
|
|
513
658
|
voxel_data (ndarray): 3D array of voxel values
|
|
514
659
|
target_value (int, optional): Value used to identify landmark voxels. Defaults to -30.
|
|
@@ -517,6 +662,9 @@ def compute_landmark_visibility(voxel_data, target_value=-30, view_height_voxel=
|
|
|
517
662
|
|
|
518
663
|
Returns:
|
|
519
664
|
ndarray: 2D array of visibility values (0 or 1) with y-axis flipped
|
|
665
|
+
NaN = invalid observer location
|
|
666
|
+
0 = no landmarks visible
|
|
667
|
+
1 = at least one landmark visible
|
|
520
668
|
|
|
521
669
|
Raises:
|
|
522
670
|
ValueError: If no landmark voxels are found with the specified target_value
|
|
@@ -553,7 +701,7 @@ def compute_landmark_visibility(voxel_data, target_value=-30, view_height_voxel=
|
|
|
553
701
|
|
|
554
702
|
return np.flipud(visibility_map)
|
|
555
703
|
|
|
556
|
-
def get_landmark_visibility_map(
|
|
704
|
+
def get_landmark_visibility_map(voxcity_grid_ori, building_id_grid, building_geojson, meshsize, **kwargs):
|
|
557
705
|
"""Generate a visibility map for landmark buildings in a voxel city.
|
|
558
706
|
|
|
559
707
|
Places observers at valid locations and checks visibility to any part of the
|
|
@@ -590,25 +738,29 @@ def get_landmark_visibility_map(voxcity_grid, building_id_grid, building_geojson
|
|
|
590
738
|
# Get landmark building IDs either directly or by finding buildings in rectangle
|
|
591
739
|
features = building_geojson
|
|
592
740
|
landmark_ids = kwargs.get('landmark_building_ids', None)
|
|
741
|
+
landmark_polygon = kwargs.get('landmark_polygon', None)
|
|
593
742
|
if landmark_ids is None:
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
743
|
+
if landmark_polygon is not None:
|
|
744
|
+
landmark_ids = get_buildings_in_drawn_polygon(building_geojson, landmark_polygon, operation='within')
|
|
745
|
+
else:
|
|
746
|
+
rectangle_vertices = kwargs.get("rectangle_vertices", None)
|
|
747
|
+
if rectangle_vertices is None:
|
|
748
|
+
print("Cannot set landmark buildings. You need to input either of rectangle_vertices or landmark_ids.")
|
|
749
|
+
return None
|
|
750
|
+
|
|
751
|
+
# Calculate center point of rectangle
|
|
752
|
+
lats = [coord[0] for coord in rectangle_vertices]
|
|
753
|
+
lons = [coord[1] for coord in rectangle_vertices]
|
|
754
|
+
center_lat = (min(lats) + max(lats)) / 2
|
|
755
|
+
center_lon = (min(lons) + max(lons)) / 2
|
|
756
|
+
target_point = (center_lat, center_lon)
|
|
598
757
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
lons = [coord[1] for coord in rectangle_vertices]
|
|
602
|
-
center_lat = (min(lats) + max(lats)) / 2
|
|
603
|
-
center_lon = (min(lons) + max(lons)) / 2
|
|
604
|
-
target_point = (center_lat, center_lon)
|
|
605
|
-
|
|
606
|
-
# Find buildings at center point
|
|
607
|
-
landmark_ids = find_building_containing_point(features, target_point)
|
|
758
|
+
# Find buildings at center point
|
|
759
|
+
landmark_ids = find_building_containing_point(features, target_point)
|
|
608
760
|
|
|
609
761
|
# Mark landmark buildings in voxel grid with special value
|
|
610
762
|
target_value = -30
|
|
611
|
-
mark_building_by_id(
|
|
763
|
+
voxcity_grid = mark_building_by_id(voxcity_grid_ori, building_id_grid, landmark_ids, target_value)
|
|
612
764
|
|
|
613
765
|
# Compute visibility map
|
|
614
766
|
landmark_vis_map = compute_landmark_visibility(voxcity_grid, target_value=target_value, view_height_voxel=view_height_voxel, colormap=colormap)
|
|
@@ -641,7 +793,7 @@ def get_landmark_visibility_map(voxcity_grid, building_id_grid, building_geojson
|
|
|
641
793
|
output_file_name_vox = 'voxcity_' + output_file_name
|
|
642
794
|
export_obj(voxcity_grid, output_dir, output_file_name_vox, meshsize)
|
|
643
795
|
|
|
644
|
-
return landmark_vis_map
|
|
796
|
+
return landmark_vis_map, voxcity_grid
|
|
645
797
|
|
|
646
798
|
def get_sky_view_factor_map(voxel_data, meshsize, show_plot=False, **kwargs):
|
|
647
799
|
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: voxcity
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
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>
|
|
@@ -305,6 +305,62 @@ export_magicavoxel_vox(voxcity_grid, output_path, base_filename=base_filename)
|
|
|
305
305
|
|
|
306
306
|
### 6. Additional Use Cases
|
|
307
307
|
|
|
308
|
+
#### Compute Solar Irradiance:
|
|
309
|
+
|
|
310
|
+
```python
|
|
311
|
+
from voxcity.sim.solar import get_global_solar_irradiance_using_epw
|
|
312
|
+
|
|
313
|
+
solar_kwargs = {
|
|
314
|
+
"download_nearest_epw": True, # Whether to automatically download nearest EPW weather file based on location from Climate.OneBuilding.Org
|
|
315
|
+
"rectangle_vertices": rectangle_vertices, # Coordinates defining the area of interest for calculation
|
|
316
|
+
# "epw_file_path": "./output/new.york-downtown.manhattan.heli_ny_usa_1.epw", # Path to EnergyPlus Weather (EPW) file containing climate data. Set if you already have an EPW file.
|
|
317
|
+
"calc_time": "01-01 12:00:00", # Time for instantaneous calculation in format "MM-DD HH:MM:SS"
|
|
318
|
+
"view_point_height": 1.5, # Height of view point in meters for calculating solar access. Default: 1.5 m
|
|
319
|
+
"tree_k": 0.6, # Static extinction coefficient - controls how much sunlight is blocked by trees (higher = more blocking)
|
|
320
|
+
"tree_lad": 1.0, # Leaf area density of trees - density of leaves/branches that affect shading (higher = denser foliage)
|
|
321
|
+
"dem_grid": dem_grid, # Digital elevation model grid for terrain heights
|
|
322
|
+
"colormap": 'magma', # Matplotlib colormap for visualization. Default: 'viridis'
|
|
323
|
+
"obj_export": True, # Whether to export results as 3D OBJ file
|
|
324
|
+
"output_directory": 'output/test', # Directory for saving output files
|
|
325
|
+
"output_file_name": 'instantaneous_solar_irradiance', # Base filename for outputs (without extension)
|
|
326
|
+
"alpha": 1.0, # Transparency of visualization (0.0-1.0)
|
|
327
|
+
"vmin": 0, # Minimum value for colormap scaling in visualization
|
|
328
|
+
# "vmax": 900, # Maximum value for colormap scaling in visualization
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
# Compute global solar irradiance map (direct + diffuse radiation)
|
|
332
|
+
global_map = get_global_solar_irradiance_using_epw(
|
|
333
|
+
voxcity_grid, # 3D voxel grid representing the urban environment
|
|
334
|
+
meshsize, # Size of each voxel in meters
|
|
335
|
+
calc_type='instantaneous', # Calculate instantaneous irradiance at specified time
|
|
336
|
+
direct_normal_irradiance_scaling=1.0, # Scaling factor for direct solar radiation (1.0 = no scaling)
|
|
337
|
+
diffuse_irradiance_scaling=1.0, # Scaling factor for diffuse solar radiation (1.0 = no scaling)
|
|
338
|
+
**solar_kwargs # Pass all the parameters defined above
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
# Adjust parameters for cumulative calculation
|
|
342
|
+
solar_kwargs["start_time"] = "01-01 01:00:00" # Start time for cumulative calculation
|
|
343
|
+
solar_kwargs["end_time"] = "01-31 23:00:00" # End time for cumulative calculation
|
|
344
|
+
solar_kwargs["output_file_name"] = 'cummulative_solar_irradiance', # Base filename for outputs (without extension)
|
|
345
|
+
|
|
346
|
+
# Calculate cumulative solar irradiance over the specified time period
|
|
347
|
+
global_map = get_global_solar_irradiance_using_epw(
|
|
348
|
+
voxcity_grid, # 3D voxel grid representing the urban environment
|
|
349
|
+
meshsize, # Size of each voxel in meters
|
|
350
|
+
calc_type='cumulative', # Calculate cumulative irradiance over time period instead of instantaneous
|
|
351
|
+
direct_normal_irradiance_scaling=1.0, # Scaling factor for direct solar radiation (1.0 = no scaling)
|
|
352
|
+
diffuse_irradiance_scaling=1.0, # Scaling factor for diffuse solar radiation (1.0 = no scaling)
|
|
353
|
+
**solar_kwargs # Pass all the parameters defined above
|
|
354
|
+
)
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
<p align="center">
|
|
358
|
+
<img src="https://raw.githubusercontent.com/kunifujiwara/VoxCity/main/images/solar.png" alt="Solar Irradiance Maps Rendered in Rhino" width="800">
|
|
359
|
+
</p>
|
|
360
|
+
<p align="center">
|
|
361
|
+
<em>Example Results Saved as OBJ and Rendered in Rhino</em>
|
|
362
|
+
</p>
|
|
363
|
+
|
|
308
364
|
#### Compute Green View Index (GVI) and Sky View Index (SVI):
|
|
309
365
|
|
|
310
366
|
```python
|
|
@@ -11,24 +11,24 @@ voxcity/download/overture.py,sha256=R6XtC2iP6Xp6e2Otop4FXs97gCW_bAuFQ_RCOPiHbjo,
|
|
|
11
11
|
voxcity/download/utils.py,sha256=z6MdPxM96FWQVqvZW2Eg5pMewVHVysUP7F6ueeCwMfI,1375
|
|
12
12
|
voxcity/file/__init_.py,sha256=cVyNyE6axEpSd3CT5hGuMOAlOyU1p8lVP4jkF1-0Ad8,94
|
|
13
13
|
voxcity/file/envimet.py,sha256=s3qw3kI8sO5996xdnB0MgPCCL0PvICoY1NfrtCz51Sw,24182
|
|
14
|
-
voxcity/file/geojson.py,sha256=
|
|
14
|
+
voxcity/file/geojson.py,sha256=tlkE_strKYTB2xCdZmtAUQjqnTqHYKhpylyi4E8yi0A,25205
|
|
15
15
|
voxcity/file/magicavoxel.py,sha256=Fsv7yGRXeKmp82xcG3rOb0t_HtoqltNq2tHl08xVlqY,7500
|
|
16
16
|
voxcity/file/obj.py,sha256=oW-kPoZj53nfmO9tXP3Wvizq6Kkjh-QQR8UBexRuMiI,21609
|
|
17
17
|
voxcity/geo/__init_.py,sha256=rsj0OMzrTNACccdvEfmf632mb03BRUtKLuecppsxX40,62
|
|
18
|
-
voxcity/geo/draw.py,sha256=
|
|
18
|
+
voxcity/geo/draw.py,sha256=tCWg2kPTbZP3wXyGGywB2Hj4viifaG554VDSjMfFWJg,13728
|
|
19
19
|
voxcity/geo/grid.py,sha256=l9iqi2OCmtJixCc3Y3RthF403pdrx6sB0565wZ1uHgM,40042
|
|
20
20
|
voxcity/geo/utils.py,sha256=sR9InBHxV76XjlGPLD7blg_6EjbM0MG5DOyJffhBjWk,19372
|
|
21
21
|
voxcity/sim/__init_.py,sha256=APdkcdaovj0v_RPOaA4SBvFUKT2RM7Hxuuz3Sux4gCo,65
|
|
22
|
-
voxcity/sim/solar.py,sha256=
|
|
22
|
+
voxcity/sim/solar.py,sha256=VJCWWHxrKSAg-YgajzY1B9bbnBzKybMm7Tw7dBwvoGI,31306
|
|
23
23
|
voxcity/sim/utils.py,sha256=sEYBB2-hLJxTiXQps1_-Fi7t1HN3-1OPOvBCWtgIisA,130
|
|
24
|
-
voxcity/sim/view.py,sha256=
|
|
24
|
+
voxcity/sim/view.py,sha256=xab2B7mKDTufJnE7UN0aPAvkhoGQuduIXZOkJTuS5fU,36682
|
|
25
25
|
voxcity/utils/__init_.py,sha256=xjEadXQ9wXTw0lsx0JTbyTqASWw0GJLfT6eRr0CyQzw,71
|
|
26
26
|
voxcity/utils/lc.py,sha256=RwPd-VY3POV3gTrBhM7TubgGb9MCd3nVah_G8iUEF7k,11562
|
|
27
27
|
voxcity/utils/visualization.py,sha256=GVERj0noHAvJtDT0fV3K6w7pTfuAUfwKez-UMuEakEg,42214
|
|
28
28
|
voxcity/utils/weather.py,sha256=Qwnr0paGdRQstwD0A9q2QfJIV-aQUyxH-6viRwXOuwM,21482
|
|
29
|
-
voxcity-0.3.
|
|
30
|
-
voxcity-0.3.
|
|
31
|
-
voxcity-0.3.
|
|
32
|
-
voxcity-0.3.
|
|
33
|
-
voxcity-0.3.
|
|
34
|
-
voxcity-0.3.
|
|
29
|
+
voxcity-0.3.3.dist-info/AUTHORS.rst,sha256=m82vkI5QokEGdcHof2OxK39lf81w1P58kG9ZNNAKS9U,175
|
|
30
|
+
voxcity-0.3.3.dist-info/LICENSE,sha256=-hGliOFiwUrUSoZiB5WF90xXGqinKyqiDI2t6hrnam8,1087
|
|
31
|
+
voxcity-0.3.3.dist-info/METADATA,sha256=F5jlTMuhl1a3-SfpO60KbdhCZ-Ke85BDA7w1OpGD8jM,23607
|
|
32
|
+
voxcity-0.3.3.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
33
|
+
voxcity-0.3.3.dist-info/top_level.txt,sha256=00b2U-LKfDllt6RL1R33MXie5MvxzUFye0NGD96t_8I,8
|
|
34
|
+
voxcity-0.3.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|