voxcity 0.3.1__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 +293 -69
- voxcity/sim/view.py +184 -32
- {voxcity-0.3.1.dist-info → voxcity-0.3.3.dist-info}/METADATA +57 -1
- {voxcity-0.3.1.dist-info → voxcity-0.3.3.dist-info}/RECORD +10 -10
- {voxcity-0.3.1.dist-info → voxcity-0.3.3.dist-info}/AUTHORS.rst +0 -0
- {voxcity-0.3.1.dist-info → voxcity-0.3.3.dist-info}/LICENSE +0 -0
- {voxcity-0.3.1.dist-info → voxcity-0.3.3.dist-info}/WHEEL +0 -0
- {voxcity-0.3.1.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)
|
|
@@ -303,70 +406,55 @@ def get_solar_positions_astral(times, lat, lon):
|
|
|
303
406
|
def get_cumulative_global_solar_irradiance(
|
|
304
407
|
voxel_data,
|
|
305
408
|
meshsize,
|
|
306
|
-
|
|
307
|
-
end_time,
|
|
409
|
+
df, lat, lon, tz,
|
|
308
410
|
direct_normal_irradiance_scaling=1.0,
|
|
309
411
|
diffuse_irradiance_scaling=1.0,
|
|
310
412
|
**kwargs
|
|
311
413
|
):
|
|
312
414
|
"""
|
|
313
|
-
Compute cumulative global solar irradiance over a specified period using data from an EPW file
|
|
314
|
-
|
|
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
|
|
315
422
|
|
|
316
423
|
Args:
|
|
317
424
|
voxel_data (ndarray): 3D array of voxel values.
|
|
318
425
|
meshsize (float): Size of each voxel in meters.
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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.
|
|
323
432
|
**kwargs: Additional arguments including:
|
|
324
|
-
- view_point_height (float): Observer height in meters
|
|
325
|
-
-
|
|
326
|
-
-
|
|
327
|
-
-
|
|
328
|
-
-
|
|
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
|
|
329
438
|
- show_plot (bool): Whether to show final plot
|
|
330
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
|
|
331
449
|
|
|
332
450
|
Returns:
|
|
333
|
-
ndarray: 2D array of cumulative global solar irradiance (W/m²·hour).
|
|
451
|
+
ndarray: 2D array of cumulative global solar irradiance values (W/m²·hour).
|
|
334
452
|
"""
|
|
335
453
|
view_point_height = kwargs.get("view_point_height", 1.5)
|
|
336
454
|
colormap = kwargs.get("colormap", 'magma')
|
|
455
|
+
start_time = kwargs.get("start_time", "01-01 05:00:00")
|
|
456
|
+
end_time = kwargs.get("end_time", "01-01 20:00:00")
|
|
337
457
|
|
|
338
|
-
# Get EPW file
|
|
339
|
-
download_nearest_epw = kwargs.get("download_nearest_epw", False)
|
|
340
|
-
rectangle_vertices = kwargs.get("rectangle_vertices", None)
|
|
341
|
-
epw_file_path = kwargs.get("epw_file_path", None)
|
|
342
|
-
if download_nearest_epw:
|
|
343
|
-
if rectangle_vertices is None:
|
|
344
|
-
print("rectangle_vertices is required to download nearest EPW file")
|
|
345
|
-
return None
|
|
346
|
-
else:
|
|
347
|
-
# Calculate center point of rectangle
|
|
348
|
-
lats = [coord[0] for coord in rectangle_vertices]
|
|
349
|
-
lons = [coord[1] for coord in rectangle_vertices]
|
|
350
|
-
center_lat = (min(lats) + max(lats)) / 2
|
|
351
|
-
center_lon = (min(lons) + max(lons)) / 2
|
|
352
|
-
target_point = (center_lat, center_lon)
|
|
353
|
-
|
|
354
|
-
# Optional: specify maximum distance in kilometers
|
|
355
|
-
max_distance = 100 # None for no limit
|
|
356
|
-
|
|
357
|
-
output_dir = kwargs.get("output_dir", "output")
|
|
358
|
-
|
|
359
|
-
epw_file_path, weather_data, metadata = get_nearest_epw_from_climate_onebuilding(
|
|
360
|
-
latitude=center_lat,
|
|
361
|
-
longitude=center_lon,
|
|
362
|
-
output_dir=output_dir,
|
|
363
|
-
max_distance=max_distance,
|
|
364
|
-
extract_zip=True,
|
|
365
|
-
load_data=True
|
|
366
|
-
)
|
|
367
|
-
|
|
368
|
-
# Read EPW data
|
|
369
|
-
df, lat, lon, tz, elevation_m = read_epw_for_solar_simulation(epw_file_path)
|
|
370
458
|
if df.empty:
|
|
371
459
|
raise ValueError("No data in EPW file.")
|
|
372
460
|
|
|
@@ -377,20 +465,23 @@ def get_cumulative_global_solar_irradiance(
|
|
|
377
465
|
except ValueError as ve:
|
|
378
466
|
raise ValueError("start_time and end_time must be in format 'MM-DD HH:MM:SS'") from ve
|
|
379
467
|
|
|
380
|
-
# Add hour of year column and filter data
|
|
468
|
+
# Add hour of year column and filter data
|
|
381
469
|
df['hour_of_year'] = (df.index.dayofyear - 1) * 24 + df.index.hour + 1
|
|
382
470
|
|
|
471
|
+
# Convert dates to day of year and hour
|
|
383
472
|
start_doy = datetime(2000, start_dt.month, start_dt.day).timetuple().tm_yday
|
|
384
473
|
end_doy = datetime(2000, end_dt.month, end_dt.day).timetuple().tm_yday
|
|
385
474
|
|
|
386
475
|
start_hour = (start_doy - 1) * 24 + start_dt.hour + 1
|
|
387
476
|
end_hour = (end_doy - 1) * 24 + end_dt.hour + 1
|
|
388
477
|
|
|
478
|
+
# Handle period crossing year boundary
|
|
389
479
|
if start_hour <= end_hour:
|
|
390
480
|
df_period = df[(df['hour_of_year'] >= start_hour) & (df['hour_of_year'] <= end_hour)]
|
|
391
481
|
else:
|
|
392
482
|
df_period = df[(df['hour_of_year'] >= start_hour) | (df['hour_of_year'] <= end_hour)]
|
|
393
483
|
|
|
484
|
+
# Filter by minutes within start/end hours
|
|
394
485
|
df_period = df_period[
|
|
395
486
|
((df_period.index.hour != start_dt.hour) | (df_period.index.minute >= start_dt.minute)) &
|
|
396
487
|
((df_period.index.hour != end_dt.hour) | (df_period.index.minute <= end_dt.minute))
|
|
@@ -399,14 +490,14 @@ def get_cumulative_global_solar_irradiance(
|
|
|
399
490
|
if df_period.empty:
|
|
400
491
|
raise ValueError("No EPW data in the specified period.")
|
|
401
492
|
|
|
402
|
-
#
|
|
493
|
+
# Handle timezone conversion
|
|
403
494
|
offset_minutes = int(tz * 60)
|
|
404
495
|
local_tz = pytz.FixedOffset(offset_minutes)
|
|
405
496
|
df_period_local = df_period.copy()
|
|
406
497
|
df_period_local.index = df_period_local.index.tz_localize(local_tz)
|
|
407
498
|
df_period_utc = df_period_local.tz_convert(pytz.UTC)
|
|
408
499
|
|
|
409
|
-
# Compute solar positions
|
|
500
|
+
# Compute solar positions for period
|
|
410
501
|
solar_positions = get_solar_positions_astral(df_period_utc.index, lat, lon)
|
|
411
502
|
|
|
412
503
|
# Create kwargs for diffuse calculation
|
|
@@ -424,7 +515,7 @@ def get_cumulative_global_solar_irradiance(
|
|
|
424
515
|
**diffuse_kwargs
|
|
425
516
|
)
|
|
426
517
|
|
|
427
|
-
# Initialize maps
|
|
518
|
+
# Initialize accumulation maps
|
|
428
519
|
cumulative_map = np.zeros((voxel_data.shape[0], voxel_data.shape[1]))
|
|
429
520
|
mask_map = np.ones((voxel_data.shape[0], voxel_data.shape[1]), dtype=bool)
|
|
430
521
|
|
|
@@ -436,13 +527,14 @@ def get_cumulative_global_solar_irradiance(
|
|
|
436
527
|
'obj_export': False
|
|
437
528
|
})
|
|
438
529
|
|
|
439
|
-
#
|
|
530
|
+
# Process each timestep
|
|
440
531
|
for idx, (time_utc, row) in enumerate(df_period_utc.iterrows()):
|
|
532
|
+
# Get scaled irradiance values
|
|
441
533
|
DNI = row['DNI'] * direct_normal_irradiance_scaling
|
|
442
534
|
DHI = row['DHI'] * diffuse_irradiance_scaling
|
|
443
535
|
time_local = df_period_local.index[idx]
|
|
444
536
|
|
|
445
|
-
# Get solar position
|
|
537
|
+
# Get solar position for timestep
|
|
446
538
|
solpos = solar_positions.loc[time_utc]
|
|
447
539
|
azimuth_degrees = solpos['azimuth']
|
|
448
540
|
elevation_degrees = solpos['elevation']
|
|
@@ -457,13 +549,13 @@ def get_cumulative_global_solar_irradiance(
|
|
|
457
549
|
**direct_kwargs
|
|
458
550
|
)
|
|
459
551
|
|
|
460
|
-
# Scale
|
|
552
|
+
# Scale base diffuse map by actual DHI
|
|
461
553
|
diffuse_map = base_diffuse_map * DHI
|
|
462
554
|
|
|
463
|
-
# Combine direct and diffuse
|
|
555
|
+
# Combine direct and diffuse components
|
|
464
556
|
global_map = direct_map + diffuse_map
|
|
465
557
|
|
|
466
|
-
# Update
|
|
558
|
+
# Update valid pixel mask
|
|
467
559
|
mask_map &= ~np.isnan(global_map)
|
|
468
560
|
|
|
469
561
|
# Replace NaN with 0 for accumulation
|
|
@@ -484,7 +576,7 @@ def get_cumulative_global_solar_irradiance(
|
|
|
484
576
|
plt.colorbar(label='Global Solar Irradiance (W/m²)')
|
|
485
577
|
plt.show()
|
|
486
578
|
|
|
487
|
-
# Apply mask
|
|
579
|
+
# Apply mask to final result
|
|
488
580
|
cumulative_map[~mask_map] = np.nan
|
|
489
581
|
|
|
490
582
|
# Final visualization
|
|
@@ -526,4 +618,136 @@ def get_cumulative_global_solar_irradiance(
|
|
|
526
618
|
vmax=vmax
|
|
527
619
|
)
|
|
528
620
|
|
|
529
|
-
return cumulative_map
|
|
621
|
+
return cumulative_map
|
|
622
|
+
|
|
623
|
+
def get_global_solar_irradiance_using_epw(
|
|
624
|
+
voxel_data,
|
|
625
|
+
meshsize,
|
|
626
|
+
calc_type='instantaneous',
|
|
627
|
+
direct_normal_irradiance_scaling=1.0,
|
|
628
|
+
diffuse_irradiance_scaling=1.0,
|
|
629
|
+
**kwargs
|
|
630
|
+
):
|
|
631
|
+
"""
|
|
632
|
+
Compute global solar irradiance using EPW weather data, either for a single time or cumulatively over a period.
|
|
633
|
+
|
|
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:
|
|
647
|
+
- download_nearest_epw (bool): Whether to download nearest EPW file
|
|
648
|
+
- epw_file_path (str): Path to EPW file
|
|
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
|
|
661
|
+
|
|
662
|
+
Returns:
|
|
663
|
+
ndarray: 2D array of solar irradiance values (W/m²).
|
|
664
|
+
"""
|
|
665
|
+
view_point_height = kwargs.get("view_point_height", 1.5)
|
|
666
|
+
colormap = kwargs.get("colormap", 'magma')
|
|
667
|
+
|
|
668
|
+
# Get EPW file
|
|
669
|
+
download_nearest_epw = kwargs.get("download_nearest_epw", False)
|
|
670
|
+
rectangle_vertices = kwargs.get("rectangle_vertices", None)
|
|
671
|
+
epw_file_path = kwargs.get("epw_file_path", None)
|
|
672
|
+
if download_nearest_epw:
|
|
673
|
+
if rectangle_vertices is None:
|
|
674
|
+
print("rectangle_vertices is required to download nearest EPW file")
|
|
675
|
+
return None
|
|
676
|
+
else:
|
|
677
|
+
# Calculate center point of rectangle
|
|
678
|
+
lats = [coord[0] for coord in rectangle_vertices]
|
|
679
|
+
lons = [coord[1] for coord in rectangle_vertices]
|
|
680
|
+
center_lat = (min(lats) + max(lats)) / 2
|
|
681
|
+
center_lon = (min(lons) + max(lons)) / 2
|
|
682
|
+
target_point = (center_lat, center_lon)
|
|
683
|
+
|
|
684
|
+
# Optional: specify maximum distance in kilometers
|
|
685
|
+
max_distance = 100 # None for no limit
|
|
686
|
+
|
|
687
|
+
output_dir = kwargs.get("output_dir", "output")
|
|
688
|
+
|
|
689
|
+
epw_file_path, weather_data, metadata = get_nearest_epw_from_climate_onebuilding(
|
|
690
|
+
latitude=center_lat,
|
|
691
|
+
longitude=center_lon,
|
|
692
|
+
output_dir=output_dir,
|
|
693
|
+
max_distance=max_distance,
|
|
694
|
+
extract_zip=True,
|
|
695
|
+
load_data=True
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
# Read EPW data
|
|
699
|
+
df, lat, lon, tz, elevation_m = read_epw_for_solar_simulation(epw_file_path)
|
|
700
|
+
if df.empty:
|
|
701
|
+
raise ValueError("No data in EPW file.")
|
|
702
|
+
|
|
703
|
+
if calc_type == 'instantaneous':
|
|
704
|
+
if df.empty:
|
|
705
|
+
raise ValueError("No data in EPW file.")
|
|
706
|
+
|
|
707
|
+
calc_time = kwargs.get("calc_time", "01-01 12:00:00")
|
|
708
|
+
|
|
709
|
+
# Parse start and end times without year
|
|
710
|
+
try:
|
|
711
|
+
calc_dt = datetime.strptime(calc_time, "%m-%d %H:%M:%S")
|
|
712
|
+
except ValueError as ve:
|
|
713
|
+
raise ValueError("calc_time must be in format 'MM-DD HH:MM:SS'") from ve
|
|
714
|
+
|
|
715
|
+
df_period = df[
|
|
716
|
+
(df.index.month == calc_dt.month) & (df.index.day == calc_dt.day) & (df.index.hour == calc_dt.hour)
|
|
717
|
+
]
|
|
718
|
+
|
|
719
|
+
if df_period.empty:
|
|
720
|
+
raise ValueError("No EPW data at the specified time.")
|
|
721
|
+
|
|
722
|
+
# Prepare timezone conversion
|
|
723
|
+
offset_minutes = int(tz * 60)
|
|
724
|
+
local_tz = pytz.FixedOffset(offset_minutes)
|
|
725
|
+
df_period_local = df_period.copy()
|
|
726
|
+
df_period_local.index = df_period_local.index.tz_localize(local_tz)
|
|
727
|
+
df_period_utc = df_period_local.tz_convert(pytz.UTC)
|
|
728
|
+
|
|
729
|
+
# Compute solar positions
|
|
730
|
+
solar_positions = get_solar_positions_astral(df_period_utc.index, lat, lon)
|
|
731
|
+
direct_normal_irradiance = df_period_utc.iloc[0]['DNI']
|
|
732
|
+
diffuse_irradiance = df_period_utc.iloc[0]['DHI']
|
|
733
|
+
azimuth_degrees = solar_positions.iloc[0]['azimuth']
|
|
734
|
+
elevation_degrees = solar_positions.iloc[0]['elevation']
|
|
735
|
+
solar_map = get_global_solar_irradiance_map(
|
|
736
|
+
voxel_data, # 3D voxel grid representing the urban environment
|
|
737
|
+
meshsize, # Size of each grid cell in meters
|
|
738
|
+
azimuth_degrees, # Sun's azimuth angle
|
|
739
|
+
elevation_degrees, # Sun's elevation angle
|
|
740
|
+
direct_normal_irradiance, # Direct Normal Irradiance value
|
|
741
|
+
diffuse_irradiance, # Diffuse irradiance value
|
|
742
|
+
show_plot=True, # Display visualization of results
|
|
743
|
+
**kwargs
|
|
744
|
+
)
|
|
745
|
+
if calc_type == 'cumulative':
|
|
746
|
+
solar_map = get_cumulative_global_solar_irradiance(
|
|
747
|
+
voxel_data,
|
|
748
|
+
meshsize,
|
|
749
|
+
df, lat, lon, tz,
|
|
750
|
+
**kwargs
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
return solar_map
|
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):
|
|
192
|
-
|
|
300
|
+
# Skip invalid ground types
|
|
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
|