voxcity 0.5.13__py3-none-any.whl → 0.5.15__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/downloader/citygml.py +202 -28
- voxcity/downloader/eubucco.py +91 -14
- voxcity/downloader/gee.py +164 -22
- voxcity/downloader/mbfp.py +55 -9
- voxcity/downloader/oemj.py +110 -24
- voxcity/downloader/omt.py +74 -7
- voxcity/downloader/osm.py +109 -23
- voxcity/downloader/overture.py +108 -23
- voxcity/downloader/utils.py +37 -7
- voxcity/exporter/envimet.py +180 -61
- voxcity/exporter/magicavoxel.py +138 -28
- voxcity/exporter/obj.py +159 -36
- voxcity/generator.py +159 -76
- voxcity/geoprocessor/draw.py +180 -27
- voxcity/geoprocessor/grid.py +178 -38
- voxcity/geoprocessor/mesh.py +347 -43
- voxcity/geoprocessor/network.py +196 -63
- voxcity/geoprocessor/polygon.py +365 -88
- voxcity/geoprocessor/utils.py +283 -72
- voxcity/simulator/solar.py +596 -201
- voxcity/simulator/view.py +278 -723
- voxcity/utils/lc.py +183 -0
- voxcity/utils/material.py +99 -32
- voxcity/utils/visualization.py +2578 -1988
- voxcity/utils/weather.py +816 -615
- {voxcity-0.5.13.dist-info → voxcity-0.5.15.dist-info}/METADATA +10 -12
- voxcity-0.5.15.dist-info/RECORD +38 -0
- {voxcity-0.5.13.dist-info → voxcity-0.5.15.dist-info}/WHEEL +1 -1
- voxcity-0.5.13.dist-info/RECORD +0 -38
- {voxcity-0.5.13.dist-info → voxcity-0.5.15.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-0.5.13.dist-info → voxcity-0.5.15.dist-info}/licenses/LICENSE +0 -0
- {voxcity-0.5.13.dist-info → voxcity-0.5.15.dist-info}/top_level.txt +0 -0
voxcity/simulator/solar.py
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Solar Irradiance Simulation Module
|
|
3
|
+
|
|
4
|
+
This module provides comprehensive solar irradiance calculations for urban environments,
|
|
5
|
+
including direct and diffuse solar radiation analysis with consideration for tree transmittance
|
|
6
|
+
and building shadows. It supports both instantaneous and cumulative irradiance calculations
|
|
7
|
+
using weather data from EPW files.
|
|
8
|
+
|
|
9
|
+
Key Features:
|
|
10
|
+
- Direct solar irradiance with ray tracing and shadow analysis
|
|
11
|
+
- Diffuse solar irradiance using Sky View Factor (SVF)
|
|
12
|
+
- Tree transmittance modeling using Beer-Lambert law
|
|
13
|
+
- Building surface irradiance calculation with 3D mesh support
|
|
14
|
+
- Weather data integration from EPW files
|
|
15
|
+
- Visualization and export capabilities
|
|
16
|
+
|
|
17
|
+
The module uses numba for performance optimization in computationally intensive calculations.
|
|
18
|
+
"""
|
|
19
|
+
|
|
1
20
|
import numpy as np
|
|
2
21
|
import pandas as pd
|
|
3
22
|
import matplotlib.pyplot as plt
|
|
@@ -7,6 +26,7 @@ import pytz
|
|
|
7
26
|
from astral import Observer
|
|
8
27
|
from astral.sun import elevation, azimuth
|
|
9
28
|
|
|
29
|
+
# Import custom modules for view analysis and weather data processing
|
|
10
30
|
from .view import trace_ray_generic, compute_vi_map_generic, get_sky_view_factor_map, get_surface_view_factor
|
|
11
31
|
from ..utils.weather import get_nearest_epw_from_climate_onebuilding, read_epw_for_solar_simulation
|
|
12
32
|
from ..exporter.obj import grid_to_obj, export_obj
|
|
@@ -16,63 +36,107 @@ def compute_direct_solar_irradiance_map_binary(voxel_data, sun_direction, view_p
|
|
|
16
36
|
"""
|
|
17
37
|
Compute a map of direct solar irradiation accounting for tree transmittance.
|
|
18
38
|
|
|
39
|
+
This function performs ray tracing from observer positions on the ground surface
|
|
40
|
+
towards the sun to determine direct solar irradiance at each location. It accounts
|
|
41
|
+
for shadows cast by buildings and vegetation, with special consideration for
|
|
42
|
+
tree transmittance using the Beer-Lambert law.
|
|
43
|
+
|
|
19
44
|
The function:
|
|
20
45
|
1. Places observers at valid locations (empty voxels above ground)
|
|
21
46
|
2. Casts rays from each observer in the sun direction
|
|
22
47
|
3. Computes transmittance through trees using Beer-Lambert law
|
|
23
48
|
4. Returns a 2D map of transmittance values
|
|
24
49
|
|
|
50
|
+
Observer Placement Rules:
|
|
51
|
+
- Observers are placed in empty voxels (value 0 or -2 for trees) above solid ground
|
|
52
|
+
- Observers are NOT placed on buildings, vegetation, or water surfaces
|
|
53
|
+
- Observer height is added above the detected ground surface
|
|
54
|
+
|
|
55
|
+
Ray Tracing Process:
|
|
56
|
+
- Rays are cast from each valid observer position toward the sun
|
|
57
|
+
- Intersections with obstacles (non-sky voxels) are detected
|
|
58
|
+
- Tree voxels provide partial transmittance rather than complete blocking
|
|
59
|
+
- Final transmittance value represents solar energy reaching the surface
|
|
60
|
+
|
|
25
61
|
Args:
|
|
26
|
-
voxel_data (ndarray): 3D array of voxel values.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
62
|
+
voxel_data (ndarray): 3D array of voxel values representing the urban environment.
|
|
63
|
+
Common values: 0=sky, 1-6=buildings, 7-9=special surfaces, -2=trees
|
|
64
|
+
sun_direction (tuple): Direction vector of the sun (dx, dy, dz), should be normalized
|
|
65
|
+
view_point_height (float): Observer height above ground surface in meters
|
|
66
|
+
hit_values (tuple): Values considered non-obstacles if inclusion_mode=False
|
|
67
|
+
Typically (0,) meaning only sky voxels are transparent
|
|
68
|
+
meshsize (float): Size of each voxel in meters (spatial resolution)
|
|
69
|
+
tree_k (float): Tree extinction coefficient for Beer-Lambert law (higher = more opaque)
|
|
70
|
+
tree_lad (float): Leaf area density in m^-1 (affects light attenuation through trees)
|
|
71
|
+
inclusion_mode (bool): False here, meaning any voxel not in hit_values is an obstacle
|
|
34
72
|
|
|
35
73
|
Returns:
|
|
36
|
-
ndarray: 2D array of transmittance values (0.0-1.0)
|
|
74
|
+
ndarray: 2D array of transmittance values (0.0-1.0)
|
|
75
|
+
- 1.0 = full sun exposure
|
|
76
|
+
- 0.0 = complete shadow
|
|
77
|
+
- 0.0-1.0 = partial transmittance through trees
|
|
78
|
+
- NaN = invalid observer position (cannot place observer)
|
|
79
|
+
|
|
80
|
+
Note:
|
|
81
|
+
The returned map is vertically flipped to match standard visualization conventions
|
|
82
|
+
where the origin is at the bottom-left corner.
|
|
37
83
|
"""
|
|
38
84
|
|
|
85
|
+
# Convert observer height from meters to voxel units
|
|
39
86
|
view_height_voxel = int(view_point_height / meshsize)
|
|
40
87
|
|
|
88
|
+
# Get dimensions of the voxel grid
|
|
41
89
|
nx, ny, nz = voxel_data.shape
|
|
90
|
+
|
|
91
|
+
# Initialize irradiance map with NaN (invalid positions)
|
|
42
92
|
irradiance_map = np.full((nx, ny), np.nan, dtype=np.float64)
|
|
43
93
|
|
|
44
|
-
# Normalize sun direction vector for ray tracing
|
|
94
|
+
# Normalize sun direction vector for consistent ray tracing
|
|
95
|
+
# This ensures rays travel at unit speed through the voxel grid
|
|
45
96
|
sd = np.array(sun_direction, dtype=np.float64)
|
|
46
97
|
sd_len = np.sqrt(sd[0]**2 + sd[1]**2 + sd[2]**2)
|
|
47
98
|
if sd_len == 0.0:
|
|
48
99
|
return np.flipud(irradiance_map)
|
|
49
100
|
sd /= sd_len
|
|
50
101
|
|
|
51
|
-
# Process each x,y position in parallel
|
|
102
|
+
# Process each x,y position in parallel for performance
|
|
103
|
+
# This is the main computational loop optimized with numba
|
|
52
104
|
for x in prange(nx):
|
|
53
105
|
for y in range(ny):
|
|
54
106
|
found_observer = False
|
|
55
|
-
|
|
107
|
+
|
|
108
|
+
# Search upward through the vertical column to find valid observer position
|
|
109
|
+
# Start from z=1 to ensure we can check the voxel below
|
|
56
110
|
for z in range(1, nz):
|
|
111
|
+
|
|
57
112
|
# Check if current voxel is empty/tree and voxel below is solid
|
|
113
|
+
# This identifies the ground surface where observers can be placed
|
|
58
114
|
if voxel_data[x, y, z] in (0, -2) and voxel_data[x, y, z - 1] not in (0, -2):
|
|
59
|
-
|
|
115
|
+
|
|
116
|
+
# Skip if standing on building/vegetation/water surfaces
|
|
117
|
+
# These are considered invalid observer locations
|
|
60
118
|
if (voxel_data[x, y, z - 1] in (7, 8, 9)) or (voxel_data[x, y, z - 1] < 0):
|
|
61
119
|
irradiance_map[x, y] = np.nan
|
|
62
120
|
found_observer = True
|
|
63
121
|
break
|
|
64
122
|
else:
|
|
65
|
-
# Place observer and cast
|
|
123
|
+
# Place observer at valid ground location and cast ray toward sun
|
|
66
124
|
observer_location = np.array([x, y, z + view_height_voxel], dtype=np.float64)
|
|
125
|
+
|
|
126
|
+
# Trace ray from observer to sun, accounting for obstacles and tree transmittance
|
|
67
127
|
hit, transmittance = trace_ray_generic(voxel_data, observer_location, sd,
|
|
68
128
|
hit_values, meshsize, tree_k, tree_lad, inclusion_mode)
|
|
129
|
+
|
|
130
|
+
# Store transmittance value (0 if hit solid obstacle, 0-1 if through trees)
|
|
69
131
|
irradiance_map[x, y] = transmittance if not hit else 0.0
|
|
70
132
|
found_observer = True
|
|
71
133
|
break
|
|
134
|
+
|
|
135
|
+
# If no valid observer position found in this column, mark as invalid
|
|
72
136
|
if not found_observer:
|
|
73
137
|
irradiance_map[x, y] = np.nan
|
|
74
138
|
|
|
75
|
-
# Flip map vertically to match visualization conventions
|
|
139
|
+
# Flip map vertically to match visualization conventions (origin at bottom-left)
|
|
76
140
|
return np.flipud(irradiance_map)
|
|
77
141
|
|
|
78
142
|
def get_direct_solar_irradiance_map(voxel_data, meshsize, azimuth_degrees_ori, elevation_degrees,
|
|
@@ -80,73 +144,105 @@ def get_direct_solar_irradiance_map(voxel_data, meshsize, azimuth_degrees_ori, e
|
|
|
80
144
|
"""
|
|
81
145
|
Compute direct solar irradiance map with tree transmittance.
|
|
82
146
|
|
|
147
|
+
This function converts solar angles to a 3D direction vector, computes the binary
|
|
148
|
+
transmittance map using ray tracing, and scales the results by actual solar irradiance
|
|
149
|
+
values to produce physically meaningful irradiance measurements.
|
|
150
|
+
|
|
151
|
+
Solar Geometry:
|
|
152
|
+
- Azimuth: Horizontal angle measured from North (0°) clockwise to East (90°)
|
|
153
|
+
- Elevation: Vertical angle above the horizon (0° = horizon, 90° = zenith)
|
|
154
|
+
- The coordinate system is adjusted by 180° to match the voxel grid orientation
|
|
155
|
+
|
|
156
|
+
Physics Background:
|
|
157
|
+
- Direct Normal Irradiance (DNI): Solar energy on a surface perpendicular to sun rays
|
|
158
|
+
- Horizontal irradiance: DNI scaled by sine of elevation angle
|
|
159
|
+
- Tree transmittance: Applied using Beer-Lambert law for realistic light attenuation
|
|
160
|
+
|
|
83
161
|
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
|
|
162
|
+
1. Converts sun angles to direction vector using spherical coordinates
|
|
163
|
+
2. Computes binary transmittance map accounting for shadows and tree effects
|
|
164
|
+
3. Scales by direct normal irradiance and sun elevation for horizontal surfaces
|
|
165
|
+
4. Optionally visualizes and exports results in various formats
|
|
88
166
|
|
|
89
167
|
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
|
|
168
|
+
voxel_data (ndarray): 3D array of voxel values representing the urban environment
|
|
169
|
+
meshsize (float): Size of each voxel in meters (spatial resolution)
|
|
170
|
+
azimuth_degrees_ori (float): Sun azimuth angle in degrees (0° = North, 90° = East)
|
|
171
|
+
elevation_degrees (float): Sun elevation angle in degrees above horizon (0-90°)
|
|
172
|
+
direct_normal_irradiance (float): Direct normal irradiance in W/m² (from weather data)
|
|
173
|
+
show_plot (bool): Whether to display visualization of results
|
|
96
174
|
**kwargs: Additional arguments including:
|
|
97
175
|
- view_point_height (float): Observer height in meters (default: 1.5)
|
|
98
|
-
|
|
99
|
-
-
|
|
100
|
-
-
|
|
176
|
+
Height above ground where irradiance is measured
|
|
177
|
+
- colormap (str): Matplotlib colormap name for visualization (default: 'magma')
|
|
178
|
+
- vmin (float): Minimum value for colormap scaling
|
|
179
|
+
- vmax (float): Maximum value for colormap scaling
|
|
101
180
|
- tree_k (float): Tree extinction coefficient (default: 0.6)
|
|
181
|
+
Higher values mean trees block more light
|
|
102
182
|
- tree_lad (float): Leaf area density in m^-1 (default: 1.0)
|
|
103
|
-
|
|
104
|
-
-
|
|
105
|
-
-
|
|
106
|
-
-
|
|
107
|
-
-
|
|
108
|
-
-
|
|
183
|
+
Affects light attenuation through tree canopies
|
|
184
|
+
- obj_export (bool): Whether to export results as 3D OBJ file
|
|
185
|
+
- output_directory (str): Directory for file exports
|
|
186
|
+
- output_file_name (str): Base filename for exports
|
|
187
|
+
- dem_grid (ndarray): Digital elevation model for 3D export
|
|
188
|
+
- num_colors (int): Number of discrete colors for OBJ export
|
|
189
|
+
- alpha (float): Transparency value for 3D visualization
|
|
109
190
|
|
|
110
191
|
Returns:
|
|
111
|
-
ndarray: 2D array of direct solar irradiance values
|
|
192
|
+
ndarray: 2D array of direct solar irradiance values in W/m²
|
|
193
|
+
- Values represent energy flux on horizontal surfaces
|
|
194
|
+
- NaN indicates invalid measurement locations
|
|
195
|
+
- Range typically 0 to direct_normal_irradiance * sin(elevation)
|
|
196
|
+
|
|
197
|
+
Note:
|
|
198
|
+
The azimuth is internally adjusted by 180° to match the coordinate system
|
|
199
|
+
where the voxel grid's y-axis points in the opposite direction from geographic north.
|
|
112
200
|
"""
|
|
201
|
+
# Extract parameters with defaults for observer and visualization settings
|
|
113
202
|
view_point_height = kwargs.get("view_point_height", 1.5)
|
|
114
203
|
colormap = kwargs.get("colormap", 'magma')
|
|
115
204
|
vmin = kwargs.get("vmin", 0.0)
|
|
116
205
|
vmax = kwargs.get("vmax", direct_normal_irradiance)
|
|
117
206
|
|
|
118
|
-
# Get tree transmittance parameters
|
|
207
|
+
# Get tree transmittance parameters for Beer-Lambert law calculations
|
|
119
208
|
tree_k = kwargs.get("tree_k", 0.6)
|
|
120
209
|
tree_lad = kwargs.get("tree_lad", 1.0)
|
|
121
210
|
|
|
122
|
-
# Convert sun angles to direction vector
|
|
123
|
-
# Note: azimuth is adjusted by 180° to match coordinate system
|
|
211
|
+
# Convert sun angles to 3D direction vector using spherical coordinates
|
|
212
|
+
# Note: azimuth is adjusted by 180° to match coordinate system orientation
|
|
124
213
|
azimuth_degrees = 180 - azimuth_degrees_ori
|
|
125
214
|
azimuth_radians = np.deg2rad(azimuth_degrees)
|
|
126
215
|
elevation_radians = np.deg2rad(elevation_degrees)
|
|
216
|
+
|
|
217
|
+
# Calculate direction vector components
|
|
218
|
+
# dx, dy: horizontal components, dz: vertical component (upward positive)
|
|
127
219
|
dx = np.cos(elevation_radians) * np.cos(azimuth_radians)
|
|
128
220
|
dy = np.cos(elevation_radians) * np.sin(azimuth_radians)
|
|
129
221
|
dz = np.sin(elevation_radians)
|
|
130
222
|
sun_direction = (dx, dy, dz)
|
|
131
223
|
|
|
224
|
+
# Define obstacle detection parameters for ray tracing
|
|
132
225
|
# All non-zero voxels are obstacles except for trees which have transmittance
|
|
133
|
-
hit_values = (0,)
|
|
134
|
-
inclusion_mode = False
|
|
226
|
+
hit_values = (0,) # Only sky voxels (value 0) are transparent
|
|
227
|
+
inclusion_mode = False # Values NOT in hit_values are considered obstacles
|
|
135
228
|
|
|
136
|
-
# Compute transmittance map
|
|
229
|
+
# Compute transmittance map using optimized ray tracing
|
|
137
230
|
transmittance_map = compute_direct_solar_irradiance_map_binary(
|
|
138
231
|
voxel_data, sun_direction, view_point_height, hit_values,
|
|
139
232
|
meshsize, tree_k, tree_lad, inclusion_mode
|
|
140
233
|
)
|
|
141
234
|
|
|
142
|
-
# Scale by
|
|
235
|
+
# Scale transmittance by solar irradiance and geometry
|
|
236
|
+
# For horizontal surfaces: multiply by sine of elevation angle
|
|
143
237
|
sin_elev = dz
|
|
144
238
|
direct_map = transmittance_map * direct_normal_irradiance * sin_elev
|
|
145
239
|
|
|
146
|
-
# Optional visualization
|
|
240
|
+
# Optional visualization of results
|
|
147
241
|
if show_plot:
|
|
242
|
+
# Set up colormap with special handling for invalid data
|
|
148
243
|
cmap = plt.cm.get_cmap(colormap).copy()
|
|
149
|
-
cmap.set_bad(color='lightgray')
|
|
244
|
+
cmap.set_bad(color='lightgray') # NaN values shown in gray
|
|
245
|
+
|
|
150
246
|
plt.figure(figsize=(10, 8))
|
|
151
247
|
# plt.title("Horizontal Direct Solar Irradiance Map (0° = North)")
|
|
152
248
|
plt.imshow(direct_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
|
|
@@ -154,14 +250,17 @@ def get_direct_solar_irradiance_map(voxel_data, meshsize, azimuth_degrees_ori, e
|
|
|
154
250
|
plt.axis('off')
|
|
155
251
|
plt.show()
|
|
156
252
|
|
|
157
|
-
# Optional OBJ
|
|
253
|
+
# Optional export to 3D OBJ format for external visualization
|
|
158
254
|
obj_export = kwargs.get("obj_export", False)
|
|
159
255
|
if obj_export:
|
|
256
|
+
# Get export parameters with defaults
|
|
160
257
|
dem_grid = kwargs.get("dem_grid", np.zeros_like(direct_map))
|
|
161
258
|
output_dir = kwargs.get("output_directory", "output")
|
|
162
259
|
output_file_name = kwargs.get("output_file_name", "direct_solar_irradiance")
|
|
163
260
|
num_colors = kwargs.get("num_colors", 10)
|
|
164
261
|
alpha = kwargs.get("alpha", 1.0)
|
|
262
|
+
|
|
263
|
+
# Export as colored 3D mesh
|
|
165
264
|
grid_to_obj(
|
|
166
265
|
direct_map,
|
|
167
266
|
dem_grid,
|
|
@@ -182,55 +281,93 @@ def get_diffuse_solar_irradiance_map(voxel_data, meshsize, diffuse_irradiance=1.
|
|
|
182
281
|
"""
|
|
183
282
|
Compute diffuse solar irradiance map using the Sky View Factor (SVF) with tree transmittance.
|
|
184
283
|
|
|
284
|
+
This function calculates the diffuse component of solar radiation, which consists of
|
|
285
|
+
sunlight scattered by the atmosphere and reaches surfaces from all directions across
|
|
286
|
+
the sky hemisphere. The calculation is based on the Sky View Factor (SVF), which
|
|
287
|
+
quantifies how much of the sky dome is visible from each location.
|
|
288
|
+
|
|
289
|
+
Physics Background:
|
|
290
|
+
- Diffuse radiation: Solar energy scattered by atmospheric particles and clouds
|
|
291
|
+
- Sky View Factor (SVF): Fraction of sky hemisphere visible from a point (0.0 to 1.0)
|
|
292
|
+
- Isotropic sky model: Assumes uniform diffuse radiation distribution across the sky
|
|
293
|
+
- Tree effects: Partial transmittance through canopies reduces effective sky visibility
|
|
294
|
+
|
|
295
|
+
SVF Characteristics:
|
|
296
|
+
- SVF = 1.0: Completely open sky (maximum diffuse radiation)
|
|
297
|
+
- SVF = 0.0: Completely blocked sky (no diffuse radiation)
|
|
298
|
+
- SVF = 0.5: Half of sky visible (typical for urban canyons)
|
|
299
|
+
- Trees reduce SVF through partial light attenuation rather than complete blocking
|
|
300
|
+
|
|
185
301
|
The function:
|
|
186
|
-
1. Computes SVF map accounting for tree transmittance
|
|
187
|
-
2. Scales SVF by diffuse horizontal irradiance
|
|
188
|
-
3. Optionally visualizes and exports results
|
|
302
|
+
1. Computes SVF map accounting for building shadows and tree transmittance
|
|
303
|
+
2. Scales SVF by diffuse horizontal irradiance from weather data
|
|
304
|
+
3. Optionally visualizes and exports results for analysis
|
|
189
305
|
|
|
190
306
|
Args:
|
|
191
|
-
voxel_data (ndarray): 3D array of voxel values
|
|
192
|
-
meshsize (float): Size of each voxel in meters
|
|
193
|
-
diffuse_irradiance (float): Diffuse horizontal irradiance in W/m
|
|
194
|
-
|
|
307
|
+
voxel_data (ndarray): 3D array of voxel values representing the urban environment
|
|
308
|
+
meshsize (float): Size of each voxel in meters (spatial resolution)
|
|
309
|
+
diffuse_irradiance (float): Diffuse horizontal irradiance in W/m² (from weather data)
|
|
310
|
+
Default 1.0 for normalized calculations
|
|
311
|
+
show_plot (bool): Whether to display visualization of results
|
|
195
312
|
**kwargs: Additional arguments including:
|
|
196
313
|
- view_point_height (float): Observer height in meters (default: 1.5)
|
|
197
|
-
|
|
198
|
-
-
|
|
199
|
-
-
|
|
200
|
-
-
|
|
314
|
+
Height above ground where measurements are taken
|
|
315
|
+
- colormap (str): Matplotlib colormap name for visualization (default: 'magma')
|
|
316
|
+
- vmin (float): Minimum value for colormap scaling
|
|
317
|
+
- vmax (float): Maximum value for colormap scaling
|
|
318
|
+
- tree_k (float): Tree extinction coefficient for transmittance calculations
|
|
319
|
+
Higher values mean trees block more diffuse light
|
|
201
320
|
- tree_lad (float): Leaf area density in m^-1
|
|
202
|
-
|
|
203
|
-
-
|
|
204
|
-
-
|
|
205
|
-
-
|
|
206
|
-
-
|
|
207
|
-
-
|
|
321
|
+
Affects light attenuation through tree canopies
|
|
322
|
+
- obj_export (bool): Whether to export results as 3D OBJ file
|
|
323
|
+
- output_directory (str): Directory for file exports
|
|
324
|
+
- output_file_name (str): Base filename for exports
|
|
325
|
+
- dem_grid (ndarray): Digital elevation model for 3D export
|
|
326
|
+
- num_colors (int): Number of discrete colors for OBJ export
|
|
327
|
+
- alpha (float): Transparency value for 3D visualization
|
|
208
328
|
|
|
209
329
|
Returns:
|
|
210
|
-
ndarray: 2D array of diffuse solar irradiance values
|
|
330
|
+
ndarray: 2D array of diffuse solar irradiance values in W/m²
|
|
331
|
+
- Values represent diffuse energy flux on horizontal surfaces
|
|
332
|
+
- Range: 0.0 to diffuse_irradiance (input parameter)
|
|
333
|
+
- NaN indicates invalid measurement locations
|
|
334
|
+
|
|
335
|
+
Note:
|
|
336
|
+
The SVF calculation internally handles tree transmittance effects, so trees
|
|
337
|
+
contribute partial sky visibility rather than complete obstruction.
|
|
211
338
|
"""
|
|
212
339
|
|
|
340
|
+
# Extract parameters with defaults for observer and visualization settings
|
|
213
341
|
view_point_height = kwargs.get("view_point_height", 1.5)
|
|
214
342
|
colormap = kwargs.get("colormap", 'magma')
|
|
215
343
|
vmin = kwargs.get("vmin", 0.0)
|
|
216
344
|
vmax = kwargs.get("vmax", diffuse_irradiance)
|
|
217
345
|
|
|
346
|
+
# Prepare parameters for SVF calculation with appropriate visualization settings
|
|
218
347
|
# Pass tree transmittance parameters to SVF calculation
|
|
219
348
|
svf_kwargs = kwargs.copy()
|
|
220
|
-
svf_kwargs["colormap"] = "BuPu_r"
|
|
221
|
-
svf_kwargs["vmin"] = 0
|
|
349
|
+
svf_kwargs["colormap"] = "BuPu_r" # Purple colormap for SVF visualization
|
|
350
|
+
svf_kwargs["vmin"] = 0 # SVF ranges from 0 to 1
|
|
222
351
|
svf_kwargs["vmax"] = 1
|
|
223
352
|
|
|
353
|
+
# Calculate Sky View Factor map accounting for all obstructions
|
|
224
354
|
# SVF calculation now handles tree transmittance internally
|
|
225
355
|
SVF_map = get_sky_view_factor_map(voxel_data, meshsize, **svf_kwargs)
|
|
356
|
+
|
|
357
|
+
# Convert SVF to diffuse irradiance by scaling with weather data
|
|
358
|
+
# Each location receives diffuse radiation proportional to its sky visibility
|
|
226
359
|
diffuse_map = SVF_map * diffuse_irradiance
|
|
227
360
|
|
|
228
|
-
# Optional visualization
|
|
361
|
+
# Optional visualization of diffuse irradiance results
|
|
229
362
|
if show_plot:
|
|
363
|
+
# Use parameters from kwargs for consistent visualization
|
|
230
364
|
vmin = kwargs.get("vmin", 0.0)
|
|
231
365
|
vmax = kwargs.get("vmax", diffuse_irradiance)
|
|
366
|
+
|
|
367
|
+
# Set up colormap with special handling for invalid data
|
|
232
368
|
cmap = plt.cm.get_cmap(colormap).copy()
|
|
233
|
-
cmap.set_bad(color='lightgray')
|
|
369
|
+
cmap.set_bad(color='lightgray') # NaN values shown in gray
|
|
370
|
+
|
|
234
371
|
plt.figure(figsize=(10, 8))
|
|
235
372
|
# plt.title("Diffuse Solar Irradiance Map")
|
|
236
373
|
plt.imshow(diffuse_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
|
|
@@ -238,14 +375,17 @@ def get_diffuse_solar_irradiance_map(voxel_data, meshsize, diffuse_irradiance=1.
|
|
|
238
375
|
plt.axis('off')
|
|
239
376
|
plt.show()
|
|
240
377
|
|
|
241
|
-
# Optional OBJ
|
|
378
|
+
# Optional export to 3D OBJ format for external visualization
|
|
242
379
|
obj_export = kwargs.get("obj_export", False)
|
|
243
380
|
if obj_export:
|
|
381
|
+
# Get export parameters with defaults
|
|
244
382
|
dem_grid = kwargs.get("dem_grid", np.zeros_like(diffuse_map))
|
|
245
383
|
output_dir = kwargs.get("output_directory", "output")
|
|
246
384
|
output_file_name = kwargs.get("output_file_name", "diffuse_solar_irradiance")
|
|
247
385
|
num_colors = kwargs.get("num_colors", 10)
|
|
248
386
|
alpha = kwargs.get("alpha", 1.0)
|
|
387
|
+
|
|
388
|
+
# Export as colored 3D mesh
|
|
249
389
|
grid_to_obj(
|
|
250
390
|
diffuse_map,
|
|
251
391
|
dem_grid,
|
|
@@ -276,47 +416,76 @@ def get_global_solar_irradiance_map(
|
|
|
276
416
|
"""
|
|
277
417
|
Compute global solar irradiance (direct + diffuse) on a horizontal plane at each valid observer location.
|
|
278
418
|
|
|
419
|
+
This function combines both direct and diffuse components of solar radiation to calculate
|
|
420
|
+
the total solar irradiance at each location. Global horizontal irradiance (GHI) is the
|
|
421
|
+
most commonly used metric for solar energy assessment and represents the total solar
|
|
422
|
+
energy available on a horizontal surface.
|
|
423
|
+
|
|
424
|
+
Global Irradiance Components:
|
|
425
|
+
- Direct component: Solar radiation from the sun's disk, affected by shadows and obstacles
|
|
426
|
+
- Diffuse component: Solar radiation scattered by the atmosphere, affected by sky view
|
|
427
|
+
- Total irradiance: Sum of direct and diffuse components at each location
|
|
428
|
+
|
|
429
|
+
Physical Considerations:
|
|
430
|
+
- Direct radiation varies with sun position and local obstructions
|
|
431
|
+
- Diffuse radiation varies with sky visibility (Sky View Factor)
|
|
432
|
+
- Both components are affected by tree transmittance using Beer-Lambert law
|
|
433
|
+
- Invalid locations (e.g., on water, buildings) are marked as NaN
|
|
434
|
+
|
|
279
435
|
The function:
|
|
280
|
-
1. Computes direct solar irradiance map
|
|
281
|
-
2. Computes diffuse solar irradiance map
|
|
282
|
-
3. Combines maps and optionally visualizes/exports results
|
|
436
|
+
1. Computes direct solar irradiance map accounting for sun position and shadows
|
|
437
|
+
2. Computes diffuse solar irradiance map based on Sky View Factor
|
|
438
|
+
3. Combines maps and optionally visualizes/exports results for analysis
|
|
283
439
|
|
|
284
440
|
Args:
|
|
285
|
-
voxel_data (ndarray): 3D voxel array
|
|
286
|
-
meshsize (float): Voxel size in meters
|
|
287
|
-
azimuth_degrees (float): Sun azimuth angle in degrees (0° = North, 90° = East)
|
|
288
|
-
elevation_degrees (float): Sun elevation angle in degrees above horizon
|
|
289
|
-
direct_normal_irradiance (float): Direct normal irradiance in W/m
|
|
290
|
-
diffuse_irradiance (float): Diffuse horizontal irradiance in W/m
|
|
291
|
-
show_plot (bool): Whether to display visualization
|
|
441
|
+
voxel_data (ndarray): 3D voxel array representing the urban environment
|
|
442
|
+
meshsize (float): Voxel size in meters (spatial resolution)
|
|
443
|
+
azimuth_degrees (float): Sun azimuth angle in degrees (0° = North, 90° = East)
|
|
444
|
+
elevation_degrees (float): Sun elevation angle in degrees above horizon (0-90°)
|
|
445
|
+
direct_normal_irradiance (float): Direct normal irradiance in W/m² (from weather data)
|
|
446
|
+
diffuse_irradiance (float): Diffuse horizontal irradiance in W/m² (from weather data)
|
|
447
|
+
show_plot (bool): Whether to display visualization of results
|
|
292
448
|
**kwargs: Additional arguments including:
|
|
293
449
|
- view_point_height (float): Observer height in meters (default: 1.5)
|
|
294
|
-
|
|
295
|
-
-
|
|
296
|
-
-
|
|
297
|
-
-
|
|
450
|
+
Height above ground where measurements are taken
|
|
451
|
+
- colormap (str): Matplotlib colormap name for visualization (default: 'magma')
|
|
452
|
+
- vmin (float): Minimum value for colormap scaling
|
|
453
|
+
- vmax (float): Maximum value for colormap scaling
|
|
454
|
+
- tree_k (float): Tree extinction coefficient for transmittance calculations
|
|
455
|
+
Higher values mean trees block more light
|
|
298
456
|
- tree_lad (float): Leaf area density in m^-1
|
|
299
|
-
|
|
300
|
-
-
|
|
301
|
-
-
|
|
302
|
-
-
|
|
303
|
-
-
|
|
304
|
-
-
|
|
457
|
+
Affects light attenuation through tree canopies
|
|
458
|
+
- obj_export (bool): Whether to export results as 3D OBJ file
|
|
459
|
+
- output_directory (str): Directory for file exports
|
|
460
|
+
- output_file_name (str): Base filename for exports
|
|
461
|
+
- dem_grid (ndarray): Digital elevation model for 3D export
|
|
462
|
+
- num_colors (int): Number of discrete colors for OBJ export
|
|
463
|
+
- alpha (float): Transparency value for 3D visualization
|
|
305
464
|
|
|
306
465
|
Returns:
|
|
307
|
-
ndarray: 2D array of global solar irradiance values
|
|
466
|
+
ndarray: 2D array of global solar irradiance values in W/m²
|
|
467
|
+
- Values represent total solar energy flux on horizontal surfaces
|
|
468
|
+
- Range: 0.0 to (direct_normal_irradiance * sin(elevation) + diffuse_irradiance)
|
|
469
|
+
- NaN indicates invalid measurement locations
|
|
470
|
+
|
|
471
|
+
Note:
|
|
472
|
+
Global irradiance is the standard metric used for solar energy assessment
|
|
473
|
+
and represents the maximum solar energy available at each location.
|
|
308
474
|
"""
|
|
309
475
|
|
|
476
|
+
# Extract visualization parameters
|
|
310
477
|
colormap = kwargs.get("colormap", 'magma')
|
|
311
478
|
|
|
312
|
-
# Create kwargs for
|
|
479
|
+
# Create kwargs for individual component calculations
|
|
480
|
+
# Both direct and diffuse calculations use the same base parameters
|
|
313
481
|
direct_diffuse_kwargs = kwargs.copy()
|
|
314
482
|
direct_diffuse_kwargs.update({
|
|
315
|
-
'show_plot': True,
|
|
316
|
-
'obj_export': False
|
|
483
|
+
'show_plot': True, # Show intermediate results for debugging
|
|
484
|
+
'obj_export': False # Don't export intermediate results
|
|
317
485
|
})
|
|
318
486
|
|
|
319
|
-
# Compute direct irradiance
|
|
487
|
+
# Compute direct irradiance component
|
|
488
|
+
# Accounts for sun position, shadows, and tree transmittance
|
|
320
489
|
direct_map = get_direct_solar_irradiance_map(
|
|
321
490
|
voxel_data,
|
|
322
491
|
meshsize,
|
|
@@ -326,7 +495,8 @@ def get_global_solar_irradiance_map(
|
|
|
326
495
|
**direct_diffuse_kwargs
|
|
327
496
|
)
|
|
328
497
|
|
|
329
|
-
# Compute diffuse irradiance
|
|
498
|
+
# Compute diffuse irradiance component
|
|
499
|
+
# Based on Sky View Factor and atmospheric scattering
|
|
330
500
|
diffuse_map = get_diffuse_solar_irradiance_map(
|
|
331
501
|
voxel_data,
|
|
332
502
|
meshsize,
|
|
@@ -334,16 +504,20 @@ def get_global_solar_irradiance_map(
|
|
|
334
504
|
**direct_diffuse_kwargs
|
|
335
505
|
)
|
|
336
506
|
|
|
337
|
-
# Sum the two components
|
|
507
|
+
# Sum the two components to get total global irradiance
|
|
508
|
+
# This represents the total solar energy available at each location
|
|
338
509
|
global_map = direct_map + diffuse_map
|
|
339
510
|
|
|
511
|
+
# Determine colormap scaling range from actual data
|
|
340
512
|
vmin = kwargs.get("vmin", np.nanmin(global_map))
|
|
341
513
|
vmax = kwargs.get("vmax", np.nanmax(global_map))
|
|
342
514
|
|
|
343
|
-
# Optional visualization
|
|
515
|
+
# Optional visualization of combined results
|
|
344
516
|
if show_plot:
|
|
517
|
+
# Set up colormap with special handling for invalid data
|
|
345
518
|
cmap = plt.cm.get_cmap(colormap).copy()
|
|
346
|
-
cmap.set_bad(color='lightgray')
|
|
519
|
+
cmap.set_bad(color='lightgray') # NaN values shown in gray
|
|
520
|
+
|
|
347
521
|
plt.figure(figsize=(10, 8))
|
|
348
522
|
# plt.title("Global Solar Irradiance Map")
|
|
349
523
|
plt.imshow(global_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
|
|
@@ -351,9 +525,10 @@ def get_global_solar_irradiance_map(
|
|
|
351
525
|
plt.axis('off')
|
|
352
526
|
plt.show()
|
|
353
527
|
|
|
354
|
-
# Optional OBJ
|
|
528
|
+
# Optional export to 3D OBJ format for external visualization
|
|
355
529
|
obj_export = kwargs.get("obj_export", False)
|
|
356
530
|
if obj_export:
|
|
531
|
+
# Get export parameters with defaults
|
|
357
532
|
dem_grid = kwargs.get("dem_grid", np.zeros_like(global_map))
|
|
358
533
|
output_dir = kwargs.get("output_directory", "output")
|
|
359
534
|
output_file_name = kwargs.get("output_file_name", "global_solar_irradiance")
|
|
@@ -361,6 +536,8 @@ def get_global_solar_irradiance_map(
|
|
|
361
536
|
alpha = kwargs.get("alpha", 1.0)
|
|
362
537
|
meshsize_param = kwargs.get("meshsize", meshsize)
|
|
363
538
|
view_point_height = kwargs.get("view_point_height", 1.5)
|
|
539
|
+
|
|
540
|
+
# Export as colored 3D mesh
|
|
364
541
|
grid_to_obj(
|
|
365
542
|
global_map,
|
|
366
543
|
dem_grid,
|
|
@@ -381,26 +558,61 @@ def get_solar_positions_astral(times, lon, lat):
|
|
|
381
558
|
"""
|
|
382
559
|
Compute solar azimuth and elevation using Astral for given times and location.
|
|
383
560
|
|
|
561
|
+
This function uses the Astral astronomical library to calculate precise solar positions
|
|
562
|
+
based on location coordinates and timestamps. The calculations account for Earth's
|
|
563
|
+
orbital mechanics, axial tilt, and atmospheric refraction effects.
|
|
564
|
+
|
|
565
|
+
Astronomical Background:
|
|
566
|
+
- Solar position depends on date, time, and geographic location
|
|
567
|
+
- Azimuth: Horizontal angle measured clockwise from North (0°-360°)
|
|
568
|
+
- Elevation: Vertical angle above the horizon (-90° to +90°)
|
|
569
|
+
- Calculations use standard astronomical algorithms (e.g., NREL SPA)
|
|
570
|
+
|
|
571
|
+
Coordinate System:
|
|
572
|
+
- Azimuth: 0° = North, 90° = East, 180° = South, 270° = West
|
|
573
|
+
- Elevation: 0° = horizon, 90° = zenith, negative values = below horizon
|
|
574
|
+
- All angles are in degrees for consistency with weather data formats
|
|
575
|
+
|
|
384
576
|
The function:
|
|
385
|
-
1. Creates an Astral observer at the specified location
|
|
386
|
-
2. Computes sun position for each timestamp
|
|
387
|
-
3. Returns DataFrame with azimuth and elevation angles
|
|
577
|
+
1. Creates an Astral observer at the specified geographic location
|
|
578
|
+
2. Computes sun position for each timestamp in the input array
|
|
579
|
+
3. Returns DataFrame with azimuth and elevation angles for further processing
|
|
388
580
|
|
|
389
581
|
Args:
|
|
390
|
-
times (DatetimeIndex): Array of timezone-aware datetime objects
|
|
391
|
-
|
|
392
|
-
|
|
582
|
+
times (DatetimeIndex): Array of timezone-aware datetime objects
|
|
583
|
+
Must include timezone information for accurate calculations
|
|
584
|
+
lon (float): Longitude in degrees (positive = East, negative = West)
|
|
585
|
+
Range: -180° to +180°
|
|
586
|
+
lat (float): Latitude in degrees (positive = North, negative = South)
|
|
587
|
+
Range: -90° to +90°
|
|
393
588
|
|
|
394
589
|
Returns:
|
|
395
|
-
DataFrame: DataFrame with columns 'azimuth' and 'elevation' containing solar positions
|
|
590
|
+
DataFrame: DataFrame with columns 'azimuth' and 'elevation' containing solar positions
|
|
591
|
+
- Index: Input timestamps (timezone-aware)
|
|
592
|
+
- 'azimuth': Solar azimuth angles in degrees (0°-360°)
|
|
593
|
+
- 'elevation': Solar elevation angles in degrees (-90° to +90°)
|
|
594
|
+
- All values are float type for numerical calculations
|
|
595
|
+
|
|
596
|
+
Note:
|
|
597
|
+
Input times must be timezone-aware. The function preserves the original
|
|
598
|
+
timezone information and performs calculations in the specified timezone.
|
|
396
599
|
"""
|
|
600
|
+
# Create an astronomical observer at the specified geographic location
|
|
397
601
|
observer = Observer(latitude=lat, longitude=lon)
|
|
602
|
+
|
|
603
|
+
# Initialize result DataFrame with appropriate structure
|
|
398
604
|
df_pos = pd.DataFrame(index=times, columns=['azimuth', 'elevation'], dtype=float)
|
|
399
605
|
|
|
606
|
+
# Calculate solar position for each timestamp
|
|
400
607
|
for t in times:
|
|
401
608
|
# t is already timezone-aware; no need to replace tzinfo
|
|
609
|
+
# Calculate solar elevation (vertical angle above horizon)
|
|
402
610
|
el = elevation(observer=observer, dateandtime=t)
|
|
611
|
+
|
|
612
|
+
# Calculate solar azimuth (horizontal angle from North)
|
|
403
613
|
az = azimuth(observer=observer, dateandtime=t)
|
|
614
|
+
|
|
615
|
+
# Store results in DataFrame
|
|
404
616
|
df_pos.at[t, 'elevation'] = el
|
|
405
617
|
df_pos.at[t, 'azimuth'] = az
|
|
406
618
|
|
|
@@ -417,100 +629,150 @@ def get_cumulative_global_solar_irradiance(
|
|
|
417
629
|
"""
|
|
418
630
|
Compute cumulative global solar irradiance over a specified period using data from an EPW file.
|
|
419
631
|
|
|
632
|
+
This function performs time-series analysis of solar irradiance by processing weather data
|
|
633
|
+
over a user-defined period and accumulating irradiance values at each location. The result
|
|
634
|
+
represents the total solar energy received during the specified time period, which is
|
|
635
|
+
essential for seasonal analysis, solar panel positioning, and energy yield predictions.
|
|
636
|
+
|
|
637
|
+
Cumulative Analysis Concept:
|
|
638
|
+
- Instantaneous irradiance (W/m²): Power at a specific moment
|
|
639
|
+
- Cumulative irradiance (Wh/m²): Energy accumulated over time
|
|
640
|
+
- Integration: Sum of (irradiance × time_step) for all timesteps
|
|
641
|
+
- Applications: Annual energy yield, seasonal variations, optimal siting
|
|
642
|
+
|
|
643
|
+
Time Period Processing:
|
|
644
|
+
- Supports flexible time ranges (daily, seasonal, annual analysis)
|
|
645
|
+
- Handles timezone conversions between local and UTC time
|
|
646
|
+
- Filters weather data based on user-specified start/end times
|
|
647
|
+
- Accounts for leap years and varying daylight hours
|
|
648
|
+
|
|
649
|
+
Performance Optimization:
|
|
650
|
+
- Pre-calculates diffuse map once (scales linearly with DHI)
|
|
651
|
+
- Processes direct component for each timestep (varies with sun position)
|
|
652
|
+
- Uses efficient memory management for large time series
|
|
653
|
+
- Provides optional progress monitoring for long calculations
|
|
654
|
+
|
|
420
655
|
The function:
|
|
421
|
-
1. Filters EPW data for specified time period
|
|
422
|
-
2. Computes sun positions for each timestep
|
|
423
|
-
3. Calculates and accumulates global irradiance maps
|
|
424
|
-
4. Handles tree transmittance and visualization
|
|
656
|
+
1. Filters EPW data for specified time period with timezone handling
|
|
657
|
+
2. Computes sun positions for each timestep using astronomical calculations
|
|
658
|
+
3. Calculates and accumulates global irradiance maps over the entire period
|
|
659
|
+
4. Handles tree transmittance and provides visualization/export options
|
|
425
660
|
|
|
426
661
|
Args:
|
|
427
|
-
voxel_data (ndarray): 3D array of voxel values
|
|
428
|
-
meshsize (float): Size of each voxel in meters
|
|
429
|
-
df (DataFrame): EPW weather data
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
662
|
+
voxel_data (ndarray): 3D array of voxel values representing the urban environment
|
|
663
|
+
meshsize (float): Size of each voxel in meters (spatial resolution)
|
|
664
|
+
df (DataFrame): EPW weather data with columns 'DNI', 'DHI' and datetime index
|
|
665
|
+
Must include complete meteorological dataset
|
|
666
|
+
lon (float): Longitude in degrees for solar position calculations
|
|
667
|
+
lat (float): Latitude in degrees for solar position calculations
|
|
668
|
+
tz (float): Timezone offset in hours from UTC (positive = East of UTC)
|
|
669
|
+
direct_normal_irradiance_scaling (float): Scaling factor for direct normal irradiance
|
|
670
|
+
Allows sensitivity analysis or unit conversions
|
|
671
|
+
diffuse_irradiance_scaling (float): Scaling factor for diffuse horizontal irradiance
|
|
672
|
+
Allows sensitivity analysis or unit conversions
|
|
435
673
|
**kwargs: Additional arguments including:
|
|
436
674
|
- view_point_height (float): Observer height in meters (default: 1.5)
|
|
675
|
+
Height above ground where measurements are taken
|
|
437
676
|
- start_time (str): Start time in format 'MM-DD HH:MM:SS'
|
|
438
|
-
|
|
439
|
-
-
|
|
677
|
+
Defines beginning of analysis period (default: "01-01 05:00:00")
|
|
678
|
+
- end_time (str): End time in format 'MM-DD HH:MM:SS'
|
|
679
|
+
Defines end of analysis period (default: "01-01 20:00:00")
|
|
680
|
+
- tree_k (float): Tree extinction coefficient for transmittance calculations
|
|
681
|
+
Higher values mean trees block more light
|
|
440
682
|
- tree_lad (float): Leaf area density in m^-1
|
|
441
|
-
|
|
683
|
+
Affects light attenuation through tree canopies
|
|
684
|
+
- show_plot (bool): Whether to show final accumulated results
|
|
442
685
|
- show_each_timestep (bool): Whether to show plots for each timestep
|
|
443
|
-
|
|
444
|
-
-
|
|
445
|
-
-
|
|
446
|
-
-
|
|
447
|
-
-
|
|
448
|
-
-
|
|
449
|
-
-
|
|
450
|
-
-
|
|
451
|
-
-
|
|
686
|
+
Useful for debugging but significantly increases computation time
|
|
687
|
+
- colormap (str): Matplotlib colormap name for visualization
|
|
688
|
+
- vmin (float): Minimum value for colormap scaling
|
|
689
|
+
- vmax (float): Maximum value for colormap scaling
|
|
690
|
+
- obj_export (bool): Whether to export results as 3D OBJ file
|
|
691
|
+
- output_directory (str): Directory for file exports
|
|
692
|
+
- output_file_name (str): Base filename for exports
|
|
693
|
+
- dem_grid (ndarray): Digital elevation model for 3D export
|
|
694
|
+
- num_colors (int): Number of discrete colors for OBJ export
|
|
695
|
+
- alpha (float): Transparency value for 3D visualization
|
|
452
696
|
|
|
453
697
|
Returns:
|
|
454
|
-
ndarray: 2D array of cumulative global solar irradiance values
|
|
698
|
+
ndarray: 2D array of cumulative global solar irradiance values in W/m²·hour
|
|
699
|
+
- Values represent total solar energy received during the analysis period
|
|
700
|
+
- Range depends on period length and local climate conditions
|
|
701
|
+
- NaN indicates invalid measurement locations (e.g., on buildings, water)
|
|
702
|
+
|
|
703
|
+
Note:
|
|
704
|
+
The function efficiently handles large time series by pre-computing the diffuse
|
|
705
|
+
component once and scaling it for each timestep, significantly reducing
|
|
706
|
+
computation time for long-term analysis.
|
|
455
707
|
"""
|
|
708
|
+
# Extract parameters with defaults for observer positioning and visualization
|
|
456
709
|
view_point_height = kwargs.get("view_point_height", 1.5)
|
|
457
710
|
colormap = kwargs.get("colormap", 'magma')
|
|
458
711
|
start_time = kwargs.get("start_time", "01-01 05:00:00")
|
|
459
712
|
end_time = kwargs.get("end_time", "01-01 20:00:00")
|
|
460
713
|
|
|
714
|
+
# Validate input data
|
|
461
715
|
if df.empty:
|
|
462
716
|
raise ValueError("No data in EPW file.")
|
|
463
717
|
|
|
464
|
-
# Parse start and end times without year
|
|
718
|
+
# Parse start and end times without year (supports multi-year analysis)
|
|
465
719
|
try:
|
|
466
720
|
start_dt = datetime.strptime(start_time, "%m-%d %H:%M:%S")
|
|
467
721
|
end_dt = datetime.strptime(end_time, "%m-%d %H:%M:%S")
|
|
468
722
|
except ValueError as ve:
|
|
469
723
|
raise ValueError("start_time and end_time must be in format 'MM-DD HH:MM:SS'") from ve
|
|
470
724
|
|
|
471
|
-
# Add hour of year column
|
|
725
|
+
# Add hour of year column for efficient time filtering
|
|
726
|
+
# Hour 1 = January 1st, 00:00; Hour 8760 = December 31st, 23:00
|
|
472
727
|
df['hour_of_year'] = (df.index.dayofyear - 1) * 24 + df.index.hour + 1
|
|
473
728
|
|
|
474
|
-
# Convert dates to day of year and hour
|
|
729
|
+
# Convert parsed dates to day of year and hour for filtering
|
|
475
730
|
start_doy = datetime(2000, start_dt.month, start_dt.day).timetuple().tm_yday
|
|
476
731
|
end_doy = datetime(2000, end_dt.month, end_dt.day).timetuple().tm_yday
|
|
477
732
|
|
|
478
733
|
start_hour = (start_doy - 1) * 24 + start_dt.hour + 1
|
|
479
734
|
end_hour = (end_doy - 1) * 24 + end_dt.hour + 1
|
|
480
735
|
|
|
481
|
-
# Handle period crossing year boundary
|
|
736
|
+
# Handle period crossing year boundary (e.g., Dec 15 to Jan 15)
|
|
482
737
|
if start_hour <= end_hour:
|
|
738
|
+
# Normal period within single year
|
|
483
739
|
df_period = df[(df['hour_of_year'] >= start_hour) & (df['hour_of_year'] <= end_hour)]
|
|
484
740
|
else:
|
|
741
|
+
# Period crosses year boundary - include end and beginning of year
|
|
485
742
|
df_period = df[(df['hour_of_year'] >= start_hour) | (df['hour_of_year'] <= end_hour)]
|
|
486
743
|
|
|
487
|
-
#
|
|
744
|
+
# Apply minute-level filtering within start/end hours for precision
|
|
488
745
|
df_period = df_period[
|
|
489
746
|
((df_period.index.hour != start_dt.hour) | (df_period.index.minute >= start_dt.minute)) &
|
|
490
747
|
((df_period.index.hour != end_dt.hour) | (df_period.index.minute <= end_dt.minute))
|
|
491
748
|
]
|
|
492
749
|
|
|
750
|
+
# Validate filtered data
|
|
493
751
|
if df_period.empty:
|
|
494
752
|
raise ValueError("No EPW data in the specified period.")
|
|
495
753
|
|
|
496
|
-
# Handle timezone conversion
|
|
754
|
+
# Handle timezone conversion for accurate solar position calculations
|
|
755
|
+
# Convert local time (from EPW) to UTC for astronomical calculations
|
|
497
756
|
offset_minutes = int(tz * 60)
|
|
498
757
|
local_tz = pytz.FixedOffset(offset_minutes)
|
|
499
758
|
df_period_local = df_period.copy()
|
|
500
759
|
df_period_local.index = df_period_local.index.tz_localize(local_tz)
|
|
501
760
|
df_period_utc = df_period_local.tz_convert(pytz.UTC)
|
|
502
761
|
|
|
503
|
-
# Compute solar positions for period
|
|
762
|
+
# Compute solar positions for entire analysis period
|
|
763
|
+
# This is done once to optimize performance
|
|
504
764
|
solar_positions = get_solar_positions_astral(df_period_utc.index, lon, lat)
|
|
505
765
|
|
|
506
|
-
#
|
|
766
|
+
# Prepare parameters for efficient diffuse irradiance calculation
|
|
767
|
+
# Create kwargs for diffuse calculation with visualization disabled
|
|
507
768
|
diffuse_kwargs = kwargs.copy()
|
|
508
769
|
diffuse_kwargs.update({
|
|
509
770
|
'show_plot': False,
|
|
510
771
|
'obj_export': False
|
|
511
772
|
})
|
|
512
773
|
|
|
513
|
-
#
|
|
774
|
+
# Pre-compute base diffuse map once with unit irradiance
|
|
775
|
+
# This map will be scaled by actual DHI values for each timestep
|
|
514
776
|
base_diffuse_map = get_diffuse_solar_irradiance_map(
|
|
515
777
|
voxel_data,
|
|
516
778
|
meshsize,
|
|
@@ -518,11 +780,12 @@ def get_cumulative_global_solar_irradiance(
|
|
|
518
780
|
**diffuse_kwargs
|
|
519
781
|
)
|
|
520
782
|
|
|
521
|
-
# Initialize accumulation
|
|
783
|
+
# Initialize accumulation arrays for energy integration
|
|
522
784
|
cumulative_map = np.zeros((voxel_data.shape[0], voxel_data.shape[1]))
|
|
523
785
|
mask_map = np.ones((voxel_data.shape[0], voxel_data.shape[1]), dtype=bool)
|
|
524
786
|
|
|
525
|
-
#
|
|
787
|
+
# Prepare parameters for direct irradiance calculations
|
|
788
|
+
# Create kwargs for direct calculation with visualization disabled
|
|
526
789
|
direct_kwargs = kwargs.copy()
|
|
527
790
|
direct_kwargs.update({
|
|
528
791
|
'show_plot': False,
|
|
@@ -530,9 +793,10 @@ def get_cumulative_global_solar_irradiance(
|
|
|
530
793
|
'obj_export': False
|
|
531
794
|
})
|
|
532
795
|
|
|
533
|
-
#
|
|
796
|
+
# Main processing loop: iterate through each timestep in the analysis period
|
|
534
797
|
for idx, (time_utc, row) in enumerate(df_period_utc.iterrows()):
|
|
535
|
-
#
|
|
798
|
+
# Apply scaling factors to weather data
|
|
799
|
+
# Allows for sensitivity analysis or unit conversions
|
|
536
800
|
DNI = row['DNI'] * direct_normal_irradiance_scaling
|
|
537
801
|
DHI = row['DHI'] * diffuse_irradiance_scaling
|
|
538
802
|
time_local = df_period_local.index[idx]
|
|
@@ -791,44 +1055,78 @@ def compute_solar_irradiance_for_all_faces(
|
|
|
791
1055
|
):
|
|
792
1056
|
"""
|
|
793
1057
|
Numba-compiled function to compute direct, diffuse, and global solar irradiance
|
|
794
|
-
for each face in
|
|
1058
|
+
for each face in a 3D building mesh.
|
|
1059
|
+
|
|
1060
|
+
This optimized function processes all mesh faces in parallel to calculate solar
|
|
1061
|
+
irradiance components. It handles both direct radiation (dependent on sun position
|
|
1062
|
+
and surface orientation) and diffuse radiation (dependent on sky visibility).
|
|
1063
|
+
The function is compiled with Numba for high-performance computation on large meshes.
|
|
1064
|
+
|
|
1065
|
+
Surface Irradiance Physics:
|
|
1066
|
+
- Direct component: DNI × cos(incidence_angle) × transmittance
|
|
1067
|
+
- Diffuse component: DHI × sky_view_factor
|
|
1068
|
+
- Incidence angle: Angle between sun direction and surface normal
|
|
1069
|
+
- Transmittance: Attenuation factor from obstacles and vegetation
|
|
1070
|
+
|
|
1071
|
+
Boundary Condition Handling:
|
|
1072
|
+
- Vertical boundary faces are excluded (mesh edges touching domain boundaries)
|
|
1073
|
+
- Invalid faces (NaN SVF) are skipped to maintain data consistency
|
|
1074
|
+
- Surface orientation affects direct radiation calculation
|
|
1075
|
+
|
|
1076
|
+
Performance Optimizations:
|
|
1077
|
+
- Numba JIT compilation for near C-speed execution
|
|
1078
|
+
- Parallel processing of face calculations
|
|
1079
|
+
- Efficient geometric computations using vectorized operations
|
|
1080
|
+
- Memory-optimized array operations
|
|
795
1081
|
|
|
796
1082
|
Args:
|
|
797
|
-
face_centers (float64[:, :]): (N x 3) array of face center
|
|
798
|
-
face_normals (float64[:, :]): (N x 3) array of
|
|
799
|
-
face_svf (float64[:]): (N) array of
|
|
800
|
-
sun_direction (float64[:]): (3) array for sun direction (dx, dy, dz)
|
|
1083
|
+
face_centers (float64[:, :]): (N x 3) array of face center coordinates in real-world units
|
|
1084
|
+
face_normals (float64[:, :]): (N x 3) array of outward-pointing unit normal vectors
|
|
1085
|
+
face_svf (float64[:]): (N,) array of Sky View Factor values for each face (0.0-1.0)
|
|
1086
|
+
sun_direction (float64[:]): (3,) array for normalized sun direction vector (dx, dy, dz)
|
|
801
1087
|
direct_normal_irradiance (float): Direct normal irradiance (DNI) in W/m²
|
|
802
1088
|
diffuse_irradiance (float): Diffuse horizontal irradiance (DHI) in W/m²
|
|
803
|
-
voxel_data (ndarray): 3D array of voxel values
|
|
804
|
-
meshsize (float): Size of each voxel in meters
|
|
805
|
-
tree_k (float): Tree extinction coefficient
|
|
806
|
-
tree_lad (float): Leaf area density
|
|
807
|
-
hit_values (tuple): Values considered 'sky' (e.g. (0,))
|
|
808
|
-
inclusion_mode (bool): Whether
|
|
809
|
-
grid_bounds_real (float64[2,3]): [[x_min,
|
|
810
|
-
boundary_epsilon (float): Distance threshold for
|
|
1089
|
+
voxel_data (ndarray): 3D array of voxel values for obstacle detection
|
|
1090
|
+
meshsize (float): Size of each voxel in meters (spatial resolution)
|
|
1091
|
+
tree_k (float): Tree extinction coefficient for Beer-Lambert law
|
|
1092
|
+
tree_lad (float): Leaf area density in m^-1
|
|
1093
|
+
hit_values (tuple): Values considered 'sky' for ray tracing (e.g. (0,))
|
|
1094
|
+
inclusion_mode (bool): Whether hit_values are included (True) or excluded (False)
|
|
1095
|
+
grid_bounds_real (float64[2,3]): Domain boundaries [[x_min,y_min,z_min],[x_max,y_max,z_max]]
|
|
1096
|
+
boundary_epsilon (float): Distance threshold for boundary face detection
|
|
811
1097
|
|
|
812
1098
|
Returns:
|
|
813
|
-
|
|
1099
|
+
tuple: Three float64[N] arrays containing:
|
|
1100
|
+
- direct_irr: Direct solar irradiance for each face (W/m²)
|
|
1101
|
+
- diffuse_irr: Diffuse solar irradiance for each face (W/m²)
|
|
1102
|
+
- global_irr: Global solar irradiance for each face (W/m²)
|
|
1103
|
+
|
|
1104
|
+
Note:
|
|
1105
|
+
This function is optimized with Numba and should not be called directly.
|
|
1106
|
+
Use the higher-level wrapper functions for normal operation.
|
|
814
1107
|
"""
|
|
815
1108
|
n_faces = face_centers.shape[0]
|
|
816
1109
|
|
|
1110
|
+
# Initialize output arrays for each irradiance component
|
|
817
1111
|
face_direct = np.zeros(n_faces, dtype=np.float64)
|
|
818
1112
|
face_diffuse = np.zeros(n_faces, dtype=np.float64)
|
|
819
1113
|
face_global = np.zeros(n_faces, dtype=np.float64)
|
|
820
1114
|
|
|
1115
|
+
# Extract domain boundaries for boundary face detection
|
|
821
1116
|
x_min, y_min, z_min = grid_bounds_real[0, 0], grid_bounds_real[0, 1], grid_bounds_real[0, 2]
|
|
822
1117
|
x_max, y_max, z_max = grid_bounds_real[1, 0], grid_bounds_real[1, 1], grid_bounds_real[1, 2]
|
|
823
1118
|
|
|
1119
|
+
# Process each face individually (Numba optimizes this loop)
|
|
824
1120
|
for fidx in range(n_faces):
|
|
825
1121
|
center = face_centers[fidx]
|
|
826
1122
|
normal = face_normals[fidx]
|
|
827
1123
|
svf = face_svf[fidx]
|
|
828
1124
|
|
|
829
|
-
#
|
|
830
|
-
|
|
1125
|
+
# Check for vertical boundary faces that should be excluded
|
|
1126
|
+
# These are mesh edges at domain boundaries, not actual building surfaces
|
|
1127
|
+
is_vertical = (abs(normal[2]) < 0.01) # Nearly vertical normal
|
|
831
1128
|
|
|
1129
|
+
# Check if face center is at domain boundary
|
|
832
1130
|
on_x_min = (abs(center[0] - x_min) < boundary_epsilon)
|
|
833
1131
|
on_y_min = (abs(center[1] - y_min) < boundary_epsilon)
|
|
834
1132
|
on_x_max = (abs(center[0] - x_max) < boundary_epsilon)
|
|
@@ -836,33 +1134,35 @@ def compute_solar_irradiance_for_all_faces(
|
|
|
836
1134
|
|
|
837
1135
|
is_boundary_vertical = is_vertical and (on_x_min or on_y_min or on_x_max or on_y_max)
|
|
838
1136
|
|
|
1137
|
+
# Skip boundary faces to avoid artifacts
|
|
839
1138
|
if is_boundary_vertical:
|
|
840
1139
|
face_direct[fidx] = np.nan
|
|
841
1140
|
face_diffuse[fidx] = np.nan
|
|
842
1141
|
face_global[fidx] = np.nan
|
|
843
1142
|
continue
|
|
844
1143
|
|
|
845
|
-
#
|
|
846
|
-
if svf != svf: # NaN check in Numba
|
|
1144
|
+
# Skip faces with invalid SVF data
|
|
1145
|
+
if svf != svf: # NaN check in Numba-compatible way
|
|
847
1146
|
face_direct[fidx] = np.nan
|
|
848
1147
|
face_diffuse[fidx] = np.nan
|
|
849
1148
|
face_global[fidx] = np.nan
|
|
850
1149
|
continue
|
|
851
1150
|
|
|
852
|
-
#
|
|
1151
|
+
# Calculate direct irradiance component
|
|
1152
|
+
# Only surfaces oriented towards the sun receive direct radiation
|
|
853
1153
|
cos_incidence = normal[0]*sun_direction[0] + \
|
|
854
1154
|
normal[1]*sun_direction[1] + \
|
|
855
1155
|
normal[2]*sun_direction[2]
|
|
856
1156
|
|
|
857
1157
|
direct_val = 0.0
|
|
858
|
-
if cos_incidence > 0.0:
|
|
859
|
-
# Offset ray origin slightly to avoid self-intersection
|
|
860
|
-
offset_vox = 0.1
|
|
1158
|
+
if cos_incidence > 0.0: # Surface faces towards sun
|
|
1159
|
+
# Offset ray origin slightly along normal to avoid self-intersection
|
|
1160
|
+
offset_vox = 0.1 # Small offset in voxel units
|
|
861
1161
|
ray_origin_x = center[0]/meshsize + normal[0]*offset_vox
|
|
862
1162
|
ray_origin_y = center[1]/meshsize + normal[1]*offset_vox
|
|
863
1163
|
ray_origin_z = center[2]/meshsize + normal[2]*offset_vox
|
|
864
1164
|
|
|
865
|
-
#
|
|
1165
|
+
# Cast ray toward the sun to check for obstructions
|
|
866
1166
|
hit_detected, transmittance = trace_ray_generic(
|
|
867
1167
|
voxel_data,
|
|
868
1168
|
np.array([ray_origin_x, ray_origin_y, ray_origin_z], dtype=np.float64),
|
|
@@ -873,15 +1173,20 @@ def compute_solar_irradiance_for_all_faces(
|
|
|
873
1173
|
tree_lad,
|
|
874
1174
|
inclusion_mode
|
|
875
1175
|
)
|
|
1176
|
+
|
|
1177
|
+
# Calculate direct irradiance if path to sun is clear/partially clear
|
|
876
1178
|
if not hit_detected:
|
|
877
1179
|
direct_val = direct_normal_irradiance * cos_incidence * transmittance
|
|
878
1180
|
|
|
879
|
-
#
|
|
1181
|
+
# Calculate diffuse irradiance component using Sky View Factor
|
|
1182
|
+
# All surfaces receive diffuse radiation proportional to their sky visibility
|
|
880
1183
|
diffuse_val = svf * diffuse_irradiance
|
|
1184
|
+
|
|
1185
|
+
# Ensure diffuse irradiance doesn't exceed theoretical maximum
|
|
881
1186
|
if diffuse_val > diffuse_irradiance:
|
|
882
1187
|
diffuse_val = diffuse_irradiance
|
|
883
1188
|
|
|
884
|
-
#
|
|
1189
|
+
# Store results for this face
|
|
885
1190
|
face_direct[fidx] = direct_val
|
|
886
1191
|
face_diffuse[fidx] = diffuse_val
|
|
887
1192
|
face_global[fidx] = direct_val + diffuse_val
|
|
@@ -903,57 +1208,100 @@ def get_building_solar_irradiance(
|
|
|
903
1208
|
**kwargs
|
|
904
1209
|
):
|
|
905
1210
|
"""
|
|
906
|
-
Calculate solar irradiance on building surfaces using SVF,
|
|
907
|
-
with
|
|
1211
|
+
Calculate solar irradiance on building surfaces using Sky View Factor (SVF) analysis,
|
|
1212
|
+
with high-performance computation accelerated by Numba JIT compilation.
|
|
1213
|
+
|
|
1214
|
+
This function performs detailed solar irradiance analysis on 3D building surfaces
|
|
1215
|
+
represented as triangulated meshes. It calculates both direct and diffuse components
|
|
1216
|
+
of solar radiation for each mesh face, accounting for surface orientation, shadows,
|
|
1217
|
+
and sky visibility. The computation is optimized for large urban models using
|
|
1218
|
+
efficient algorithms and parallel processing.
|
|
1219
|
+
|
|
1220
|
+
Mesh-Based Analysis Advantages:
|
|
1221
|
+
- Surface-specific calculations for facades, roofs, and complex geometries
|
|
1222
|
+
- Accurate accounting of surface orientation and local shading effects
|
|
1223
|
+
- Integration with 3D visualization and CAD workflows
|
|
1224
|
+
- Detailed irradiance data for building energy modeling
|
|
1225
|
+
|
|
1226
|
+
Performance Features:
|
|
1227
|
+
- Numba JIT compilation for near C-speed execution
|
|
1228
|
+
- Parallel processing of mesh faces
|
|
1229
|
+
- Efficient ray tracing with tree transmittance
|
|
1230
|
+
- Memory-optimized operations for large datasets
|
|
1231
|
+
|
|
1232
|
+
Physical Modeling:
|
|
1233
|
+
- Direct irradiance: Based on sun position and surface orientation
|
|
1234
|
+
- Diffuse irradiance: Based on Sky View Factor from each surface
|
|
1235
|
+
- Tree effects: Partial transmittance using Beer-Lambert law
|
|
1236
|
+
- Boundary handling: Automatic exclusion of domain boundary artifacts
|
|
908
1237
|
|
|
909
1238
|
Args:
|
|
910
|
-
voxel_data (ndarray): 3D array of voxel values
|
|
911
|
-
meshsize (float): Size of each voxel in meters
|
|
912
|
-
building_svf_mesh (trimesh.Trimesh): Building mesh with SVF values in metadata
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1239
|
+
voxel_data (ndarray): 3D array of voxel values representing the urban environment
|
|
1240
|
+
meshsize (float): Size of each voxel in meters (spatial resolution)
|
|
1241
|
+
building_svf_mesh (trimesh.Trimesh): Building mesh with pre-calculated SVF values in metadata
|
|
1242
|
+
Must have 'svf' array in mesh.metadata
|
|
1243
|
+
azimuth_degrees (float): Sun azimuth angle in degrees (0=North, 90=East)
|
|
1244
|
+
elevation_degrees (float): Sun elevation angle in degrees above horizon (0-90°)
|
|
1245
|
+
direct_normal_irradiance (float): Direct normal irradiance (DNI) in W/m² from weather data
|
|
1246
|
+
diffuse_irradiance (float): Diffuse horizontal irradiance (DHI) in W/m² from weather data
|
|
1247
|
+
**kwargs: Additional parameters including:
|
|
1248
|
+
- tree_k (float): Tree extinction coefficient (default: 0.6)
|
|
1249
|
+
Higher values mean trees block more light
|
|
1250
|
+
- tree_lad (float): Leaf area density in m^-1 (default: 1.0)
|
|
1251
|
+
Affects light attenuation through tree canopies
|
|
1252
|
+
- progress_report (bool): Whether to print timing information (default: False)
|
|
1253
|
+
- obj_export (bool): Whether to export results as OBJ file
|
|
1254
|
+
- output_directory (str): Directory for file exports
|
|
1255
|
+
- output_file_name (str): Base filename for exports
|
|
918
1256
|
|
|
919
1257
|
Returns:
|
|
920
|
-
trimesh.Trimesh: A copy of the input mesh with
|
|
1258
|
+
trimesh.Trimesh: A copy of the input mesh with irradiance data stored in metadata:
|
|
1259
|
+
- 'svf': Sky View Factor for each face (preserved from input)
|
|
1260
|
+
- 'direct': Direct solar irradiance for each face (W/m²)
|
|
1261
|
+
- 'diffuse': Diffuse solar irradiance for each face (W/m²)
|
|
1262
|
+
- 'global': Global solar irradiance for each face (W/m²)
|
|
1263
|
+
|
|
1264
|
+
Note:
|
|
1265
|
+
The input mesh must have SVF values pre-calculated and stored in metadata.
|
|
1266
|
+
Use get_surface_view_factor() to compute SVF before calling this function.
|
|
921
1267
|
"""
|
|
922
1268
|
import time
|
|
923
1269
|
|
|
1270
|
+
# Extract tree transmittance parameters with defaults
|
|
924
1271
|
tree_k = kwargs.get("tree_k", 0.6)
|
|
925
1272
|
tree_lad = kwargs.get("tree_lad", 1.0)
|
|
926
1273
|
progress_report = kwargs.get("progress_report", False)
|
|
927
1274
|
|
|
928
|
-
#
|
|
929
|
-
hit_values = (0,) # '0' = sky
|
|
930
|
-
inclusion_mode = False
|
|
1275
|
+
# Define sky detection parameters for ray tracing
|
|
1276
|
+
hit_values = (0,) # '0' = sky voxel value
|
|
1277
|
+
inclusion_mode = False # Treat non-sky values as obstacles
|
|
931
1278
|
|
|
932
|
-
# Convert angles
|
|
933
|
-
az_rad = np.deg2rad(180 - azimuth_degrees)
|
|
1279
|
+
# Convert solar angles to 3D direction vector using spherical coordinates
|
|
1280
|
+
az_rad = np.deg2rad(180 - azimuth_degrees) # Adjust for coordinate system
|
|
934
1281
|
el_rad = np.deg2rad(elevation_degrees)
|
|
935
1282
|
sun_dx = np.cos(el_rad) * np.cos(az_rad)
|
|
936
1283
|
sun_dy = np.cos(el_rad) * np.sin(az_rad)
|
|
937
1284
|
sun_dz = np.sin(el_rad)
|
|
938
1285
|
sun_direction = np.array([sun_dx, sun_dy, sun_dz], dtype=np.float64)
|
|
939
1286
|
|
|
940
|
-
# Extract mesh data
|
|
941
|
-
face_centers = building_svf_mesh.triangles_center
|
|
942
|
-
face_normals = building_svf_mesh.face_normals
|
|
1287
|
+
# Extract mesh geometry data for processing
|
|
1288
|
+
face_centers = building_svf_mesh.triangles_center # Center point of each face
|
|
1289
|
+
face_normals = building_svf_mesh.face_normals # Normal vector for each face
|
|
943
1290
|
|
|
944
|
-
#
|
|
1291
|
+
# Extract Sky View Factor data from mesh metadata
|
|
945
1292
|
if hasattr(building_svf_mesh, 'metadata') and ('svf' in building_svf_mesh.metadata):
|
|
946
1293
|
face_svf = building_svf_mesh.metadata['svf']
|
|
947
1294
|
else:
|
|
1295
|
+
# Initialize with zeros if SVF not available (should be pre-calculated)
|
|
948
1296
|
face_svf = np.zeros(len(building_svf_mesh.faces), dtype=np.float64)
|
|
949
1297
|
|
|
950
|
-
#
|
|
1298
|
+
# Set up domain boundaries for boundary face detection
|
|
951
1299
|
grid_shape = voxel_data.shape
|
|
952
1300
|
grid_bounds_voxel = np.array([[0,0,0],[grid_shape[0], grid_shape[1], grid_shape[2]]], dtype=np.float64)
|
|
953
1301
|
grid_bounds_real = grid_bounds_voxel * meshsize
|
|
954
|
-
boundary_epsilon = meshsize * 0.05
|
|
1302
|
+
boundary_epsilon = meshsize * 0.05 # Small tolerance for boundary detection
|
|
955
1303
|
|
|
956
|
-
# Call Numba-compiled function
|
|
1304
|
+
# Call high-performance Numba-compiled calculation function
|
|
957
1305
|
t0 = time.time()
|
|
958
1306
|
face_direct, face_diffuse, face_global = compute_solar_irradiance_for_all_faces(
|
|
959
1307
|
face_centers,
|
|
@@ -971,11 +1319,13 @@ def get_building_solar_irradiance(
|
|
|
971
1319
|
grid_bounds_real,
|
|
972
1320
|
boundary_epsilon
|
|
973
1321
|
)
|
|
1322
|
+
|
|
1323
|
+
# Report performance timing if requested
|
|
974
1324
|
if progress_report:
|
|
975
1325
|
elapsed = time.time() - t0
|
|
976
1326
|
print(f"Numba-based solar irradiance calculation took {elapsed:.2f} seconds")
|
|
977
1327
|
|
|
978
|
-
# Create a copy of the mesh
|
|
1328
|
+
# Create a copy of the input mesh to store results
|
|
979
1329
|
irradiance_mesh = building_svf_mesh.copy()
|
|
980
1330
|
if not hasattr(irradiance_mesh, 'metadata'):
|
|
981
1331
|
irradiance_mesh.metadata = {}
|
|
@@ -1387,35 +1737,80 @@ def get_building_global_solar_irradiance_using_epw(
|
|
|
1387
1737
|
|
|
1388
1738
|
def save_irradiance_mesh(irradiance_mesh, output_file_path):
|
|
1389
1739
|
"""
|
|
1390
|
-
Save the irradiance mesh data to a file using pickle.
|
|
1740
|
+
Save the irradiance mesh data to a file using pickle serialization.
|
|
1741
|
+
|
|
1742
|
+
This function provides persistent storage for computed irradiance results,
|
|
1743
|
+
enabling reuse of expensive calculations and sharing of results between
|
|
1744
|
+
analysis sessions. The mesh data includes all geometry, irradiance values,
|
|
1745
|
+
and metadata required for visualization and further analysis.
|
|
1746
|
+
|
|
1747
|
+
Serialization Benefits:
|
|
1748
|
+
- Preserves complete mesh structure with all computed data
|
|
1749
|
+
- Enables offline analysis and visualization workflows
|
|
1750
|
+
- Supports sharing results between different tools and users
|
|
1751
|
+
- Avoids recomputation of expensive irradiance calculations
|
|
1752
|
+
|
|
1753
|
+
Data Preservation:
|
|
1754
|
+
- All mesh geometry (vertices, faces, normals)
|
|
1755
|
+
- Computed irradiance values (direct, diffuse, global)
|
|
1756
|
+
- Sky View Factor data and other metadata
|
|
1757
|
+
- Material properties and visualization settings
|
|
1391
1758
|
|
|
1392
1759
|
Args:
|
|
1393
|
-
irradiance_mesh (trimesh.Trimesh): Mesh with irradiance data in metadata
|
|
1394
|
-
|
|
1760
|
+
irradiance_mesh (trimesh.Trimesh): Mesh with irradiance data in metadata
|
|
1761
|
+
Should contain computed irradiance results
|
|
1762
|
+
output_file_path (str): Path to save the mesh data file
|
|
1763
|
+
Recommended extension: .pkl for clarity
|
|
1764
|
+
|
|
1765
|
+
Note:
|
|
1766
|
+
The function automatically creates the output directory if it doesn't exist.
|
|
1767
|
+
Use pickle format for maximum compatibility with Python data structures.
|
|
1395
1768
|
"""
|
|
1396
1769
|
import pickle
|
|
1397
1770
|
import os
|
|
1398
1771
|
|
|
1399
|
-
# Create output directory if it doesn't exist
|
|
1772
|
+
# Create output directory structure if it doesn't exist
|
|
1400
1773
|
os.makedirs(os.path.dirname(output_file_path), exist_ok=True)
|
|
1401
1774
|
|
|
1402
|
-
#
|
|
1775
|
+
# Serialize mesh data using pickle for complete data preservation
|
|
1403
1776
|
with open(output_file_path, 'wb') as f:
|
|
1404
1777
|
pickle.dump(irradiance_mesh, f)
|
|
1405
1778
|
|
|
1406
1779
|
def load_irradiance_mesh(input_file_path):
|
|
1407
1780
|
"""
|
|
1408
|
-
Load
|
|
1781
|
+
Load previously saved irradiance mesh data from a file.
|
|
1782
|
+
|
|
1783
|
+
This function restores complete mesh data including geometry, computed
|
|
1784
|
+
irradiance values, and all associated metadata. It enables continuation
|
|
1785
|
+
of analysis workflows and reuse of expensive computation results.
|
|
1786
|
+
|
|
1787
|
+
Restoration Capabilities:
|
|
1788
|
+
- Complete mesh geometry with all topological information
|
|
1789
|
+
- All computed irradiance data (direct, diffuse, global components)
|
|
1790
|
+
- Sky View Factor values and analysis metadata
|
|
1791
|
+
- Visualization settings and material properties
|
|
1792
|
+
|
|
1793
|
+
Workflow Integration:
|
|
1794
|
+
- Load results from previous analysis sessions
|
|
1795
|
+
- Share computed data between team members
|
|
1796
|
+
- Perform post-processing and visualization
|
|
1797
|
+
- Compare results from different scenarios
|
|
1409
1798
|
|
|
1410
1799
|
Args:
|
|
1411
|
-
input_file_path (str): Path to the saved mesh data file
|
|
1800
|
+
input_file_path (str): Path to the saved mesh data file
|
|
1801
|
+
Should be a file created by save_irradiance_mesh()
|
|
1412
1802
|
|
|
1413
1803
|
Returns:
|
|
1414
|
-
trimesh.Trimesh:
|
|
1804
|
+
trimesh.Trimesh: Complete mesh with all irradiance data in metadata
|
|
1805
|
+
Ready for visualization, analysis, or further processing
|
|
1806
|
+
|
|
1807
|
+
Note:
|
|
1808
|
+
The loaded mesh maintains all original data structure and can be used
|
|
1809
|
+
immediately for visualization or additional analysis operations.
|
|
1415
1810
|
"""
|
|
1416
1811
|
import pickle
|
|
1417
1812
|
|
|
1418
|
-
#
|
|
1813
|
+
# Deserialize mesh data preserving all original structure
|
|
1419
1814
|
with open(input_file_path, 'rb') as f:
|
|
1420
1815
|
irradiance_mesh = pickle.load(f)
|
|
1421
1816
|
|