voxcity 0.6.26__py3-none-any.whl → 1.0.2__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.
- voxcity/__init__.py +10 -4
- voxcity/downloader/__init__.py +2 -1
- voxcity/downloader/gba.py +210 -0
- voxcity/downloader/gee.py +5 -1
- voxcity/downloader/mbfp.py +1 -1
- voxcity/downloader/oemj.py +80 -8
- voxcity/downloader/utils.py +73 -73
- voxcity/errors.py +30 -0
- voxcity/exporter/__init__.py +9 -1
- voxcity/exporter/cityles.py +129 -34
- voxcity/exporter/envimet.py +51 -26
- voxcity/exporter/magicavoxel.py +42 -5
- voxcity/exporter/netcdf.py +27 -0
- voxcity/exporter/obj.py +103 -28
- voxcity/generator/__init__.py +47 -0
- voxcity/generator/api.py +721 -0
- voxcity/generator/grids.py +381 -0
- voxcity/generator/io.py +94 -0
- voxcity/generator/pipeline.py +282 -0
- voxcity/generator/update.py +429 -0
- voxcity/generator/voxelizer.py +392 -0
- voxcity/geoprocessor/__init__.py +75 -6
- voxcity/geoprocessor/conversion.py +153 -0
- voxcity/geoprocessor/draw.py +1488 -1169
- voxcity/geoprocessor/heights.py +199 -0
- voxcity/geoprocessor/io.py +101 -0
- voxcity/geoprocessor/merge_utils.py +91 -0
- voxcity/geoprocessor/mesh.py +26 -10
- voxcity/geoprocessor/network.py +35 -6
- voxcity/geoprocessor/overlap.py +84 -0
- voxcity/geoprocessor/raster/__init__.py +82 -0
- voxcity/geoprocessor/raster/buildings.py +435 -0
- voxcity/geoprocessor/raster/canopy.py +258 -0
- voxcity/geoprocessor/raster/core.py +150 -0
- voxcity/geoprocessor/raster/export.py +93 -0
- voxcity/geoprocessor/raster/landcover.py +159 -0
- voxcity/geoprocessor/raster/raster.py +110 -0
- voxcity/geoprocessor/selection.py +85 -0
- voxcity/geoprocessor/utils.py +824 -820
- voxcity/models.py +113 -0
- voxcity/simulator/common/__init__.py +22 -0
- voxcity/simulator/common/geometry.py +98 -0
- voxcity/simulator/common/raytracing.py +450 -0
- voxcity/simulator/solar/__init__.py +66 -0
- voxcity/simulator/solar/integration.py +336 -0
- voxcity/simulator/solar/kernels.py +62 -0
- voxcity/simulator/solar/radiation.py +648 -0
- voxcity/simulator/solar/sky.py +668 -0
- voxcity/simulator/solar/temporal.py +792 -0
- voxcity/simulator/view.py +36 -2286
- voxcity/simulator/visibility/__init__.py +29 -0
- voxcity/simulator/visibility/landmark.py +392 -0
- voxcity/simulator/visibility/view.py +508 -0
- voxcity/utils/__init__.py +11 -0
- voxcity/utils/classes.py +194 -0
- voxcity/utils/lc.py +80 -39
- voxcity/utils/logging.py +61 -0
- voxcity/utils/orientation.py +51 -0
- voxcity/utils/shape.py +230 -0
- voxcity/utils/weather/__init__.py +26 -0
- voxcity/utils/weather/epw.py +146 -0
- voxcity/utils/weather/files.py +36 -0
- voxcity/utils/weather/onebuilding.py +486 -0
- voxcity/visualizer/__init__.py +24 -0
- voxcity/visualizer/builder.py +43 -0
- voxcity/visualizer/grids.py +141 -0
- voxcity/visualizer/maps.py +187 -0
- voxcity/visualizer/palette.py +228 -0
- voxcity/visualizer/renderer.py +1145 -0
- {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/METADATA +162 -48
- voxcity-1.0.2.dist-info/RECORD +81 -0
- voxcity/generator.py +0 -1302
- voxcity/geoprocessor/grid.py +0 -1739
- voxcity/geoprocessor/polygon.py +0 -1344
- voxcity/simulator/solar.py +0 -2339
- voxcity/utils/visualization.py +0 -2849
- voxcity/utils/weather.py +0 -1038
- voxcity-0.6.26.dist-info/RECORD +0 -38
- {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/WHEEL +0 -0
- {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/licenses/LICENSE +0 -0
voxcity/simulator/solar.py
DELETED
|
@@ -1,2339 +0,0 @@
|
|
|
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
|
-
|
|
20
|
-
import numpy as np
|
|
21
|
-
import pandas as pd
|
|
22
|
-
import matplotlib.pyplot as plt
|
|
23
|
-
from numba import njit, prange
|
|
24
|
-
import os
|
|
25
|
-
import numba
|
|
26
|
-
from datetime import datetime, timezone
|
|
27
|
-
import pytz
|
|
28
|
-
from astral import Observer
|
|
29
|
-
from astral.sun import elevation, azimuth
|
|
30
|
-
|
|
31
|
-
# Import custom modules for view analysis and weather data processing
|
|
32
|
-
from .view import trace_ray_generic, compute_vi_map_generic, get_sky_view_factor_map, get_surface_view_factor
|
|
33
|
-
from ..utils.weather import get_nearest_epw_from_climate_onebuilding, read_epw_for_solar_simulation
|
|
34
|
-
from ..exporter.obj import grid_to_obj, export_obj
|
|
35
|
-
|
|
36
|
-
@njit(parallel=True)
|
|
37
|
-
def compute_direct_solar_irradiance_map_binary(voxel_data, sun_direction, view_point_height, hit_values, meshsize, tree_k, tree_lad, inclusion_mode):
|
|
38
|
-
"""
|
|
39
|
-
Compute a map of direct solar irradiation accounting for tree transmittance.
|
|
40
|
-
|
|
41
|
-
This function performs ray tracing from observer positions on the ground surface
|
|
42
|
-
towards the sun to determine direct solar irradiance at each location. It accounts
|
|
43
|
-
for shadows cast by buildings and vegetation, with special consideration for
|
|
44
|
-
tree transmittance using the Beer-Lambert law.
|
|
45
|
-
|
|
46
|
-
The function:
|
|
47
|
-
1. Places observers at valid locations (empty voxels above ground)
|
|
48
|
-
2. Casts rays from each observer in the sun direction
|
|
49
|
-
3. Computes transmittance through trees using Beer-Lambert law
|
|
50
|
-
4. Returns a 2D map of transmittance values
|
|
51
|
-
|
|
52
|
-
Observer Placement Rules:
|
|
53
|
-
- Observers are placed in empty voxels (value 0 or -2 for trees) above solid ground
|
|
54
|
-
- Observers are NOT placed on buildings, vegetation, or water surfaces
|
|
55
|
-
- Observer height is added above the detected ground surface
|
|
56
|
-
|
|
57
|
-
Ray Tracing Process:
|
|
58
|
-
- Rays are cast from each valid observer position toward the sun
|
|
59
|
-
- Intersections with obstacles (non-sky voxels) are detected
|
|
60
|
-
- Tree voxels provide partial transmittance rather than complete blocking
|
|
61
|
-
- Final transmittance value represents solar energy reaching the surface
|
|
62
|
-
|
|
63
|
-
Args:
|
|
64
|
-
voxel_data (ndarray): 3D array of voxel values representing the urban environment.
|
|
65
|
-
Common values: 0=sky, 1-6=buildings, 7-9=special surfaces, -2=trees
|
|
66
|
-
sun_direction (tuple): Direction vector of the sun (dx, dy, dz), should be normalized
|
|
67
|
-
view_point_height (float): Observer height above ground surface in meters
|
|
68
|
-
hit_values (tuple): Values considered non-obstacles if inclusion_mode=False
|
|
69
|
-
Typically (0,) meaning only sky voxels are transparent
|
|
70
|
-
meshsize (float): Size of each voxel in meters (spatial resolution)
|
|
71
|
-
tree_k (float): Tree extinction coefficient for Beer-Lambert law (higher = more opaque)
|
|
72
|
-
tree_lad (float): Leaf area density in m^-1 (affects light attenuation through trees)
|
|
73
|
-
inclusion_mode (bool): False here, meaning any voxel not in hit_values is an obstacle
|
|
74
|
-
|
|
75
|
-
Returns:
|
|
76
|
-
ndarray: 2D array of transmittance values (0.0-1.0)
|
|
77
|
-
- 1.0 = full sun exposure
|
|
78
|
-
- 0.0 = complete shadow
|
|
79
|
-
- 0.0-1.0 = partial transmittance through trees
|
|
80
|
-
- NaN = invalid observer position (cannot place observer)
|
|
81
|
-
|
|
82
|
-
Note:
|
|
83
|
-
The returned map is vertically flipped to match standard visualization conventions
|
|
84
|
-
where the origin is at the bottom-left corner.
|
|
85
|
-
"""
|
|
86
|
-
|
|
87
|
-
# Convert observer height from meters to voxel units
|
|
88
|
-
view_height_voxel = int(view_point_height / meshsize)
|
|
89
|
-
|
|
90
|
-
# Get dimensions of the voxel grid
|
|
91
|
-
nx, ny, nz = voxel_data.shape
|
|
92
|
-
|
|
93
|
-
# Initialize irradiance map with NaN (invalid positions)
|
|
94
|
-
irradiance_map = np.full((nx, ny), np.nan, dtype=np.float64)
|
|
95
|
-
|
|
96
|
-
# Normalize sun direction vector for consistent ray tracing
|
|
97
|
-
# This ensures rays travel at unit speed through the voxel grid
|
|
98
|
-
sd = np.array(sun_direction, dtype=np.float64)
|
|
99
|
-
sd_len = np.sqrt(sd[0]**2 + sd[1]**2 + sd[2]**2)
|
|
100
|
-
if sd_len == 0.0:
|
|
101
|
-
return np.flipud(irradiance_map)
|
|
102
|
-
sd /= sd_len
|
|
103
|
-
|
|
104
|
-
# Process each x,y position in parallel for performance
|
|
105
|
-
# This is the main computational loop optimized with numba
|
|
106
|
-
for x in prange(nx):
|
|
107
|
-
for y in range(ny):
|
|
108
|
-
found_observer = False
|
|
109
|
-
|
|
110
|
-
# Search upward through the vertical column to find valid observer position
|
|
111
|
-
# Start from z=1 to ensure we can check the voxel below
|
|
112
|
-
for z in range(1, nz):
|
|
113
|
-
|
|
114
|
-
# Check if current voxel is empty/tree and voxel below is solid
|
|
115
|
-
# This identifies the ground surface where observers can be placed
|
|
116
|
-
if voxel_data[x, y, z] in (0, -2) and voxel_data[x, y, z - 1] not in (0, -2):
|
|
117
|
-
|
|
118
|
-
# Skip if standing on building/vegetation/water surfaces
|
|
119
|
-
# These are considered invalid observer locations
|
|
120
|
-
if (voxel_data[x, y, z - 1] in (7, 8, 9)) or (voxel_data[x, y, z - 1] < 0):
|
|
121
|
-
irradiance_map[x, y] = np.nan
|
|
122
|
-
found_observer = True
|
|
123
|
-
break
|
|
124
|
-
else:
|
|
125
|
-
# Place observer at valid ground location and cast ray toward sun
|
|
126
|
-
observer_location = np.array([x, y, z + view_height_voxel], dtype=np.float64)
|
|
127
|
-
|
|
128
|
-
# Trace ray from observer to sun, accounting for obstacles and tree transmittance
|
|
129
|
-
hit, transmittance = trace_ray_generic(voxel_data, observer_location, sd,
|
|
130
|
-
hit_values, meshsize, tree_k, tree_lad, inclusion_mode)
|
|
131
|
-
|
|
132
|
-
# Store transmittance value (0 if hit solid obstacle, 0-1 if through trees)
|
|
133
|
-
irradiance_map[x, y] = transmittance if not hit else 0.0
|
|
134
|
-
found_observer = True
|
|
135
|
-
break
|
|
136
|
-
|
|
137
|
-
# If no valid observer position found in this column, mark as invalid
|
|
138
|
-
if not found_observer:
|
|
139
|
-
irradiance_map[x, y] = np.nan
|
|
140
|
-
|
|
141
|
-
# Flip map vertically to match visualization conventions (origin at bottom-left)
|
|
142
|
-
return np.flipud(irradiance_map)
|
|
143
|
-
|
|
144
|
-
def get_direct_solar_irradiance_map(voxel_data, meshsize, azimuth_degrees_ori, elevation_degrees,
|
|
145
|
-
direct_normal_irradiance, show_plot=False, **kwargs):
|
|
146
|
-
"""
|
|
147
|
-
Compute direct solar irradiance map with tree transmittance.
|
|
148
|
-
|
|
149
|
-
This function converts solar angles to a 3D direction vector, computes the binary
|
|
150
|
-
transmittance map using ray tracing, and scales the results by actual solar irradiance
|
|
151
|
-
values to produce physically meaningful irradiance measurements.
|
|
152
|
-
|
|
153
|
-
Solar Geometry:
|
|
154
|
-
- Azimuth: Horizontal angle measured from North (0°) clockwise to East (90°)
|
|
155
|
-
- Elevation: Vertical angle above the horizon (0° = horizon, 90° = zenith)
|
|
156
|
-
- The coordinate system is adjusted by 180° to match the voxel grid orientation
|
|
157
|
-
|
|
158
|
-
Physics Background:
|
|
159
|
-
- Direct Normal Irradiance (DNI): Solar energy on a surface perpendicular to sun rays
|
|
160
|
-
- Horizontal irradiance: DNI scaled by sine of elevation angle
|
|
161
|
-
- Tree transmittance: Applied using Beer-Lambert law for realistic light attenuation
|
|
162
|
-
|
|
163
|
-
The function:
|
|
164
|
-
1. Converts sun angles to direction vector using spherical coordinates
|
|
165
|
-
2. Computes binary transmittance map accounting for shadows and tree effects
|
|
166
|
-
3. Scales by direct normal irradiance and sun elevation for horizontal surfaces
|
|
167
|
-
4. Optionally visualizes and exports results in various formats
|
|
168
|
-
|
|
169
|
-
Args:
|
|
170
|
-
voxel_data (ndarray): 3D array of voxel values representing the urban environment
|
|
171
|
-
meshsize (float): Size of each voxel in meters (spatial resolution)
|
|
172
|
-
azimuth_degrees_ori (float): Sun azimuth angle in degrees (0° = North, 90° = East)
|
|
173
|
-
elevation_degrees (float): Sun elevation angle in degrees above horizon (0-90°)
|
|
174
|
-
direct_normal_irradiance (float): Direct normal irradiance in W/m² (from weather data)
|
|
175
|
-
show_plot (bool): Whether to display visualization of results
|
|
176
|
-
**kwargs: Additional arguments including:
|
|
177
|
-
- view_point_height (float): Observer height in meters (default: 1.5)
|
|
178
|
-
Height above ground where irradiance is measured
|
|
179
|
-
- colormap (str): Matplotlib colormap name for visualization (default: 'magma')
|
|
180
|
-
- vmin (float): Minimum value for colormap scaling
|
|
181
|
-
- vmax (float): Maximum value for colormap scaling
|
|
182
|
-
- tree_k (float): Tree extinction coefficient (default: 0.6)
|
|
183
|
-
Higher values mean trees block more light
|
|
184
|
-
- tree_lad (float): Leaf area density in m^-1 (default: 1.0)
|
|
185
|
-
Affects light attenuation through tree canopies
|
|
186
|
-
- obj_export (bool): Whether to export results as 3D OBJ file
|
|
187
|
-
- output_directory (str): Directory for file exports
|
|
188
|
-
- output_file_name (str): Base filename for exports
|
|
189
|
-
- dem_grid (ndarray): Digital elevation model for 3D export
|
|
190
|
-
- num_colors (int): Number of discrete colors for OBJ export
|
|
191
|
-
- alpha (float): Transparency value for 3D visualization
|
|
192
|
-
|
|
193
|
-
Returns:
|
|
194
|
-
ndarray: 2D array of direct solar irradiance values in W/m²
|
|
195
|
-
- Values represent energy flux on horizontal surfaces
|
|
196
|
-
- NaN indicates invalid measurement locations
|
|
197
|
-
- Range typically 0 to direct_normal_irradiance * sin(elevation)
|
|
198
|
-
|
|
199
|
-
Note:
|
|
200
|
-
The azimuth is internally adjusted by 180° to match the coordinate system
|
|
201
|
-
where the voxel grid's y-axis points in the opposite direction from geographic north.
|
|
202
|
-
"""
|
|
203
|
-
# Extract parameters with defaults for observer and visualization settings
|
|
204
|
-
view_point_height = kwargs.get("view_point_height", 1.5)
|
|
205
|
-
colormap = kwargs.get("colormap", 'magma')
|
|
206
|
-
vmin = kwargs.get("vmin", 0.0)
|
|
207
|
-
vmax = kwargs.get("vmax", direct_normal_irradiance)
|
|
208
|
-
|
|
209
|
-
# Get tree transmittance parameters for Beer-Lambert law calculations
|
|
210
|
-
tree_k = kwargs.get("tree_k", 0.6)
|
|
211
|
-
tree_lad = kwargs.get("tree_lad", 1.0)
|
|
212
|
-
|
|
213
|
-
# Convert sun angles to 3D direction vector using spherical coordinates
|
|
214
|
-
# Note: azimuth is adjusted by 180° to match coordinate system orientation
|
|
215
|
-
azimuth_degrees = 180 - azimuth_degrees_ori
|
|
216
|
-
azimuth_radians = np.deg2rad(azimuth_degrees)
|
|
217
|
-
elevation_radians = np.deg2rad(elevation_degrees)
|
|
218
|
-
|
|
219
|
-
# Calculate direction vector components
|
|
220
|
-
# dx, dy: horizontal components, dz: vertical component (upward positive)
|
|
221
|
-
dx = np.cos(elevation_radians) * np.cos(azimuth_radians)
|
|
222
|
-
dy = np.cos(elevation_radians) * np.sin(azimuth_radians)
|
|
223
|
-
dz = np.sin(elevation_radians)
|
|
224
|
-
sun_direction = (dx, dy, dz)
|
|
225
|
-
|
|
226
|
-
# Define obstacle detection parameters for ray tracing
|
|
227
|
-
# All non-zero voxels are obstacles except for trees which have transmittance
|
|
228
|
-
hit_values = (0,) # Only sky voxels (value 0) are transparent
|
|
229
|
-
inclusion_mode = False # Values NOT in hit_values are considered obstacles
|
|
230
|
-
|
|
231
|
-
# Compute transmittance map using optimized ray tracing
|
|
232
|
-
transmittance_map = compute_direct_solar_irradiance_map_binary(
|
|
233
|
-
voxel_data, sun_direction, view_point_height, hit_values,
|
|
234
|
-
meshsize, tree_k, tree_lad, inclusion_mode
|
|
235
|
-
)
|
|
236
|
-
|
|
237
|
-
# Scale transmittance by solar irradiance and geometry
|
|
238
|
-
# For horizontal surfaces: multiply by sine of elevation angle
|
|
239
|
-
sin_elev = dz
|
|
240
|
-
direct_map = transmittance_map * direct_normal_irradiance * sin_elev
|
|
241
|
-
|
|
242
|
-
# Optional visualization of results
|
|
243
|
-
if show_plot:
|
|
244
|
-
# Set up colormap with special handling for invalid data
|
|
245
|
-
cmap = plt.cm.get_cmap(colormap).copy()
|
|
246
|
-
cmap.set_bad(color='lightgray') # NaN values shown in gray
|
|
247
|
-
|
|
248
|
-
plt.figure(figsize=(10, 8))
|
|
249
|
-
# plt.title("Horizontal Direct Solar Irradiance Map (0° = North)")
|
|
250
|
-
plt.imshow(direct_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
|
|
251
|
-
plt.colorbar(label='Direct Solar Irradiance (W/m²)')
|
|
252
|
-
plt.axis('off')
|
|
253
|
-
plt.show()
|
|
254
|
-
|
|
255
|
-
# Optional export to 3D OBJ format for external visualization
|
|
256
|
-
obj_export = kwargs.get("obj_export", False)
|
|
257
|
-
if obj_export:
|
|
258
|
-
# Get export parameters with defaults
|
|
259
|
-
dem_grid = kwargs.get("dem_grid", np.zeros_like(direct_map))
|
|
260
|
-
output_dir = kwargs.get("output_directory", "output")
|
|
261
|
-
output_file_name = kwargs.get("output_file_name", "direct_solar_irradiance")
|
|
262
|
-
num_colors = kwargs.get("num_colors", 10)
|
|
263
|
-
alpha = kwargs.get("alpha", 1.0)
|
|
264
|
-
|
|
265
|
-
# Export as colored 3D mesh
|
|
266
|
-
grid_to_obj(
|
|
267
|
-
direct_map,
|
|
268
|
-
dem_grid,
|
|
269
|
-
output_dir,
|
|
270
|
-
output_file_name,
|
|
271
|
-
meshsize,
|
|
272
|
-
view_point_height,
|
|
273
|
-
colormap_name=colormap,
|
|
274
|
-
num_colors=num_colors,
|
|
275
|
-
alpha=alpha,
|
|
276
|
-
vmin=vmin,
|
|
277
|
-
vmax=vmax
|
|
278
|
-
)
|
|
279
|
-
|
|
280
|
-
return direct_map
|
|
281
|
-
|
|
282
|
-
def get_diffuse_solar_irradiance_map(voxel_data, meshsize, diffuse_irradiance=1.0, show_plot=False, **kwargs):
|
|
283
|
-
"""
|
|
284
|
-
Compute diffuse solar irradiance map using the Sky View Factor (SVF) with tree transmittance.
|
|
285
|
-
|
|
286
|
-
This function calculates the diffuse component of solar radiation, which consists of
|
|
287
|
-
sunlight scattered by the atmosphere and reaches surfaces from all directions across
|
|
288
|
-
the sky hemisphere. The calculation is based on the Sky View Factor (SVF), which
|
|
289
|
-
quantifies how much of the sky dome is visible from each location.
|
|
290
|
-
|
|
291
|
-
Physics Background:
|
|
292
|
-
- Diffuse radiation: Solar energy scattered by atmospheric particles and clouds
|
|
293
|
-
- Sky View Factor (SVF): Fraction of sky hemisphere visible from a point (0.0 to 1.0)
|
|
294
|
-
- Isotropic sky model: Assumes uniform diffuse radiation distribution across the sky
|
|
295
|
-
- Tree effects: Partial transmittance through canopies reduces effective sky visibility
|
|
296
|
-
|
|
297
|
-
SVF Characteristics:
|
|
298
|
-
- SVF = 1.0: Completely open sky (maximum diffuse radiation)
|
|
299
|
-
- SVF = 0.0: Completely blocked sky (no diffuse radiation)
|
|
300
|
-
- SVF = 0.5: Half of sky visible (typical for urban canyons)
|
|
301
|
-
- Trees reduce SVF through partial light attenuation rather than complete blocking
|
|
302
|
-
|
|
303
|
-
The function:
|
|
304
|
-
1. Computes SVF map accounting for building shadows and tree transmittance
|
|
305
|
-
2. Scales SVF by diffuse horizontal irradiance from weather data
|
|
306
|
-
3. Optionally visualizes and exports results for analysis
|
|
307
|
-
|
|
308
|
-
Args:
|
|
309
|
-
voxel_data (ndarray): 3D array of voxel values representing the urban environment
|
|
310
|
-
meshsize (float): Size of each voxel in meters (spatial resolution)
|
|
311
|
-
diffuse_irradiance (float): Diffuse horizontal irradiance in W/m² (from weather data)
|
|
312
|
-
Default 1.0 for normalized calculations
|
|
313
|
-
show_plot (bool): Whether to display visualization of results
|
|
314
|
-
**kwargs: Additional arguments including:
|
|
315
|
-
- view_point_height (float): Observer height in meters (default: 1.5)
|
|
316
|
-
Height above ground where measurements are taken
|
|
317
|
-
- colormap (str): Matplotlib colormap name for visualization (default: 'magma')
|
|
318
|
-
- vmin (float): Minimum value for colormap scaling
|
|
319
|
-
- vmax (float): Maximum value for colormap scaling
|
|
320
|
-
- tree_k (float): Tree extinction coefficient for transmittance calculations
|
|
321
|
-
Higher values mean trees block more diffuse light
|
|
322
|
-
- tree_lad (float): Leaf area density in m^-1
|
|
323
|
-
Affects light attenuation through tree canopies
|
|
324
|
-
- obj_export (bool): Whether to export results as 3D OBJ file
|
|
325
|
-
- output_directory (str): Directory for file exports
|
|
326
|
-
- output_file_name (str): Base filename for exports
|
|
327
|
-
- dem_grid (ndarray): Digital elevation model for 3D export
|
|
328
|
-
- num_colors (int): Number of discrete colors for OBJ export
|
|
329
|
-
- alpha (float): Transparency value for 3D visualization
|
|
330
|
-
|
|
331
|
-
Returns:
|
|
332
|
-
ndarray: 2D array of diffuse solar irradiance values in W/m²
|
|
333
|
-
- Values represent diffuse energy flux on horizontal surfaces
|
|
334
|
-
- Range: 0.0 to diffuse_irradiance (input parameter)
|
|
335
|
-
- NaN indicates invalid measurement locations
|
|
336
|
-
|
|
337
|
-
Note:
|
|
338
|
-
The SVF calculation internally handles tree transmittance effects, so trees
|
|
339
|
-
contribute partial sky visibility rather than complete obstruction.
|
|
340
|
-
"""
|
|
341
|
-
|
|
342
|
-
# Extract parameters with defaults for observer and visualization settings
|
|
343
|
-
view_point_height = kwargs.get("view_point_height", 1.5)
|
|
344
|
-
colormap = kwargs.get("colormap", 'magma')
|
|
345
|
-
vmin = kwargs.get("vmin", 0.0)
|
|
346
|
-
vmax = kwargs.get("vmax", diffuse_irradiance)
|
|
347
|
-
|
|
348
|
-
# Prepare parameters for SVF calculation with appropriate visualization settings
|
|
349
|
-
# Pass tree transmittance parameters to SVF calculation
|
|
350
|
-
svf_kwargs = kwargs.copy()
|
|
351
|
-
svf_kwargs["colormap"] = "BuPu_r" # Purple colormap for SVF visualization
|
|
352
|
-
svf_kwargs["vmin"] = 0 # SVF ranges from 0 to 1
|
|
353
|
-
svf_kwargs["vmax"] = 1
|
|
354
|
-
|
|
355
|
-
# Calculate Sky View Factor map accounting for all obstructions
|
|
356
|
-
# SVF calculation now handles tree transmittance internally
|
|
357
|
-
SVF_map = get_sky_view_factor_map(voxel_data, meshsize, **svf_kwargs)
|
|
358
|
-
|
|
359
|
-
# Convert SVF to diffuse irradiance by scaling with weather data
|
|
360
|
-
# Each location receives diffuse radiation proportional to its sky visibility
|
|
361
|
-
diffuse_map = SVF_map * diffuse_irradiance
|
|
362
|
-
|
|
363
|
-
# Optional visualization of diffuse irradiance results
|
|
364
|
-
if show_plot:
|
|
365
|
-
# Use parameters from kwargs for consistent visualization
|
|
366
|
-
vmin = kwargs.get("vmin", 0.0)
|
|
367
|
-
vmax = kwargs.get("vmax", diffuse_irradiance)
|
|
368
|
-
|
|
369
|
-
# Set up colormap with special handling for invalid data
|
|
370
|
-
cmap = plt.cm.get_cmap(colormap).copy()
|
|
371
|
-
cmap.set_bad(color='lightgray') # NaN values shown in gray
|
|
372
|
-
|
|
373
|
-
plt.figure(figsize=(10, 8))
|
|
374
|
-
# plt.title("Diffuse Solar Irradiance Map")
|
|
375
|
-
plt.imshow(diffuse_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
|
|
376
|
-
plt.colorbar(label='Diffuse Solar Irradiance (W/m²)')
|
|
377
|
-
plt.axis('off')
|
|
378
|
-
plt.show()
|
|
379
|
-
|
|
380
|
-
# Optional export to 3D OBJ format for external visualization
|
|
381
|
-
obj_export = kwargs.get("obj_export", False)
|
|
382
|
-
if obj_export:
|
|
383
|
-
# Get export parameters with defaults
|
|
384
|
-
dem_grid = kwargs.get("dem_grid", np.zeros_like(diffuse_map))
|
|
385
|
-
output_dir = kwargs.get("output_directory", "output")
|
|
386
|
-
output_file_name = kwargs.get("output_file_name", "diffuse_solar_irradiance")
|
|
387
|
-
num_colors = kwargs.get("num_colors", 10)
|
|
388
|
-
alpha = kwargs.get("alpha", 1.0)
|
|
389
|
-
|
|
390
|
-
# Export as colored 3D mesh
|
|
391
|
-
grid_to_obj(
|
|
392
|
-
diffuse_map,
|
|
393
|
-
dem_grid,
|
|
394
|
-
output_dir,
|
|
395
|
-
output_file_name,
|
|
396
|
-
meshsize,
|
|
397
|
-
view_point_height,
|
|
398
|
-
colormap_name=colormap,
|
|
399
|
-
num_colors=num_colors,
|
|
400
|
-
alpha=alpha,
|
|
401
|
-
vmin=vmin,
|
|
402
|
-
vmax=vmax
|
|
403
|
-
)
|
|
404
|
-
|
|
405
|
-
return diffuse_map
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
def get_global_solar_irradiance_map(
|
|
409
|
-
voxel_data,
|
|
410
|
-
meshsize,
|
|
411
|
-
azimuth_degrees,
|
|
412
|
-
elevation_degrees,
|
|
413
|
-
direct_normal_irradiance,
|
|
414
|
-
diffuse_irradiance,
|
|
415
|
-
show_plot=False,
|
|
416
|
-
**kwargs
|
|
417
|
-
):
|
|
418
|
-
"""
|
|
419
|
-
Compute global solar irradiance (direct + diffuse) on a horizontal plane at each valid observer location.
|
|
420
|
-
|
|
421
|
-
This function combines both direct and diffuse components of solar radiation to calculate
|
|
422
|
-
the total solar irradiance at each location. Global horizontal irradiance (GHI) is the
|
|
423
|
-
most commonly used metric for solar energy assessment and represents the total solar
|
|
424
|
-
energy available on a horizontal surface.
|
|
425
|
-
|
|
426
|
-
Global Irradiance Components:
|
|
427
|
-
- Direct component: Solar radiation from the sun's disk, affected by shadows and obstacles
|
|
428
|
-
- Diffuse component: Solar radiation scattered by the atmosphere, affected by sky view
|
|
429
|
-
- Total irradiance: Sum of direct and diffuse components at each location
|
|
430
|
-
|
|
431
|
-
Physical Considerations:
|
|
432
|
-
- Direct radiation varies with sun position and local obstructions
|
|
433
|
-
- Diffuse radiation varies with sky visibility (Sky View Factor)
|
|
434
|
-
- Both components are affected by tree transmittance using Beer-Lambert law
|
|
435
|
-
- Invalid locations (e.g., on water, buildings) are marked as NaN
|
|
436
|
-
|
|
437
|
-
The function:
|
|
438
|
-
1. Computes direct solar irradiance map accounting for sun position and shadows
|
|
439
|
-
2. Computes diffuse solar irradiance map based on Sky View Factor
|
|
440
|
-
3. Combines maps and optionally visualizes/exports results for analysis
|
|
441
|
-
|
|
442
|
-
Args:
|
|
443
|
-
voxel_data (ndarray): 3D voxel array representing the urban environment
|
|
444
|
-
meshsize (float): Voxel size in meters (spatial resolution)
|
|
445
|
-
azimuth_degrees (float): Sun azimuth angle in degrees (0° = North, 90° = East)
|
|
446
|
-
elevation_degrees (float): Sun elevation angle in degrees above horizon (0-90°)
|
|
447
|
-
direct_normal_irradiance (float): Direct normal irradiance in W/m² (from weather data)
|
|
448
|
-
diffuse_irradiance (float): Diffuse horizontal irradiance in W/m² (from weather data)
|
|
449
|
-
show_plot (bool): Whether to display visualization of results
|
|
450
|
-
**kwargs: Additional arguments including:
|
|
451
|
-
- view_point_height (float): Observer height in meters (default: 1.5)
|
|
452
|
-
Height above ground where measurements are taken
|
|
453
|
-
- colormap (str): Matplotlib colormap name for visualization (default: 'magma')
|
|
454
|
-
- vmin (float): Minimum value for colormap scaling
|
|
455
|
-
- vmax (float): Maximum value for colormap scaling
|
|
456
|
-
- tree_k (float): Tree extinction coefficient for transmittance calculations
|
|
457
|
-
Higher values mean trees block more light
|
|
458
|
-
- tree_lad (float): Leaf area density in m^-1
|
|
459
|
-
Affects light attenuation through tree canopies
|
|
460
|
-
- obj_export (bool): Whether to export results as 3D OBJ file
|
|
461
|
-
- output_directory (str): Directory for file exports
|
|
462
|
-
- output_file_name (str): Base filename for exports
|
|
463
|
-
- dem_grid (ndarray): Digital elevation model for 3D export
|
|
464
|
-
- num_colors (int): Number of discrete colors for OBJ export
|
|
465
|
-
- alpha (float): Transparency value for 3D visualization
|
|
466
|
-
|
|
467
|
-
Returns:
|
|
468
|
-
ndarray: 2D array of global solar irradiance values in W/m²
|
|
469
|
-
- Values represent total solar energy flux on horizontal surfaces
|
|
470
|
-
- Range: 0.0 to (direct_normal_irradiance * sin(elevation) + diffuse_irradiance)
|
|
471
|
-
- NaN indicates invalid measurement locations
|
|
472
|
-
|
|
473
|
-
Note:
|
|
474
|
-
Global irradiance is the standard metric used for solar energy assessment
|
|
475
|
-
and represents the maximum solar energy available at each location.
|
|
476
|
-
"""
|
|
477
|
-
|
|
478
|
-
# Extract visualization parameters
|
|
479
|
-
colormap = kwargs.get("colormap", 'magma')
|
|
480
|
-
|
|
481
|
-
# Create kwargs for individual component calculations
|
|
482
|
-
# Both direct and diffuse calculations use the same base parameters
|
|
483
|
-
direct_diffuse_kwargs = kwargs.copy()
|
|
484
|
-
direct_diffuse_kwargs.update({
|
|
485
|
-
'show_plot': True, # Show intermediate results for debugging
|
|
486
|
-
'obj_export': False # Don't export intermediate results
|
|
487
|
-
})
|
|
488
|
-
|
|
489
|
-
# Compute direct irradiance component
|
|
490
|
-
# Accounts for sun position, shadows, and tree transmittance
|
|
491
|
-
direct_map = get_direct_solar_irradiance_map(
|
|
492
|
-
voxel_data,
|
|
493
|
-
meshsize,
|
|
494
|
-
azimuth_degrees,
|
|
495
|
-
elevation_degrees,
|
|
496
|
-
direct_normal_irradiance,
|
|
497
|
-
**direct_diffuse_kwargs
|
|
498
|
-
)
|
|
499
|
-
|
|
500
|
-
# Compute diffuse irradiance component
|
|
501
|
-
# Based on Sky View Factor and atmospheric scattering
|
|
502
|
-
diffuse_map = get_diffuse_solar_irradiance_map(
|
|
503
|
-
voxel_data,
|
|
504
|
-
meshsize,
|
|
505
|
-
diffuse_irradiance=diffuse_irradiance,
|
|
506
|
-
**direct_diffuse_kwargs
|
|
507
|
-
)
|
|
508
|
-
|
|
509
|
-
# Sum the two components to get total global irradiance
|
|
510
|
-
# This represents the total solar energy available at each location
|
|
511
|
-
global_map = direct_map + diffuse_map
|
|
512
|
-
|
|
513
|
-
# Determine colormap scaling range from actual data
|
|
514
|
-
vmin = kwargs.get("vmin", np.nanmin(global_map))
|
|
515
|
-
vmax = kwargs.get("vmax", np.nanmax(global_map))
|
|
516
|
-
|
|
517
|
-
# Optional visualization of combined results
|
|
518
|
-
if show_plot:
|
|
519
|
-
# Set up colormap with special handling for invalid data
|
|
520
|
-
cmap = plt.cm.get_cmap(colormap).copy()
|
|
521
|
-
cmap.set_bad(color='lightgray') # NaN values shown in gray
|
|
522
|
-
|
|
523
|
-
plt.figure(figsize=(10, 8))
|
|
524
|
-
# plt.title("Global Solar Irradiance Map")
|
|
525
|
-
plt.imshow(global_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
|
|
526
|
-
plt.colorbar(label='Global Solar Irradiance (W/m²)')
|
|
527
|
-
plt.axis('off')
|
|
528
|
-
plt.show()
|
|
529
|
-
|
|
530
|
-
# Optional export to 3D OBJ format for external visualization
|
|
531
|
-
obj_export = kwargs.get("obj_export", False)
|
|
532
|
-
if obj_export:
|
|
533
|
-
# Get export parameters with defaults
|
|
534
|
-
dem_grid = kwargs.get("dem_grid", np.zeros_like(global_map))
|
|
535
|
-
output_dir = kwargs.get("output_directory", "output")
|
|
536
|
-
output_file_name = kwargs.get("output_file_name", "global_solar_irradiance")
|
|
537
|
-
num_colors = kwargs.get("num_colors", 10)
|
|
538
|
-
alpha = kwargs.get("alpha", 1.0)
|
|
539
|
-
meshsize_param = kwargs.get("meshsize", meshsize)
|
|
540
|
-
view_point_height = kwargs.get("view_point_height", 1.5)
|
|
541
|
-
|
|
542
|
-
# Export as colored 3D mesh
|
|
543
|
-
grid_to_obj(
|
|
544
|
-
global_map,
|
|
545
|
-
dem_grid,
|
|
546
|
-
output_dir,
|
|
547
|
-
output_file_name,
|
|
548
|
-
meshsize_param,
|
|
549
|
-
view_point_height,
|
|
550
|
-
colormap_name=colormap,
|
|
551
|
-
num_colors=num_colors,
|
|
552
|
-
alpha=alpha,
|
|
553
|
-
vmin=vmin,
|
|
554
|
-
vmax=vmax
|
|
555
|
-
)
|
|
556
|
-
|
|
557
|
-
return global_map
|
|
558
|
-
|
|
559
|
-
def get_solar_positions_astral(times, lon, lat):
|
|
560
|
-
"""
|
|
561
|
-
Compute solar azimuth and elevation using Astral for given times and location.
|
|
562
|
-
|
|
563
|
-
This function uses the Astral astronomical library to calculate precise solar positions
|
|
564
|
-
based on location coordinates and timestamps. The calculations account for Earth's
|
|
565
|
-
orbital mechanics, axial tilt, and atmospheric refraction effects.
|
|
566
|
-
|
|
567
|
-
Astronomical Background:
|
|
568
|
-
- Solar position depends on date, time, and geographic location
|
|
569
|
-
- Azimuth: Horizontal angle measured clockwise from North (0°-360°)
|
|
570
|
-
- Elevation: Vertical angle above the horizon (-90° to +90°)
|
|
571
|
-
- Calculations use standard astronomical algorithms (e.g., NREL SPA)
|
|
572
|
-
|
|
573
|
-
Coordinate System:
|
|
574
|
-
- Azimuth: 0° = North, 90° = East, 180° = South, 270° = West
|
|
575
|
-
- Elevation: 0° = horizon, 90° = zenith, negative values = below horizon
|
|
576
|
-
- All angles are in degrees for consistency with weather data formats
|
|
577
|
-
|
|
578
|
-
The function:
|
|
579
|
-
1. Creates an Astral observer at the specified geographic location
|
|
580
|
-
2. Computes sun position for each timestamp in the input array
|
|
581
|
-
3. Returns DataFrame with azimuth and elevation angles for further processing
|
|
582
|
-
|
|
583
|
-
Args:
|
|
584
|
-
times (DatetimeIndex): Array of timezone-aware datetime objects
|
|
585
|
-
Must include timezone information for accurate calculations
|
|
586
|
-
lon (float): Longitude in degrees (positive = East, negative = West)
|
|
587
|
-
Range: -180° to +180°
|
|
588
|
-
lat (float): Latitude in degrees (positive = North, negative = South)
|
|
589
|
-
Range: -90° to +90°
|
|
590
|
-
|
|
591
|
-
Returns:
|
|
592
|
-
DataFrame: DataFrame with columns 'azimuth' and 'elevation' containing solar positions
|
|
593
|
-
- Index: Input timestamps (timezone-aware)
|
|
594
|
-
- 'azimuth': Solar azimuth angles in degrees (0°-360°)
|
|
595
|
-
- 'elevation': Solar elevation angles in degrees (-90° to +90°)
|
|
596
|
-
- All values are float type for numerical calculations
|
|
597
|
-
|
|
598
|
-
Note:
|
|
599
|
-
Input times must be timezone-aware. The function preserves the original
|
|
600
|
-
timezone information and performs calculations in the specified timezone.
|
|
601
|
-
"""
|
|
602
|
-
# Create an astronomical observer at the specified geographic location
|
|
603
|
-
observer = Observer(latitude=lat, longitude=lon)
|
|
604
|
-
|
|
605
|
-
# Initialize result DataFrame with appropriate structure
|
|
606
|
-
df_pos = pd.DataFrame(index=times, columns=['azimuth', 'elevation'], dtype=float)
|
|
607
|
-
|
|
608
|
-
# Calculate solar position for each timestamp
|
|
609
|
-
for t in times:
|
|
610
|
-
# t is already timezone-aware; no need to replace tzinfo
|
|
611
|
-
# Calculate solar elevation (vertical angle above horizon)
|
|
612
|
-
el = elevation(observer=observer, dateandtime=t)
|
|
613
|
-
|
|
614
|
-
# Calculate solar azimuth (horizontal angle from North)
|
|
615
|
-
az = azimuth(observer=observer, dateandtime=t)
|
|
616
|
-
|
|
617
|
-
# Store results in DataFrame
|
|
618
|
-
df_pos.at[t, 'elevation'] = el
|
|
619
|
-
df_pos.at[t, 'azimuth'] = az
|
|
620
|
-
|
|
621
|
-
return df_pos
|
|
622
|
-
|
|
623
|
-
def _configure_num_threads(desired_threads=None, progress=False):
|
|
624
|
-
try:
|
|
625
|
-
cores = os.cpu_count() or 4
|
|
626
|
-
except Exception:
|
|
627
|
-
cores = 4
|
|
628
|
-
used = desired_threads if desired_threads is not None else cores
|
|
629
|
-
try:
|
|
630
|
-
numba.set_num_threads(int(used))
|
|
631
|
-
except Exception:
|
|
632
|
-
pass
|
|
633
|
-
# Best-effort oversubscription guards (only set defaults if unset)
|
|
634
|
-
os.environ.setdefault('MKL_NUM_THREADS', '1')
|
|
635
|
-
if 'OMP_NUM_THREADS' not in os.environ:
|
|
636
|
-
os.environ['OMP_NUM_THREADS'] = str(int(used))
|
|
637
|
-
if progress:
|
|
638
|
-
try:
|
|
639
|
-
print(f"Numba threads: {numba.get_num_threads()} (requested {used})")
|
|
640
|
-
except Exception:
|
|
641
|
-
print(f"Numba threads set to {used}")
|
|
642
|
-
return used
|
|
643
|
-
|
|
644
|
-
def _auto_time_batch_size(n_faces, total_steps, user_value=None):
|
|
645
|
-
if user_value is not None:
|
|
646
|
-
return max(1, int(user_value))
|
|
647
|
-
# Heuristic based on face count
|
|
648
|
-
if total_steps <= 0:
|
|
649
|
-
return 1
|
|
650
|
-
if n_faces <= 5_000:
|
|
651
|
-
batches = 2
|
|
652
|
-
elif n_faces <= 50_000:
|
|
653
|
-
batches = 8
|
|
654
|
-
elif n_faces <= 200_000:
|
|
655
|
-
batches = 16
|
|
656
|
-
else:
|
|
657
|
-
batches = 32
|
|
658
|
-
batches = min(batches, total_steps)
|
|
659
|
-
return max(1, total_steps // batches)
|
|
660
|
-
|
|
661
|
-
def get_cumulative_global_solar_irradiance(
|
|
662
|
-
voxel_data,
|
|
663
|
-
meshsize,
|
|
664
|
-
df, lon, lat, tz,
|
|
665
|
-
direct_normal_irradiance_scaling=1.0,
|
|
666
|
-
diffuse_irradiance_scaling=1.0,
|
|
667
|
-
**kwargs
|
|
668
|
-
):
|
|
669
|
-
"""
|
|
670
|
-
Compute cumulative global solar irradiance over a specified period using data from an EPW file.
|
|
671
|
-
|
|
672
|
-
This function performs time-series analysis of solar irradiance by processing weather data
|
|
673
|
-
over a user-defined period and accumulating irradiance values at each location. The result
|
|
674
|
-
represents the total solar energy received during the specified time period, which is
|
|
675
|
-
essential for seasonal analysis, solar panel positioning, and energy yield predictions.
|
|
676
|
-
|
|
677
|
-
Cumulative Analysis Concept:
|
|
678
|
-
- Instantaneous irradiance (W/m²): Power at a specific moment
|
|
679
|
-
- Cumulative irradiance (Wh/m²): Energy accumulated over time
|
|
680
|
-
- Integration: Sum of (irradiance × time_step) for all timesteps
|
|
681
|
-
- Applications: Annual energy yield, seasonal variations, optimal siting
|
|
682
|
-
|
|
683
|
-
Time Period Processing:
|
|
684
|
-
- Supports flexible time ranges (daily, seasonal, annual analysis)
|
|
685
|
-
- Handles timezone conversions between local and UTC time
|
|
686
|
-
- Filters weather data based on user-specified start/end times
|
|
687
|
-
- Accounts for leap years and varying daylight hours
|
|
688
|
-
|
|
689
|
-
Performance Optimization:
|
|
690
|
-
- Pre-calculates diffuse map once (scales linearly with DHI)
|
|
691
|
-
- Processes direct component for each timestep (varies with sun position)
|
|
692
|
-
- Uses efficient memory management for large time series
|
|
693
|
-
- Provides optional progress monitoring for long calculations
|
|
694
|
-
|
|
695
|
-
The function:
|
|
696
|
-
1. Filters EPW data for specified time period with timezone handling
|
|
697
|
-
2. Computes sun positions for each timestep using astronomical calculations
|
|
698
|
-
3. Calculates and accumulates global irradiance maps over the entire period
|
|
699
|
-
4. Handles tree transmittance and provides visualization/export options
|
|
700
|
-
|
|
701
|
-
Args:
|
|
702
|
-
voxel_data (ndarray): 3D array of voxel values representing the urban environment
|
|
703
|
-
meshsize (float): Size of each voxel in meters (spatial resolution)
|
|
704
|
-
df (DataFrame): EPW weather data with columns 'DNI', 'DHI' and datetime index
|
|
705
|
-
Must include complete meteorological dataset
|
|
706
|
-
lon (float): Longitude in degrees for solar position calculations
|
|
707
|
-
lat (float): Latitude in degrees for solar position calculations
|
|
708
|
-
tz (float): Timezone offset in hours from UTC (positive = East of UTC)
|
|
709
|
-
direct_normal_irradiance_scaling (float): Scaling factor for direct normal irradiance
|
|
710
|
-
Allows sensitivity analysis or unit conversions
|
|
711
|
-
diffuse_irradiance_scaling (float): Scaling factor for diffuse horizontal irradiance
|
|
712
|
-
Allows sensitivity analysis or unit conversions
|
|
713
|
-
**kwargs: Additional arguments including:
|
|
714
|
-
- view_point_height (float): Observer height in meters (default: 1.5)
|
|
715
|
-
Height above ground where measurements are taken
|
|
716
|
-
- start_time (str): Start time in format 'MM-DD HH:MM:SS'
|
|
717
|
-
Defines beginning of analysis period (default: "01-01 05:00:00")
|
|
718
|
-
- end_time (str): End time in format 'MM-DD HH:MM:SS'
|
|
719
|
-
Defines end of analysis period (default: "01-01 20:00:00")
|
|
720
|
-
- tree_k (float): Tree extinction coefficient for transmittance calculations
|
|
721
|
-
Higher values mean trees block more light
|
|
722
|
-
- tree_lad (float): Leaf area density in m^-1
|
|
723
|
-
Affects light attenuation through tree canopies
|
|
724
|
-
- show_plot (bool): Whether to show final accumulated results
|
|
725
|
-
- show_each_timestep (bool): Whether to show plots for each timestep
|
|
726
|
-
Useful for debugging but significantly increases computation time
|
|
727
|
-
- colormap (str): Matplotlib colormap name for visualization
|
|
728
|
-
- vmin (float): Minimum value for colormap scaling
|
|
729
|
-
- vmax (float): Maximum value for colormap scaling
|
|
730
|
-
- obj_export (bool): Whether to export results as 3D OBJ file
|
|
731
|
-
- output_directory (str): Directory for file exports
|
|
732
|
-
- output_file_name (str): Base filename for exports
|
|
733
|
-
- dem_grid (ndarray): Digital elevation model for 3D export
|
|
734
|
-
- num_colors (int): Number of discrete colors for OBJ export
|
|
735
|
-
- alpha (float): Transparency value for 3D visualization
|
|
736
|
-
|
|
737
|
-
Returns:
|
|
738
|
-
ndarray: 2D array of cumulative global solar irradiance values in W/m²·hour
|
|
739
|
-
- Values represent total solar energy received during the analysis period
|
|
740
|
-
- Range depends on period length and local climate conditions
|
|
741
|
-
- NaN indicates invalid measurement locations (e.g., on buildings, water)
|
|
742
|
-
|
|
743
|
-
Note:
|
|
744
|
-
The function efficiently handles large time series by pre-computing the diffuse
|
|
745
|
-
component once and scaling it for each timestep, significantly reducing
|
|
746
|
-
computation time for long-term analysis.
|
|
747
|
-
"""
|
|
748
|
-
# Extract parameters with defaults for observer positioning and visualization
|
|
749
|
-
view_point_height = kwargs.get("view_point_height", 1.5)
|
|
750
|
-
colormap = kwargs.get("colormap", 'magma')
|
|
751
|
-
start_time = kwargs.get("start_time", "01-01 05:00:00")
|
|
752
|
-
end_time = kwargs.get("end_time", "01-01 20:00:00")
|
|
753
|
-
# Optional: configure num threads here as well when called directly
|
|
754
|
-
desired_threads = kwargs.get("numba_num_threads", None)
|
|
755
|
-
progress_report = kwargs.get("progress_report", False)
|
|
756
|
-
_configure_num_threads(desired_threads, progress=progress_report)
|
|
757
|
-
|
|
758
|
-
# Validate input data
|
|
759
|
-
if df.empty:
|
|
760
|
-
raise ValueError("No data in EPW file.")
|
|
761
|
-
|
|
762
|
-
# Parse start and end times without year (supports multi-year analysis)
|
|
763
|
-
try:
|
|
764
|
-
start_dt = datetime.strptime(start_time, "%m-%d %H:%M:%S")
|
|
765
|
-
end_dt = datetime.strptime(end_time, "%m-%d %H:%M:%S")
|
|
766
|
-
except ValueError as ve:
|
|
767
|
-
raise ValueError("start_time and end_time must be in format 'MM-DD HH:MM:SS'") from ve
|
|
768
|
-
|
|
769
|
-
# Add hour of year column for efficient time filtering
|
|
770
|
-
# Hour 1 = January 1st, 00:00; Hour 8760 = December 31st, 23:00
|
|
771
|
-
df['hour_of_year'] = (df.index.dayofyear - 1) * 24 + df.index.hour + 1
|
|
772
|
-
|
|
773
|
-
# Convert parsed dates to day of year and hour for filtering
|
|
774
|
-
start_doy = datetime(2000, start_dt.month, start_dt.day).timetuple().tm_yday
|
|
775
|
-
end_doy = datetime(2000, end_dt.month, end_dt.day).timetuple().tm_yday
|
|
776
|
-
|
|
777
|
-
start_hour = (start_doy - 1) * 24 + start_dt.hour + 1
|
|
778
|
-
end_hour = (end_doy - 1) * 24 + end_dt.hour + 1
|
|
779
|
-
|
|
780
|
-
# Handle period crossing year boundary (e.g., Dec 15 to Jan 15)
|
|
781
|
-
if start_hour <= end_hour:
|
|
782
|
-
# Normal period within single year
|
|
783
|
-
df_period = df[(df['hour_of_year'] >= start_hour) & (df['hour_of_year'] <= end_hour)]
|
|
784
|
-
else:
|
|
785
|
-
# Period crosses year boundary - include end and beginning of year
|
|
786
|
-
df_period = df[(df['hour_of_year'] >= start_hour) | (df['hour_of_year'] <= end_hour)]
|
|
787
|
-
|
|
788
|
-
# Apply minute-level filtering within start/end hours for precision
|
|
789
|
-
df_period = df_period[
|
|
790
|
-
((df_period.index.hour != start_dt.hour) | (df_period.index.minute >= start_dt.minute)) &
|
|
791
|
-
((df_period.index.hour != end_dt.hour) | (df_period.index.minute <= end_dt.minute))
|
|
792
|
-
]
|
|
793
|
-
|
|
794
|
-
# Validate filtered data
|
|
795
|
-
if df_period.empty:
|
|
796
|
-
raise ValueError("No EPW data in the specified period.")
|
|
797
|
-
|
|
798
|
-
# Handle timezone conversion for accurate solar position calculations
|
|
799
|
-
# Convert local time (from EPW) to UTC for astronomical calculations
|
|
800
|
-
offset_minutes = int(tz * 60)
|
|
801
|
-
local_tz = pytz.FixedOffset(offset_minutes)
|
|
802
|
-
df_period_local = df_period.copy()
|
|
803
|
-
df_period_local.index = df_period_local.index.tz_localize(local_tz)
|
|
804
|
-
df_period_utc = df_period_local.tz_convert(pytz.UTC)
|
|
805
|
-
|
|
806
|
-
# Compute solar positions for entire analysis period
|
|
807
|
-
# This is done once to optimize performance
|
|
808
|
-
solar_positions = get_solar_positions_astral(df_period_utc.index, lon, lat)
|
|
809
|
-
|
|
810
|
-
# Prepare parameters for efficient diffuse irradiance calculation
|
|
811
|
-
# Create kwargs for diffuse calculation with visualization disabled
|
|
812
|
-
diffuse_kwargs = kwargs.copy()
|
|
813
|
-
diffuse_kwargs.update({
|
|
814
|
-
'show_plot': False,
|
|
815
|
-
'obj_export': False
|
|
816
|
-
})
|
|
817
|
-
|
|
818
|
-
# Pre-compute base diffuse map once with unit irradiance
|
|
819
|
-
# This map will be scaled by actual DHI values for each timestep
|
|
820
|
-
base_diffuse_map = get_diffuse_solar_irradiance_map(
|
|
821
|
-
voxel_data,
|
|
822
|
-
meshsize,
|
|
823
|
-
diffuse_irradiance=1.0,
|
|
824
|
-
**diffuse_kwargs
|
|
825
|
-
)
|
|
826
|
-
|
|
827
|
-
# Initialize accumulation arrays for energy integration
|
|
828
|
-
cumulative_map = np.zeros((voxel_data.shape[0], voxel_data.shape[1]))
|
|
829
|
-
mask_map = np.ones((voxel_data.shape[0], voxel_data.shape[1]), dtype=bool)
|
|
830
|
-
|
|
831
|
-
# Prepare parameters for direct irradiance calculations
|
|
832
|
-
# Create kwargs for direct calculation with visualization disabled
|
|
833
|
-
direct_kwargs = kwargs.copy()
|
|
834
|
-
direct_kwargs.update({
|
|
835
|
-
'show_plot': False,
|
|
836
|
-
'view_point_height': view_point_height,
|
|
837
|
-
'obj_export': False
|
|
838
|
-
})
|
|
839
|
-
|
|
840
|
-
# Main processing loop: iterate through each timestep in the analysis period
|
|
841
|
-
for idx, (time_utc, row) in enumerate(df_period_utc.iterrows()):
|
|
842
|
-
# Apply scaling factors to weather data
|
|
843
|
-
# Allows for sensitivity analysis or unit conversions
|
|
844
|
-
DNI = row['DNI'] * direct_normal_irradiance_scaling
|
|
845
|
-
DHI = row['DHI'] * diffuse_irradiance_scaling
|
|
846
|
-
time_local = df_period_local.index[idx]
|
|
847
|
-
|
|
848
|
-
# Get solar position for timestep
|
|
849
|
-
solpos = solar_positions.loc[time_utc]
|
|
850
|
-
azimuth_degrees = solpos['azimuth']
|
|
851
|
-
elevation_degrees = solpos['elevation']
|
|
852
|
-
|
|
853
|
-
# Compute direct irradiance map with transmittance
|
|
854
|
-
direct_map = get_direct_solar_irradiance_map(
|
|
855
|
-
voxel_data,
|
|
856
|
-
meshsize,
|
|
857
|
-
azimuth_degrees,
|
|
858
|
-
elevation_degrees,
|
|
859
|
-
direct_normal_irradiance=DNI,
|
|
860
|
-
**direct_kwargs
|
|
861
|
-
)
|
|
862
|
-
|
|
863
|
-
# Scale base diffuse map by actual DHI
|
|
864
|
-
diffuse_map = base_diffuse_map * DHI
|
|
865
|
-
|
|
866
|
-
# Combine direct and diffuse components
|
|
867
|
-
global_map = direct_map + diffuse_map
|
|
868
|
-
|
|
869
|
-
# Update valid pixel mask
|
|
870
|
-
mask_map &= ~np.isnan(global_map)
|
|
871
|
-
|
|
872
|
-
# Replace NaN with 0 for accumulation
|
|
873
|
-
global_map_filled = np.nan_to_num(global_map, nan=0.0)
|
|
874
|
-
cumulative_map += global_map_filled
|
|
875
|
-
|
|
876
|
-
# Optional timestep visualization
|
|
877
|
-
show_each_timestep = kwargs.get("show_each_timestep", False)
|
|
878
|
-
if show_each_timestep:
|
|
879
|
-
colormap = kwargs.get("colormap", 'viridis')
|
|
880
|
-
vmin = kwargs.get("vmin", 0.0)
|
|
881
|
-
vmax = kwargs.get("vmax", max(direct_normal_irradiance_scaling, diffuse_irradiance_scaling) * 1000)
|
|
882
|
-
cmap = plt.cm.get_cmap(colormap).copy()
|
|
883
|
-
cmap.set_bad(color='lightgray')
|
|
884
|
-
plt.figure(figsize=(10, 8))
|
|
885
|
-
# plt.title(f"Global Solar Irradiance at {time_local.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
886
|
-
plt.imshow(global_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
|
|
887
|
-
plt.axis('off')
|
|
888
|
-
plt.colorbar(label='Global Solar Irradiance (W/m²)')
|
|
889
|
-
plt.show()
|
|
890
|
-
|
|
891
|
-
# Apply mask to final result
|
|
892
|
-
cumulative_map[~mask_map] = np.nan
|
|
893
|
-
|
|
894
|
-
# Final visualization
|
|
895
|
-
show_plot = kwargs.get("show_plot", True)
|
|
896
|
-
if show_plot:
|
|
897
|
-
colormap = kwargs.get("colormap", 'magma')
|
|
898
|
-
vmin = kwargs.get("vmin", np.nanmin(cumulative_map))
|
|
899
|
-
vmax = kwargs.get("vmax", np.nanmax(cumulative_map))
|
|
900
|
-
cmap = plt.cm.get_cmap(colormap).copy()
|
|
901
|
-
cmap.set_bad(color='lightgray')
|
|
902
|
-
plt.figure(figsize=(10, 8))
|
|
903
|
-
# plt.title("Cumulative Global Solar Irradiance Map")
|
|
904
|
-
plt.imshow(cumulative_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
|
|
905
|
-
plt.colorbar(label='Cumulative Global Solar Irradiance (W/m²·hour)')
|
|
906
|
-
plt.axis('off')
|
|
907
|
-
plt.show()
|
|
908
|
-
|
|
909
|
-
# Optional OBJ export
|
|
910
|
-
obj_export = kwargs.get("obj_export", False)
|
|
911
|
-
if obj_export:
|
|
912
|
-
colormap = kwargs.get("colormap", "magma")
|
|
913
|
-
vmin = kwargs.get("vmin", np.nanmin(cumulative_map))
|
|
914
|
-
vmax = kwargs.get("vmax", np.nanmax(cumulative_map))
|
|
915
|
-
dem_grid = kwargs.get("dem_grid", np.zeros_like(cumulative_map))
|
|
916
|
-
output_dir = kwargs.get("output_directory", "output")
|
|
917
|
-
output_file_name = kwargs.get("output_file_name", "cummurative_global_solar_irradiance")
|
|
918
|
-
num_colors = kwargs.get("num_colors", 10)
|
|
919
|
-
alpha = kwargs.get("alpha", 1.0)
|
|
920
|
-
grid_to_obj(
|
|
921
|
-
cumulative_map,
|
|
922
|
-
dem_grid,
|
|
923
|
-
output_dir,
|
|
924
|
-
output_file_name,
|
|
925
|
-
meshsize,
|
|
926
|
-
view_point_height,
|
|
927
|
-
colormap_name=colormap,
|
|
928
|
-
num_colors=num_colors,
|
|
929
|
-
alpha=alpha,
|
|
930
|
-
vmin=vmin,
|
|
931
|
-
vmax=vmax
|
|
932
|
-
)
|
|
933
|
-
|
|
934
|
-
return cumulative_map
|
|
935
|
-
|
|
936
|
-
def get_global_solar_irradiance_using_epw(
|
|
937
|
-
voxel_data,
|
|
938
|
-
meshsize,
|
|
939
|
-
calc_type='instantaneous',
|
|
940
|
-
direct_normal_irradiance_scaling=1.0,
|
|
941
|
-
diffuse_irradiance_scaling=1.0,
|
|
942
|
-
**kwargs
|
|
943
|
-
):
|
|
944
|
-
"""
|
|
945
|
-
Compute global solar irradiance using EPW weather data, either for a single time or cumulatively over a period.
|
|
946
|
-
|
|
947
|
-
The function:
|
|
948
|
-
1. Optionally downloads and reads EPW weather data
|
|
949
|
-
2. Handles timezone conversions and solar position calculations
|
|
950
|
-
3. Computes either instantaneous or cumulative irradiance maps
|
|
951
|
-
4. Supports visualization and export options
|
|
952
|
-
|
|
953
|
-
Args:
|
|
954
|
-
voxel_data (ndarray): 3D array of voxel values.
|
|
955
|
-
meshsize (float): Size of each voxel in meters.
|
|
956
|
-
calc_type (str): 'instantaneous' or 'cumulative'.
|
|
957
|
-
direct_normal_irradiance_scaling (float): Scaling factor for direct normal irradiance.
|
|
958
|
-
diffuse_irradiance_scaling (float): Scaling factor for diffuse horizontal irradiance.
|
|
959
|
-
**kwargs: Additional arguments including:
|
|
960
|
-
- download_nearest_epw (bool): Whether to download nearest EPW file
|
|
961
|
-
- epw_file_path (str): Path to EPW file
|
|
962
|
-
- rectangle_vertices (list): List of (lat,lon) coordinates for EPW download
|
|
963
|
-
- output_dir (str): Directory for EPW download
|
|
964
|
-
- calc_time (str): Time for instantaneous calculation ('MM-DD HH:MM:SS')
|
|
965
|
-
- start_time (str): Start time for cumulative calculation
|
|
966
|
-
- end_time (str): End time for cumulative calculation
|
|
967
|
-
- start_hour (int): Starting hour for daily time window (0-23)
|
|
968
|
-
- end_hour (int): Ending hour for daily time window (0-23)
|
|
969
|
-
- view_point_height (float): Observer height in meters
|
|
970
|
-
- tree_k (float): Tree extinction coefficient
|
|
971
|
-
- tree_lad (float): Leaf area density in m^-1
|
|
972
|
-
- show_plot (bool): Whether to show visualization
|
|
973
|
-
- show_each_timestep (bool): Whether to show timestep plots
|
|
974
|
-
- colormap (str): Matplotlib colormap name
|
|
975
|
-
- obj_export (bool): Whether to export as OBJ file
|
|
976
|
-
|
|
977
|
-
Returns:
|
|
978
|
-
ndarray: 2D array of solar irradiance values (W/m²).
|
|
979
|
-
"""
|
|
980
|
-
view_point_height = kwargs.get("view_point_height", 1.5)
|
|
981
|
-
colormap = kwargs.get("colormap", 'magma')
|
|
982
|
-
|
|
983
|
-
# Get EPW file
|
|
984
|
-
download_nearest_epw = kwargs.get("download_nearest_epw", False)
|
|
985
|
-
rectangle_vertices = kwargs.get("rectangle_vertices", None)
|
|
986
|
-
epw_file_path = kwargs.get("epw_file_path", None)
|
|
987
|
-
if download_nearest_epw:
|
|
988
|
-
if rectangle_vertices is None:
|
|
989
|
-
print("rectangle_vertices is required to download nearest EPW file")
|
|
990
|
-
return None
|
|
991
|
-
else:
|
|
992
|
-
# Calculate center point of rectangle
|
|
993
|
-
lons = [coord[0] for coord in rectangle_vertices]
|
|
994
|
-
lats = [coord[1] for coord in rectangle_vertices]
|
|
995
|
-
center_lon = (min(lons) + max(lons)) / 2
|
|
996
|
-
center_lat = (min(lats) + max(lats)) / 2
|
|
997
|
-
target_point = (center_lon, center_lat)
|
|
998
|
-
|
|
999
|
-
# Optional: specify maximum distance in kilometers
|
|
1000
|
-
max_distance = 100 # None for no limit
|
|
1001
|
-
|
|
1002
|
-
output_dir = kwargs.get("output_dir", "output")
|
|
1003
|
-
|
|
1004
|
-
epw_file_path, weather_data, metadata = get_nearest_epw_from_climate_onebuilding(
|
|
1005
|
-
longitude=center_lon,
|
|
1006
|
-
latitude=center_lat,
|
|
1007
|
-
output_dir=output_dir,
|
|
1008
|
-
max_distance=max_distance,
|
|
1009
|
-
extract_zip=True,
|
|
1010
|
-
load_data=True,
|
|
1011
|
-
allow_insecure_ssl=kwargs.get("allow_insecure_ssl", False),
|
|
1012
|
-
allow_http_fallback=kwargs.get("allow_http_fallback", False),
|
|
1013
|
-
ssl_verify=kwargs.get("ssl_verify", True)
|
|
1014
|
-
)
|
|
1015
|
-
|
|
1016
|
-
# Read EPW data
|
|
1017
|
-
if epw_file_path is None:
|
|
1018
|
-
raise RuntimeError("EPW file path is None. Set 'epw_file_path' or enable 'download_nearest_epw' and ensure network succeeds.")
|
|
1019
|
-
df, lon, lat, tz, elevation_m = read_epw_for_solar_simulation(epw_file_path)
|
|
1020
|
-
if df.empty:
|
|
1021
|
-
raise ValueError("No data in EPW file.")
|
|
1022
|
-
|
|
1023
|
-
if calc_type == 'instantaneous':
|
|
1024
|
-
|
|
1025
|
-
calc_time = kwargs.get("calc_time", "01-01 12:00:00")
|
|
1026
|
-
|
|
1027
|
-
# Parse start and end times without year
|
|
1028
|
-
try:
|
|
1029
|
-
calc_dt = datetime.strptime(calc_time, "%m-%d %H:%M:%S")
|
|
1030
|
-
except ValueError as ve:
|
|
1031
|
-
raise ValueError("calc_time must be in format 'MM-DD HH:MM:SS'") from ve
|
|
1032
|
-
|
|
1033
|
-
df_period = df[
|
|
1034
|
-
(df.index.month == calc_dt.month) & (df.index.day == calc_dt.day) & (df.index.hour == calc_dt.hour)
|
|
1035
|
-
]
|
|
1036
|
-
|
|
1037
|
-
if df_period.empty:
|
|
1038
|
-
raise ValueError("No EPW data at the specified time.")
|
|
1039
|
-
|
|
1040
|
-
# Prepare timezone conversion
|
|
1041
|
-
offset_minutes = int(tz * 60)
|
|
1042
|
-
local_tz = pytz.FixedOffset(offset_minutes)
|
|
1043
|
-
df_period_local = df_period.copy()
|
|
1044
|
-
df_period_local.index = df_period_local.index.tz_localize(local_tz)
|
|
1045
|
-
df_period_utc = df_period_local.tz_convert(pytz.UTC)
|
|
1046
|
-
|
|
1047
|
-
# Compute solar positions
|
|
1048
|
-
solar_positions = get_solar_positions_astral(df_period_utc.index, lon, lat)
|
|
1049
|
-
direct_normal_irradiance = df_period_utc.iloc[0]['DNI']
|
|
1050
|
-
diffuse_irradiance = df_period_utc.iloc[0]['DHI']
|
|
1051
|
-
azimuth_degrees = solar_positions.iloc[0]['azimuth']
|
|
1052
|
-
elevation_degrees = solar_positions.iloc[0]['elevation']
|
|
1053
|
-
solar_map = get_global_solar_irradiance_map(
|
|
1054
|
-
voxel_data, # 3D voxel grid representing the urban environment
|
|
1055
|
-
meshsize, # Size of each grid cell in meters
|
|
1056
|
-
azimuth_degrees, # Sun's azimuth angle
|
|
1057
|
-
elevation_degrees, # Sun's elevation angle
|
|
1058
|
-
direct_normal_irradiance, # Direct Normal Irradiance value
|
|
1059
|
-
diffuse_irradiance, # Diffuse irradiance value
|
|
1060
|
-
show_plot=True, # Display visualization of results
|
|
1061
|
-
**kwargs
|
|
1062
|
-
)
|
|
1063
|
-
if calc_type == 'cumulative':
|
|
1064
|
-
# Get time window parameters
|
|
1065
|
-
start_hour = kwargs.get("start_hour", 0) # Default to midnight
|
|
1066
|
-
end_hour = kwargs.get("end_hour", 23) # Default to 11 PM
|
|
1067
|
-
|
|
1068
|
-
# Filter dataframe for specified hours
|
|
1069
|
-
df_filtered = df[(df.index.hour >= start_hour) & (df.index.hour <= end_hour)]
|
|
1070
|
-
|
|
1071
|
-
solar_map = get_cumulative_global_solar_irradiance(
|
|
1072
|
-
voxel_data,
|
|
1073
|
-
meshsize,
|
|
1074
|
-
df_filtered, lon, lat, tz,
|
|
1075
|
-
**kwargs
|
|
1076
|
-
)
|
|
1077
|
-
|
|
1078
|
-
return solar_map
|
|
1079
|
-
|
|
1080
|
-
import numpy as np
|
|
1081
|
-
import trimesh
|
|
1082
|
-
import time
|
|
1083
|
-
from numba import njit, prange
|
|
1084
|
-
|
|
1085
|
-
##############################################################################
|
|
1086
|
-
# 1) New Numba helper: per-face solar irradiance computation
|
|
1087
|
-
##############################################################################
|
|
1088
|
-
@njit(parallel=True)
|
|
1089
|
-
def compute_solar_irradiance_for_all_faces(
|
|
1090
|
-
face_centers,
|
|
1091
|
-
face_normals,
|
|
1092
|
-
face_svf,
|
|
1093
|
-
sun_direction,
|
|
1094
|
-
direct_normal_irradiance,
|
|
1095
|
-
diffuse_irradiance,
|
|
1096
|
-
voxel_data,
|
|
1097
|
-
meshsize,
|
|
1098
|
-
tree_k,
|
|
1099
|
-
tree_lad,
|
|
1100
|
-
hit_values,
|
|
1101
|
-
inclusion_mode,
|
|
1102
|
-
grid_bounds_real,
|
|
1103
|
-
boundary_epsilon
|
|
1104
|
-
):
|
|
1105
|
-
"""
|
|
1106
|
-
Numba-compiled function to compute direct, diffuse, and global solar irradiance
|
|
1107
|
-
for each face in a 3D building mesh.
|
|
1108
|
-
|
|
1109
|
-
This optimized function processes all mesh faces in parallel to calculate solar
|
|
1110
|
-
irradiance components. It handles both direct radiation (dependent on sun position
|
|
1111
|
-
and surface orientation) and diffuse radiation (dependent on sky visibility).
|
|
1112
|
-
The function is compiled with Numba for high-performance computation on large meshes.
|
|
1113
|
-
|
|
1114
|
-
Surface Irradiance Physics:
|
|
1115
|
-
- Direct component: DNI × cos(incidence_angle) × transmittance
|
|
1116
|
-
- Diffuse component: DHI × sky_view_factor
|
|
1117
|
-
- Incidence angle: Angle between sun direction and surface normal
|
|
1118
|
-
- Transmittance: Attenuation factor from obstacles and vegetation
|
|
1119
|
-
|
|
1120
|
-
Boundary Condition Handling:
|
|
1121
|
-
- Vertical boundary faces are excluded (mesh edges touching domain boundaries)
|
|
1122
|
-
- Invalid faces (NaN SVF) are skipped to maintain data consistency
|
|
1123
|
-
- Surface orientation affects direct radiation calculation
|
|
1124
|
-
|
|
1125
|
-
Performance Optimizations:
|
|
1126
|
-
- Numba JIT compilation for near C-speed execution
|
|
1127
|
-
- Parallel processing of face calculations
|
|
1128
|
-
- Efficient geometric computations using vectorized operations
|
|
1129
|
-
- Memory-optimized array operations
|
|
1130
|
-
|
|
1131
|
-
Args:
|
|
1132
|
-
face_centers (float64[:, :]): (N x 3) array of face center coordinates in real-world units
|
|
1133
|
-
face_normals (float64[:, :]): (N x 3) array of outward-pointing unit normal vectors
|
|
1134
|
-
face_svf (float64[:]): (N,) array of Sky View Factor values for each face (0.0-1.0)
|
|
1135
|
-
sun_direction (float64[:]): (3,) array for normalized sun direction vector (dx, dy, dz)
|
|
1136
|
-
direct_normal_irradiance (float): Direct normal irradiance (DNI) in W/m²
|
|
1137
|
-
diffuse_irradiance (float): Diffuse horizontal irradiance (DHI) in W/m²
|
|
1138
|
-
voxel_data (ndarray): 3D array of voxel values for obstacle detection
|
|
1139
|
-
meshsize (float): Size of each voxel in meters (spatial resolution)
|
|
1140
|
-
tree_k (float): Tree extinction coefficient for Beer-Lambert law
|
|
1141
|
-
tree_lad (float): Leaf area density in m^-1
|
|
1142
|
-
hit_values (tuple): Values considered 'sky' for ray tracing (e.g. (0,))
|
|
1143
|
-
inclusion_mode (bool): Whether hit_values are included (True) or excluded (False)
|
|
1144
|
-
grid_bounds_real (float64[2,3]): Domain boundaries [[x_min,y_min,z_min],[x_max,y_max,z_max]]
|
|
1145
|
-
boundary_epsilon (float): Distance threshold for boundary face detection
|
|
1146
|
-
|
|
1147
|
-
Returns:
|
|
1148
|
-
tuple: Three float64[N] arrays containing:
|
|
1149
|
-
- direct_irr: Direct solar irradiance for each face (W/m²)
|
|
1150
|
-
- diffuse_irr: Diffuse solar irradiance for each face (W/m²)
|
|
1151
|
-
- global_irr: Global solar irradiance for each face (W/m²)
|
|
1152
|
-
|
|
1153
|
-
Note:
|
|
1154
|
-
This function is optimized with Numba and should not be called directly.
|
|
1155
|
-
Use the higher-level wrapper functions for normal operation.
|
|
1156
|
-
"""
|
|
1157
|
-
n_faces = face_centers.shape[0]
|
|
1158
|
-
|
|
1159
|
-
# Initialize output arrays for each irradiance component
|
|
1160
|
-
face_direct = np.zeros(n_faces, dtype=np.float64)
|
|
1161
|
-
face_diffuse = np.zeros(n_faces, dtype=np.float64)
|
|
1162
|
-
face_global = np.zeros(n_faces, dtype=np.float64)
|
|
1163
|
-
|
|
1164
|
-
# Extract domain boundaries for boundary face detection
|
|
1165
|
-
x_min, y_min, z_min = grid_bounds_real[0, 0], grid_bounds_real[0, 1], grid_bounds_real[0, 2]
|
|
1166
|
-
x_max, y_max, z_max = grid_bounds_real[1, 0], grid_bounds_real[1, 1], grid_bounds_real[1, 2]
|
|
1167
|
-
|
|
1168
|
-
# Process each face individually (Numba optimizes this loop)
|
|
1169
|
-
for fidx in prange(n_faces):
|
|
1170
|
-
center = face_centers[fidx]
|
|
1171
|
-
normal = face_normals[fidx]
|
|
1172
|
-
svf = face_svf[fidx]
|
|
1173
|
-
|
|
1174
|
-
# Check for vertical boundary faces that should be excluded
|
|
1175
|
-
# These are mesh edges at domain boundaries, not actual building surfaces
|
|
1176
|
-
is_vertical = (abs(normal[2]) < 0.01) # Nearly vertical normal
|
|
1177
|
-
|
|
1178
|
-
# Check if face center is at domain boundary
|
|
1179
|
-
on_x_min = (abs(center[0] - x_min) < boundary_epsilon)
|
|
1180
|
-
on_y_min = (abs(center[1] - y_min) < boundary_epsilon)
|
|
1181
|
-
on_x_max = (abs(center[0] - x_max) < boundary_epsilon)
|
|
1182
|
-
on_y_max = (abs(center[1] - y_max) < boundary_epsilon)
|
|
1183
|
-
|
|
1184
|
-
is_boundary_vertical = is_vertical and (on_x_min or on_y_min or on_x_max or on_y_max)
|
|
1185
|
-
|
|
1186
|
-
# Skip boundary faces to avoid artifacts
|
|
1187
|
-
if is_boundary_vertical:
|
|
1188
|
-
face_direct[fidx] = np.nan
|
|
1189
|
-
face_diffuse[fidx] = np.nan
|
|
1190
|
-
face_global[fidx] = np.nan
|
|
1191
|
-
continue
|
|
1192
|
-
|
|
1193
|
-
# Skip faces with invalid SVF data
|
|
1194
|
-
if svf != svf: # NaN check in Numba-compatible way
|
|
1195
|
-
face_direct[fidx] = np.nan
|
|
1196
|
-
face_diffuse[fidx] = np.nan
|
|
1197
|
-
face_global[fidx] = np.nan
|
|
1198
|
-
continue
|
|
1199
|
-
|
|
1200
|
-
# Calculate direct irradiance component
|
|
1201
|
-
# Only surfaces oriented towards the sun receive direct radiation
|
|
1202
|
-
cos_incidence = normal[0]*sun_direction[0] + \
|
|
1203
|
-
normal[1]*sun_direction[1] + \
|
|
1204
|
-
normal[2]*sun_direction[2]
|
|
1205
|
-
|
|
1206
|
-
direct_val = 0.0
|
|
1207
|
-
if cos_incidence > 0.0: # Surface faces towards sun
|
|
1208
|
-
# Offset ray origin slightly along normal to avoid self-intersection
|
|
1209
|
-
offset_vox = 0.1 # Small offset in voxel units
|
|
1210
|
-
ray_origin_x = center[0]/meshsize + normal[0]*offset_vox
|
|
1211
|
-
ray_origin_y = center[1]/meshsize + normal[1]*offset_vox
|
|
1212
|
-
ray_origin_z = center[2]/meshsize + normal[2]*offset_vox
|
|
1213
|
-
|
|
1214
|
-
# Cast ray toward the sun to check for obstructions
|
|
1215
|
-
hit_detected, transmittance = trace_ray_generic(
|
|
1216
|
-
voxel_data,
|
|
1217
|
-
np.array([ray_origin_x, ray_origin_y, ray_origin_z], dtype=np.float64),
|
|
1218
|
-
sun_direction,
|
|
1219
|
-
hit_values,
|
|
1220
|
-
meshsize,
|
|
1221
|
-
tree_k,
|
|
1222
|
-
tree_lad,
|
|
1223
|
-
inclusion_mode
|
|
1224
|
-
)
|
|
1225
|
-
|
|
1226
|
-
# Calculate direct irradiance if path to sun is clear/partially clear
|
|
1227
|
-
if not hit_detected:
|
|
1228
|
-
direct_val = direct_normal_irradiance * cos_incidence * transmittance
|
|
1229
|
-
|
|
1230
|
-
# Calculate diffuse irradiance component using Sky View Factor
|
|
1231
|
-
# All surfaces receive diffuse radiation proportional to their sky visibility
|
|
1232
|
-
diffuse_val = svf * diffuse_irradiance
|
|
1233
|
-
|
|
1234
|
-
# Ensure diffuse irradiance doesn't exceed theoretical maximum
|
|
1235
|
-
if diffuse_val > diffuse_irradiance:
|
|
1236
|
-
diffuse_val = diffuse_irradiance
|
|
1237
|
-
|
|
1238
|
-
# Store results for this face
|
|
1239
|
-
face_direct[fidx] = direct_val
|
|
1240
|
-
face_diffuse[fidx] = diffuse_val
|
|
1241
|
-
face_global[fidx] = direct_val + diffuse_val
|
|
1242
|
-
|
|
1243
|
-
return face_direct, face_diffuse, face_global
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
##############################################################################
|
|
1247
|
-
# 2) Modified get_building_solar_irradiance: main Python wrapper
|
|
1248
|
-
##############################################################################
|
|
1249
|
-
def get_building_solar_irradiance(
|
|
1250
|
-
voxel_data,
|
|
1251
|
-
meshsize,
|
|
1252
|
-
building_svf_mesh,
|
|
1253
|
-
azimuth_degrees,
|
|
1254
|
-
elevation_degrees,
|
|
1255
|
-
direct_normal_irradiance,
|
|
1256
|
-
diffuse_irradiance,
|
|
1257
|
-
**kwargs
|
|
1258
|
-
):
|
|
1259
|
-
"""
|
|
1260
|
-
Calculate solar irradiance on building surfaces using Sky View Factor (SVF) analysis,
|
|
1261
|
-
with high-performance computation accelerated by Numba JIT compilation.
|
|
1262
|
-
|
|
1263
|
-
This function performs detailed solar irradiance analysis on 3D building surfaces
|
|
1264
|
-
represented as triangulated meshes. It calculates both direct and diffuse components
|
|
1265
|
-
of solar radiation for each mesh face, accounting for surface orientation, shadows,
|
|
1266
|
-
and sky visibility. The computation is optimized for large urban models using
|
|
1267
|
-
efficient algorithms and parallel processing.
|
|
1268
|
-
|
|
1269
|
-
Mesh-Based Analysis Advantages:
|
|
1270
|
-
- Surface-specific calculations for facades, roofs, and complex geometries
|
|
1271
|
-
- Accurate accounting of surface orientation and local shading effects
|
|
1272
|
-
- Integration with 3D visualization and CAD workflows
|
|
1273
|
-
- Detailed irradiance data for building energy modeling
|
|
1274
|
-
|
|
1275
|
-
Performance Features:
|
|
1276
|
-
- Numba JIT compilation for near C-speed execution
|
|
1277
|
-
- Parallel processing of mesh faces
|
|
1278
|
-
- Efficient ray tracing with tree transmittance
|
|
1279
|
-
- Memory-optimized operations for large datasets
|
|
1280
|
-
|
|
1281
|
-
Physical Modeling:
|
|
1282
|
-
- Direct irradiance: Based on sun position and surface orientation
|
|
1283
|
-
- Diffuse irradiance: Based on Sky View Factor from each surface
|
|
1284
|
-
- Tree effects: Partial transmittance using Beer-Lambert law
|
|
1285
|
-
- Boundary handling: Automatic exclusion of domain boundary artifacts
|
|
1286
|
-
|
|
1287
|
-
Args:
|
|
1288
|
-
voxel_data (ndarray): 3D array of voxel values representing the urban environment
|
|
1289
|
-
meshsize (float): Size of each voxel in meters (spatial resolution)
|
|
1290
|
-
building_svf_mesh (trimesh.Trimesh): Building mesh with pre-calculated SVF values in metadata
|
|
1291
|
-
Must have 'svf' array in mesh.metadata
|
|
1292
|
-
azimuth_degrees (float): Sun azimuth angle in degrees (0=North, 90=East)
|
|
1293
|
-
elevation_degrees (float): Sun elevation angle in degrees above horizon (0-90°)
|
|
1294
|
-
direct_normal_irradiance (float): Direct normal irradiance (DNI) in W/m² from weather data
|
|
1295
|
-
diffuse_irradiance (float): Diffuse horizontal irradiance (DHI) in W/m² from weather data
|
|
1296
|
-
**kwargs: Additional parameters including:
|
|
1297
|
-
- tree_k (float): Tree extinction coefficient (default: 0.6)
|
|
1298
|
-
Higher values mean trees block more light
|
|
1299
|
-
- tree_lad (float): Leaf area density in m^-1 (default: 1.0)
|
|
1300
|
-
Affects light attenuation through tree canopies
|
|
1301
|
-
- progress_report (bool): Whether to print timing information (default: False)
|
|
1302
|
-
- obj_export (bool): Whether to export results as OBJ file
|
|
1303
|
-
- output_directory (str): Directory for file exports
|
|
1304
|
-
- output_file_name (str): Base filename for exports
|
|
1305
|
-
|
|
1306
|
-
Returns:
|
|
1307
|
-
trimesh.Trimesh: A copy of the input mesh with irradiance data stored in metadata:
|
|
1308
|
-
- 'svf': Sky View Factor for each face (preserved from input)
|
|
1309
|
-
- 'direct': Direct solar irradiance for each face (W/m²)
|
|
1310
|
-
- 'diffuse': Diffuse solar irradiance for each face (W/m²)
|
|
1311
|
-
- 'global': Global solar irradiance for each face (W/m²)
|
|
1312
|
-
|
|
1313
|
-
Note:
|
|
1314
|
-
The input mesh must have SVF values pre-calculated and stored in metadata.
|
|
1315
|
-
Use get_surface_view_factor() to compute SVF before calling this function.
|
|
1316
|
-
"""
|
|
1317
|
-
import time
|
|
1318
|
-
|
|
1319
|
-
# Extract tree transmittance parameters with defaults
|
|
1320
|
-
tree_k = kwargs.get("tree_k", 0.6)
|
|
1321
|
-
tree_lad = kwargs.get("tree_lad", 1.0)
|
|
1322
|
-
progress_report = kwargs.get("progress_report", False)
|
|
1323
|
-
|
|
1324
|
-
# Define sky detection parameters for ray tracing
|
|
1325
|
-
hit_values = (0,) # '0' = sky voxel value
|
|
1326
|
-
inclusion_mode = False # Treat non-sky values as obstacles
|
|
1327
|
-
|
|
1328
|
-
# Convert solar angles to 3D direction vector using spherical coordinates
|
|
1329
|
-
az_rad = np.deg2rad(180 - azimuth_degrees) # Adjust for coordinate system
|
|
1330
|
-
el_rad = np.deg2rad(elevation_degrees)
|
|
1331
|
-
sun_dx = np.cos(el_rad) * np.cos(az_rad)
|
|
1332
|
-
sun_dy = np.cos(el_rad) * np.sin(az_rad)
|
|
1333
|
-
sun_dz = np.sin(el_rad)
|
|
1334
|
-
sun_direction = np.array([sun_dx, sun_dy, sun_dz], dtype=np.float64)
|
|
1335
|
-
|
|
1336
|
-
# Extract mesh geometry data for processing (optionally from cache)
|
|
1337
|
-
precomputed_geometry = kwargs.get("precomputed_geometry", None)
|
|
1338
|
-
if precomputed_geometry is not None:
|
|
1339
|
-
face_centers = precomputed_geometry.get("face_centers", building_svf_mesh.triangles_center)
|
|
1340
|
-
face_normals = precomputed_geometry.get("face_normals", building_svf_mesh.face_normals)
|
|
1341
|
-
face_svf_opt = precomputed_geometry.get("face_svf", None)
|
|
1342
|
-
grid_bounds_real = precomputed_geometry.get("grid_bounds_real", None)
|
|
1343
|
-
boundary_epsilon = precomputed_geometry.get("boundary_epsilon", None)
|
|
1344
|
-
else:
|
|
1345
|
-
face_centers = building_svf_mesh.triangles_center # Center point of each face
|
|
1346
|
-
face_normals = building_svf_mesh.face_normals # Normal vector for each face
|
|
1347
|
-
|
|
1348
|
-
# Extract Sky View Factor data from mesh metadata
|
|
1349
|
-
if hasattr(building_svf_mesh, 'metadata') and ('svf' in building_svf_mesh.metadata):
|
|
1350
|
-
face_svf = building_svf_mesh.metadata['svf']
|
|
1351
|
-
else:
|
|
1352
|
-
# Initialize with zeros if SVF not available (should be pre-calculated)
|
|
1353
|
-
face_svf = np.zeros(len(building_svf_mesh.faces), dtype=np.float64)
|
|
1354
|
-
|
|
1355
|
-
# Set up domain boundaries for boundary face detection (use cache if available)
|
|
1356
|
-
if grid_bounds_real is None or boundary_epsilon is None:
|
|
1357
|
-
grid_shape = voxel_data.shape
|
|
1358
|
-
grid_bounds_voxel = np.array([[0,0,0],[grid_shape[0], grid_shape[1], grid_shape[2]]], dtype=np.float64)
|
|
1359
|
-
grid_bounds_real = grid_bounds_voxel * meshsize
|
|
1360
|
-
boundary_epsilon = meshsize * 0.05 # Small tolerance for boundary detection
|
|
1361
|
-
|
|
1362
|
-
# Optional fast path using masked DDA kernel
|
|
1363
|
-
fast_path = kwargs.get("fast_path", True)
|
|
1364
|
-
precomputed_masks = kwargs.get("precomputed_masks", None)
|
|
1365
|
-
t0 = time.time()
|
|
1366
|
-
if fast_path:
|
|
1367
|
-
# Prepare masks (reuse cache when possible)
|
|
1368
|
-
if precomputed_masks is not None:
|
|
1369
|
-
vox_is_tree = precomputed_masks.get("vox_is_tree", (voxel_data == -2))
|
|
1370
|
-
vox_is_opaque = precomputed_masks.get("vox_is_opaque", (voxel_data != 0) & (voxel_data != -2))
|
|
1371
|
-
att = float(precomputed_masks.get("att", np.exp(-tree_k * tree_lad * meshsize)))
|
|
1372
|
-
else:
|
|
1373
|
-
vox_is_tree = (voxel_data == -2)
|
|
1374
|
-
vox_is_opaque = (voxel_data != 0) & (~vox_is_tree)
|
|
1375
|
-
att = float(np.exp(-tree_k * tree_lad * meshsize))
|
|
1376
|
-
|
|
1377
|
-
face_direct, face_diffuse, face_global = compute_solar_irradiance_for_all_faces_masked(
|
|
1378
|
-
face_centers.astype(np.float64),
|
|
1379
|
-
face_normals.astype(np.float64),
|
|
1380
|
-
face_svf.astype(np.float64),
|
|
1381
|
-
sun_direction.astype(np.float64),
|
|
1382
|
-
float(direct_normal_irradiance),
|
|
1383
|
-
float(diffuse_irradiance),
|
|
1384
|
-
vox_is_tree,
|
|
1385
|
-
vox_is_opaque,
|
|
1386
|
-
float(meshsize),
|
|
1387
|
-
att,
|
|
1388
|
-
float(grid_bounds_real[0,0]), float(grid_bounds_real[0,1]), float(grid_bounds_real[0,2]),
|
|
1389
|
-
float(grid_bounds_real[1,0]), float(grid_bounds_real[1,1]), float(grid_bounds_real[1,2]),
|
|
1390
|
-
float(boundary_epsilon)
|
|
1391
|
-
)
|
|
1392
|
-
else:
|
|
1393
|
-
face_direct, face_diffuse, face_global = compute_solar_irradiance_for_all_faces(
|
|
1394
|
-
face_centers,
|
|
1395
|
-
face_normals,
|
|
1396
|
-
face_svf_opt if (precomputed_geometry is not None and face_svf_opt is not None) else face_svf,
|
|
1397
|
-
sun_direction,
|
|
1398
|
-
direct_normal_irradiance,
|
|
1399
|
-
diffuse_irradiance,
|
|
1400
|
-
voxel_data,
|
|
1401
|
-
meshsize,
|
|
1402
|
-
tree_k,
|
|
1403
|
-
tree_lad,
|
|
1404
|
-
hit_values,
|
|
1405
|
-
inclusion_mode,
|
|
1406
|
-
grid_bounds_real,
|
|
1407
|
-
boundary_epsilon
|
|
1408
|
-
)
|
|
1409
|
-
|
|
1410
|
-
# Report performance timing if requested
|
|
1411
|
-
if progress_report:
|
|
1412
|
-
elapsed = time.time() - t0
|
|
1413
|
-
print(f"Numba-based solar irradiance calculation took {elapsed:.2f} seconds")
|
|
1414
|
-
|
|
1415
|
-
# Create a copy of the input mesh to store results
|
|
1416
|
-
irradiance_mesh = building_svf_mesh.copy()
|
|
1417
|
-
if not hasattr(irradiance_mesh, 'metadata'):
|
|
1418
|
-
irradiance_mesh.metadata = {}
|
|
1419
|
-
|
|
1420
|
-
# Store results
|
|
1421
|
-
irradiance_mesh.metadata['svf'] = face_svf
|
|
1422
|
-
irradiance_mesh.metadata['direct'] = face_direct
|
|
1423
|
-
irradiance_mesh.metadata['diffuse'] = face_diffuse
|
|
1424
|
-
irradiance_mesh.metadata['global'] = face_global
|
|
1425
|
-
|
|
1426
|
-
irradiance_mesh.name = "Solar Irradiance (W/m²)"
|
|
1427
|
-
|
|
1428
|
-
# # Optional OBJ export
|
|
1429
|
-
# obj_export = kwargs.get("obj_export", False)
|
|
1430
|
-
# if obj_export:
|
|
1431
|
-
# # Get export parameters
|
|
1432
|
-
# output_dir = kwargs.get("output_directory", "output")
|
|
1433
|
-
# output_file_name = kwargs.get("output_file_name", "solar_irradiance")
|
|
1434
|
-
|
|
1435
|
-
# # Export the mesh directly
|
|
1436
|
-
# irradiance_mesh.export(f"{output_dir}/{output_file_name}.obj")
|
|
1437
|
-
|
|
1438
|
-
return irradiance_mesh
|
|
1439
|
-
|
|
1440
|
-
##############################################################################
|
|
1441
|
-
# 2.5) Specialized masked DDA for per-face irradiance (parallel)
|
|
1442
|
-
##############################################################################
|
|
1443
|
-
@njit(cache=True, fastmath=True, nogil=True)
|
|
1444
|
-
def _trace_direct_masked(vox_is_tree, vox_is_opaque, origin, direction, att, att_cutoff=0.01):
|
|
1445
|
-
nx, ny, nz = vox_is_opaque.shape
|
|
1446
|
-
x0 = origin[0]; y0 = origin[1]; z0 = origin[2]
|
|
1447
|
-
dx = direction[0]; dy = direction[1]; dz = direction[2]
|
|
1448
|
-
|
|
1449
|
-
# Normalize
|
|
1450
|
-
L = (dx*dx + dy*dy + dz*dz) ** 0.5
|
|
1451
|
-
if L == 0.0:
|
|
1452
|
-
return False, 1.0
|
|
1453
|
-
invL = 1.0 / L
|
|
1454
|
-
dx *= invL; dy *= invL; dz *= invL
|
|
1455
|
-
|
|
1456
|
-
# Start at voxel centers
|
|
1457
|
-
x = x0 + 0.5; y = y0 + 0.5; z = z0 + 0.5
|
|
1458
|
-
i = int(x0); j = int(y0); k = int(z0)
|
|
1459
|
-
|
|
1460
|
-
step_x = 1 if dx >= 0.0 else -1
|
|
1461
|
-
step_y = 1 if dy >= 0.0 else -1
|
|
1462
|
-
step_z = 1 if dz >= 0.0 else -1
|
|
1463
|
-
|
|
1464
|
-
BIG = 1e30
|
|
1465
|
-
if dx != 0.0:
|
|
1466
|
-
t_max_x = (((i + (1 if step_x > 0 else 0)) - x) / dx)
|
|
1467
|
-
t_delta_x = abs(1.0 / dx)
|
|
1468
|
-
else:
|
|
1469
|
-
t_max_x = BIG; t_delta_x = BIG
|
|
1470
|
-
if dy != 0.0:
|
|
1471
|
-
t_max_y = (((j + (1 if step_y > 0 else 0)) - y) / dy)
|
|
1472
|
-
t_delta_y = abs(1.0 / dy)
|
|
1473
|
-
else:
|
|
1474
|
-
t_max_y = BIG; t_delta_y = BIG
|
|
1475
|
-
if dz != 0.0:
|
|
1476
|
-
t_max_z = (((k + (1 if step_z > 0 else 0)) - z) / dz)
|
|
1477
|
-
t_delta_z = abs(1.0 / dz)
|
|
1478
|
-
else:
|
|
1479
|
-
t_max_z = BIG; t_delta_z = BIG
|
|
1480
|
-
|
|
1481
|
-
T = 1.0
|
|
1482
|
-
while True:
|
|
1483
|
-
if (i < 0) or (i >= nx) or (j < 0) or (j >= ny) or (k < 0) or (k >= nz):
|
|
1484
|
-
# Exited grid: not blocked
|
|
1485
|
-
return False, T
|
|
1486
|
-
|
|
1487
|
-
if vox_is_opaque[i, j, k]:
|
|
1488
|
-
# Hit opaque (non-sky, non-tree)
|
|
1489
|
-
return True, T
|
|
1490
|
-
|
|
1491
|
-
if vox_is_tree[i, j, k]:
|
|
1492
|
-
T *= att
|
|
1493
|
-
if T < att_cutoff:
|
|
1494
|
-
# Consider fully attenuated
|
|
1495
|
-
return True, T
|
|
1496
|
-
|
|
1497
|
-
# Step DDA
|
|
1498
|
-
if t_max_x < t_max_y:
|
|
1499
|
-
if t_max_x < t_max_z:
|
|
1500
|
-
t_max_x += t_delta_x; i += step_x
|
|
1501
|
-
else:
|
|
1502
|
-
t_max_z += t_delta_z; k += step_z
|
|
1503
|
-
else:
|
|
1504
|
-
if t_max_y < t_max_z:
|
|
1505
|
-
t_max_y += t_delta_y; j += step_y
|
|
1506
|
-
else:
|
|
1507
|
-
t_max_z += t_delta_z; k += step_z
|
|
1508
|
-
|
|
1509
|
-
@njit(parallel=True, cache=True, fastmath=True, nogil=True)
|
|
1510
|
-
def compute_solar_irradiance_for_all_faces_masked(
|
|
1511
|
-
face_centers,
|
|
1512
|
-
face_normals,
|
|
1513
|
-
face_svf,
|
|
1514
|
-
sun_direction,
|
|
1515
|
-
direct_normal_irradiance,
|
|
1516
|
-
diffuse_irradiance,
|
|
1517
|
-
vox_is_tree,
|
|
1518
|
-
vox_is_opaque,
|
|
1519
|
-
meshsize,
|
|
1520
|
-
att,
|
|
1521
|
-
x_min, y_min, z_min,
|
|
1522
|
-
x_max, y_max, z_max,
|
|
1523
|
-
boundary_epsilon
|
|
1524
|
-
):
|
|
1525
|
-
n_faces = face_centers.shape[0]
|
|
1526
|
-
face_direct = np.zeros(n_faces, dtype=np.float64)
|
|
1527
|
-
face_diffuse = np.zeros(n_faces, dtype=np.float64)
|
|
1528
|
-
face_global = np.zeros(n_faces, dtype=np.float64)
|
|
1529
|
-
|
|
1530
|
-
for fidx in prange(n_faces):
|
|
1531
|
-
center = face_centers[fidx]
|
|
1532
|
-
normal = face_normals[fidx]
|
|
1533
|
-
svf = face_svf[fidx]
|
|
1534
|
-
|
|
1535
|
-
# Boundary vertical exclusion
|
|
1536
|
-
is_vertical = (abs(normal[2]) < 0.01)
|
|
1537
|
-
on_x_min = (abs(center[0] - x_min) < boundary_epsilon)
|
|
1538
|
-
on_y_min = (abs(center[1] - y_min) < boundary_epsilon)
|
|
1539
|
-
on_x_max = (abs(center[0] - x_max) < boundary_epsilon)
|
|
1540
|
-
on_y_max = (abs(center[1] - y_max) < boundary_epsilon)
|
|
1541
|
-
if is_vertical and (on_x_min or on_y_min or on_x_max or on_y_max):
|
|
1542
|
-
face_direct[fidx] = np.nan
|
|
1543
|
-
face_diffuse[fidx] = np.nan
|
|
1544
|
-
face_global[fidx] = np.nan
|
|
1545
|
-
continue
|
|
1546
|
-
|
|
1547
|
-
if svf != svf:
|
|
1548
|
-
face_direct[fidx] = np.nan
|
|
1549
|
-
face_diffuse[fidx] = np.nan
|
|
1550
|
-
face_global[fidx] = np.nan
|
|
1551
|
-
continue
|
|
1552
|
-
|
|
1553
|
-
# Direct component
|
|
1554
|
-
cos_incidence = normal[0]*sun_direction[0] + normal[1]*sun_direction[1] + normal[2]*sun_direction[2]
|
|
1555
|
-
direct_val = 0.0
|
|
1556
|
-
if cos_incidence > 0.0 and direct_normal_irradiance > 0.0:
|
|
1557
|
-
offset_vox = 0.1
|
|
1558
|
-
ox = center[0]/meshsize + normal[0]*offset_vox
|
|
1559
|
-
oy = center[1]/meshsize + normal[1]*offset_vox
|
|
1560
|
-
oz = center[2]/meshsize + normal[2]*offset_vox
|
|
1561
|
-
blocked, T = _trace_direct_masked(
|
|
1562
|
-
vox_is_tree,
|
|
1563
|
-
vox_is_opaque,
|
|
1564
|
-
np.array((ox, oy, oz), dtype=np.float64),
|
|
1565
|
-
sun_direction,
|
|
1566
|
-
att
|
|
1567
|
-
)
|
|
1568
|
-
if not blocked:
|
|
1569
|
-
direct_val = direct_normal_irradiance * cos_incidence * T
|
|
1570
|
-
|
|
1571
|
-
# Diffuse component
|
|
1572
|
-
diffuse_val = svf * diffuse_irradiance
|
|
1573
|
-
if diffuse_val > diffuse_irradiance:
|
|
1574
|
-
diffuse_val = diffuse_irradiance
|
|
1575
|
-
|
|
1576
|
-
face_direct[fidx] = direct_val
|
|
1577
|
-
face_diffuse[fidx] = diffuse_val
|
|
1578
|
-
face_global[fidx] = direct_val + diffuse_val
|
|
1579
|
-
|
|
1580
|
-
return face_direct, face_diffuse, face_global
|
|
1581
|
-
|
|
1582
|
-
@njit(parallel=True, cache=True, fastmath=True, nogil=True)
|
|
1583
|
-
def compute_cumulative_solar_irradiance_faces_masked_timeseries(
|
|
1584
|
-
face_centers,
|
|
1585
|
-
face_normals,
|
|
1586
|
-
face_svf,
|
|
1587
|
-
sun_dirs_arr, # shape (T, 3)
|
|
1588
|
-
DNI_arr, # shape (T,)
|
|
1589
|
-
DHI_arr, # shape (T,)
|
|
1590
|
-
vox_is_tree,
|
|
1591
|
-
vox_is_opaque,
|
|
1592
|
-
meshsize,
|
|
1593
|
-
att,
|
|
1594
|
-
x_min, y_min, z_min,
|
|
1595
|
-
x_max, y_max, z_max,
|
|
1596
|
-
boundary_epsilon,
|
|
1597
|
-
t_start, t_end, # [start, end) indices
|
|
1598
|
-
time_step_hours
|
|
1599
|
-
):
|
|
1600
|
-
n_faces = face_centers.shape[0]
|
|
1601
|
-
out_dir = np.zeros(n_faces, dtype=np.float64)
|
|
1602
|
-
out_diff = np.zeros(n_faces, dtype=np.float64)
|
|
1603
|
-
out_glob = np.zeros(n_faces, dtype=np.float64)
|
|
1604
|
-
|
|
1605
|
-
for fidx in prange(n_faces):
|
|
1606
|
-
center = face_centers[fidx]
|
|
1607
|
-
normal = face_normals[fidx]
|
|
1608
|
-
svf = face_svf[fidx]
|
|
1609
|
-
|
|
1610
|
-
# Boundary vertical exclusion
|
|
1611
|
-
is_vertical = (abs(normal[2]) < 0.01)
|
|
1612
|
-
on_x_min = (abs(center[0] - x_min) < boundary_epsilon)
|
|
1613
|
-
on_y_min = (abs(center[1] - y_min) < boundary_epsilon)
|
|
1614
|
-
on_x_max = (abs(center[0] - x_max) < boundary_epsilon)
|
|
1615
|
-
on_y_max = (abs(center[1] - y_max) < boundary_epsilon)
|
|
1616
|
-
if is_vertical and (on_x_min or on_y_min or on_x_max or on_y_max):
|
|
1617
|
-
out_dir[fidx] = np.nan
|
|
1618
|
-
out_diff[fidx] = np.nan
|
|
1619
|
-
out_glob[fidx] = np.nan
|
|
1620
|
-
continue
|
|
1621
|
-
|
|
1622
|
-
if svf != svf:
|
|
1623
|
-
out_dir[fidx] = np.nan
|
|
1624
|
-
out_diff[fidx] = np.nan
|
|
1625
|
-
out_glob[fidx] = np.nan
|
|
1626
|
-
continue
|
|
1627
|
-
|
|
1628
|
-
accum_dir = 0.0
|
|
1629
|
-
accum_diff = 0.0
|
|
1630
|
-
accum_glob = 0.0
|
|
1631
|
-
|
|
1632
|
-
# Precompute ray origin (voxel coords) once per face
|
|
1633
|
-
offset_vox = 0.1
|
|
1634
|
-
ox = center[0]/meshsize + normal[0]*offset_vox
|
|
1635
|
-
oy = center[1]/meshsize + normal[1]*offset_vox
|
|
1636
|
-
oz = center[2]/meshsize + normal[2]*offset_vox
|
|
1637
|
-
origin = np.array((ox, oy, oz), dtype=np.float64)
|
|
1638
|
-
|
|
1639
|
-
for t in range(t_start, t_end):
|
|
1640
|
-
dni = DNI_arr[t]
|
|
1641
|
-
dhi = DHI_arr[t]
|
|
1642
|
-
sd0 = sun_dirs_arr[t, 0]
|
|
1643
|
-
sd1 = sun_dirs_arr[t, 1]
|
|
1644
|
-
sd2 = sun_dirs_arr[t, 2]
|
|
1645
|
-
# Skip below horizon quickly: dz <= 0 implies elevation<=0
|
|
1646
|
-
if sd2 <= 0.0:
|
|
1647
|
-
# diffuse only
|
|
1648
|
-
diff_val = svf * dhi
|
|
1649
|
-
if diff_val > dhi:
|
|
1650
|
-
diff_val = dhi
|
|
1651
|
-
accum_diff += diff_val * time_step_hours
|
|
1652
|
-
accum_glob += diff_val * time_step_hours
|
|
1653
|
-
continue
|
|
1654
|
-
|
|
1655
|
-
# Direct
|
|
1656
|
-
cos_inc = normal[0]*sd0 + normal[1]*sd1 + normal[2]*sd2
|
|
1657
|
-
direct_val = 0.0
|
|
1658
|
-
if (dni > 0.0) and (cos_inc > 0.0):
|
|
1659
|
-
blocked, T = _trace_direct_masked(
|
|
1660
|
-
vox_is_tree,
|
|
1661
|
-
vox_is_opaque,
|
|
1662
|
-
origin,
|
|
1663
|
-
np.array((sd0, sd1, sd2), dtype=np.float64),
|
|
1664
|
-
att
|
|
1665
|
-
)
|
|
1666
|
-
if not blocked:
|
|
1667
|
-
direct_val = dni * cos_inc * T
|
|
1668
|
-
|
|
1669
|
-
diff_val = svf * dhi
|
|
1670
|
-
if diff_val > dhi:
|
|
1671
|
-
diff_val = dhi
|
|
1672
|
-
|
|
1673
|
-
accum_dir += direct_val * time_step_hours
|
|
1674
|
-
accum_diff += diff_val * time_step_hours
|
|
1675
|
-
accum_glob += (direct_val + diff_val) * time_step_hours
|
|
1676
|
-
|
|
1677
|
-
out_dir[fidx] = accum_dir
|
|
1678
|
-
out_diff[fidx] = accum_diff
|
|
1679
|
-
out_glob[fidx] = accum_glob
|
|
1680
|
-
|
|
1681
|
-
return out_dir, out_diff, out_glob
|
|
1682
|
-
|
|
1683
|
-
##############################################################################
|
|
1684
|
-
# 4) Modified get_cumulative_building_solar_irradiance
|
|
1685
|
-
##############################################################################
|
|
1686
|
-
def get_cumulative_building_solar_irradiance(
|
|
1687
|
-
voxel_data,
|
|
1688
|
-
meshsize,
|
|
1689
|
-
building_svf_mesh,
|
|
1690
|
-
weather_df,
|
|
1691
|
-
lon, lat, tz,
|
|
1692
|
-
**kwargs
|
|
1693
|
-
):
|
|
1694
|
-
"""
|
|
1695
|
-
Calculate cumulative solar irradiance on building surfaces over a time period.
|
|
1696
|
-
Uses the Numba-accelerated get_building_solar_irradiance for each time step.
|
|
1697
|
-
|
|
1698
|
-
Args:
|
|
1699
|
-
voxel_data (ndarray): 3D array of voxel values.
|
|
1700
|
-
meshsize (float): Size of each voxel in meters.
|
|
1701
|
-
building_svf_mesh (trimesh.Trimesh): Mesh with pre-calculated SVF in metadata.
|
|
1702
|
-
weather_df (DataFrame): Weather data with DNI (W/m²) and DHI (W/m²).
|
|
1703
|
-
lon (float): Longitude in degrees.
|
|
1704
|
-
lat (float): Latitude in degrees.
|
|
1705
|
-
tz (float): Timezone offset in hours.
|
|
1706
|
-
**kwargs: Additional parameters for time range, scaling, OBJ export, etc.
|
|
1707
|
-
|
|
1708
|
-
Returns:
|
|
1709
|
-
trimesh.Trimesh: A mesh with cumulative (Wh/m²) irradiance in metadata.
|
|
1710
|
-
"""
|
|
1711
|
-
import pytz
|
|
1712
|
-
from datetime import datetime
|
|
1713
|
-
import numpy as np
|
|
1714
|
-
|
|
1715
|
-
period_start = kwargs.get("period_start", "01-01 00:00:00")
|
|
1716
|
-
period_end = kwargs.get("period_end", "12-31 23:59:59")
|
|
1717
|
-
time_step_hours = kwargs.get("time_step_hours", 1.0)
|
|
1718
|
-
direct_normal_irradiance_scaling = kwargs.get("direct_normal_irradiance_scaling", 1.0)
|
|
1719
|
-
diffuse_irradiance_scaling = kwargs.get("diffuse_irradiance_scaling", 1.0)
|
|
1720
|
-
progress_report = kwargs.get("progress_report", False)
|
|
1721
|
-
fast_path = kwargs.get("fast_path", True)
|
|
1722
|
-
|
|
1723
|
-
# Parse times, create local tz
|
|
1724
|
-
try:
|
|
1725
|
-
start_dt = datetime.strptime(period_start, "%m-%d %H:%M:%S")
|
|
1726
|
-
end_dt = datetime.strptime(period_end, "%m-%d %H:%M:%S")
|
|
1727
|
-
except ValueError as ve:
|
|
1728
|
-
raise ValueError("Time must be in format 'MM-DD HH:MM:SS'") from ve
|
|
1729
|
-
|
|
1730
|
-
offset_minutes = int(tz * 60)
|
|
1731
|
-
local_tz = pytz.FixedOffset(offset_minutes)
|
|
1732
|
-
|
|
1733
|
-
# Filter weather_df
|
|
1734
|
-
df_period = weather_df[
|
|
1735
|
-
((weather_df.index.month > start_dt.month) |
|
|
1736
|
-
((weather_df.index.month == start_dt.month) &
|
|
1737
|
-
(weather_df.index.day >= start_dt.day) &
|
|
1738
|
-
(weather_df.index.hour >= start_dt.hour))) &
|
|
1739
|
-
((weather_df.index.month < end_dt.month) |
|
|
1740
|
-
((weather_df.index.month == end_dt.month) &
|
|
1741
|
-
(weather_df.index.day <= end_dt.day) &
|
|
1742
|
-
(weather_df.index.hour <= end_dt.hour)))
|
|
1743
|
-
]
|
|
1744
|
-
if df_period.empty:
|
|
1745
|
-
raise ValueError("No weather data in specified period.")
|
|
1746
|
-
|
|
1747
|
-
# Convert to local time, then to UTC
|
|
1748
|
-
df_period_local = df_period.copy()
|
|
1749
|
-
df_period_local.index = df_period_local.index.tz_localize(local_tz)
|
|
1750
|
-
df_period_utc = df_period_local.tz_convert(pytz.UTC)
|
|
1751
|
-
|
|
1752
|
-
# Get solar positions (allow precomputed to avoid recomputation)
|
|
1753
|
-
precomputed_solar_positions = kwargs.get("precomputed_solar_positions", None)
|
|
1754
|
-
if precomputed_solar_positions is not None and len(precomputed_solar_positions) == len(df_period_utc.index):
|
|
1755
|
-
solar_positions = precomputed_solar_positions
|
|
1756
|
-
else:
|
|
1757
|
-
solar_positions = get_solar_positions_astral(df_period_utc.index, lon, lat)
|
|
1758
|
-
|
|
1759
|
-
# Precompute arrays to avoid per-iteration pandas operations
|
|
1760
|
-
times_len = len(df_period_utc.index)
|
|
1761
|
-
azimuth_deg_arr = solar_positions['azimuth'].to_numpy()
|
|
1762
|
-
elev_deg_arr = solar_positions['elevation'].to_numpy()
|
|
1763
|
-
az_rad_arr = np.deg2rad(180.0 - azimuth_deg_arr)
|
|
1764
|
-
el_rad_arr = np.deg2rad(elev_deg_arr)
|
|
1765
|
-
sun_dx_arr = np.cos(el_rad_arr) * np.cos(az_rad_arr)
|
|
1766
|
-
sun_dy_arr = np.cos(el_rad_arr) * np.sin(az_rad_arr)
|
|
1767
|
-
sun_dz_arr = np.sin(el_rad_arr)
|
|
1768
|
-
sun_dirs_arr = np.stack([sun_dx_arr, sun_dy_arr, sun_dz_arr], axis=1).astype(np.float64)
|
|
1769
|
-
DNI_arr = (df_period_utc['DNI'].to_numpy() * direct_normal_irradiance_scaling).astype(np.float64)
|
|
1770
|
-
DHI_arr = (df_period_utc['DHI'].to_numpy() * diffuse_irradiance_scaling).astype(np.float64)
|
|
1771
|
-
sun_above_mask = elev_deg_arr > 0.0
|
|
1772
|
-
|
|
1773
|
-
# Prepare arrays for accumulation
|
|
1774
|
-
n_faces = len(building_svf_mesh.faces)
|
|
1775
|
-
face_cum_direct = np.zeros(n_faces, dtype=np.float64)
|
|
1776
|
-
face_cum_diffuse = np.zeros(n_faces, dtype=np.float64)
|
|
1777
|
-
face_cum_global = np.zeros(n_faces, dtype=np.float64)
|
|
1778
|
-
|
|
1779
|
-
# Pre-extract mesh face arrays and domain bounds for fast path
|
|
1780
|
-
# Optionally reuse precomputed geometry/bounds
|
|
1781
|
-
precomputed_geometry = kwargs.get("precomputed_geometry", None)
|
|
1782
|
-
if precomputed_geometry is not None:
|
|
1783
|
-
face_centers = precomputed_geometry.get("face_centers", building_svf_mesh.triangles_center)
|
|
1784
|
-
face_normals = precomputed_geometry.get("face_normals", building_svf_mesh.face_normals)
|
|
1785
|
-
face_svf = precomputed_geometry.get(
|
|
1786
|
-
"face_svf",
|
|
1787
|
-
building_svf_mesh.metadata['svf'] if ('svf' in building_svf_mesh.metadata) else np.zeros(n_faces, dtype=np.float64)
|
|
1788
|
-
)
|
|
1789
|
-
grid_bounds_real = precomputed_geometry.get("grid_bounds_real", None)
|
|
1790
|
-
boundary_epsilon = precomputed_geometry.get("boundary_epsilon", None)
|
|
1791
|
-
else:
|
|
1792
|
-
face_centers = building_svf_mesh.triangles_center
|
|
1793
|
-
face_normals = building_svf_mesh.face_normals
|
|
1794
|
-
face_svf = building_svf_mesh.metadata['svf'] if ('svf' in building_svf_mesh.metadata) else np.zeros(n_faces, dtype=np.float64)
|
|
1795
|
-
grid_bounds_real = None
|
|
1796
|
-
boundary_epsilon = None
|
|
1797
|
-
|
|
1798
|
-
if grid_bounds_real is None or boundary_epsilon is None:
|
|
1799
|
-
grid_shape = voxel_data.shape
|
|
1800
|
-
grid_bounds_voxel = np.array([[0, 0, 0], [grid_shape[0], grid_shape[1], grid_shape[2]]], dtype=np.float64)
|
|
1801
|
-
grid_bounds_real = grid_bounds_voxel * meshsize
|
|
1802
|
-
boundary_epsilon = meshsize * 0.05
|
|
1803
|
-
|
|
1804
|
-
# Params used in Numba kernel
|
|
1805
|
-
hit_values = (0,) # sky
|
|
1806
|
-
inclusion_mode = False # any non-sky is obstacle but trees transmit
|
|
1807
|
-
tree_k = kwargs.get("tree_k", 0.6)
|
|
1808
|
-
tree_lad = kwargs.get("tree_lad", 1.0)
|
|
1809
|
-
|
|
1810
|
-
boundary_mask = None
|
|
1811
|
-
instant_kwargs = kwargs.copy()
|
|
1812
|
-
instant_kwargs['obj_export'] = False
|
|
1813
|
-
|
|
1814
|
-
total_steps = times_len
|
|
1815
|
-
progress_every = max(1, total_steps // 20) # ~5% steps
|
|
1816
|
-
|
|
1817
|
-
# Pre-cast stable arrays to avoid repeated allocations
|
|
1818
|
-
face_centers64 = (face_centers if isinstance(face_centers, np.ndarray) else building_svf_mesh.triangles_center).astype(np.float64)
|
|
1819
|
-
face_normals64 = (face_normals if isinstance(face_normals, np.ndarray) else building_svf_mesh.face_normals).astype(np.float64)
|
|
1820
|
-
face_svf64 = face_svf.astype(np.float64)
|
|
1821
|
-
x_min, y_min, z_min = grid_bounds_real[0, 0], grid_bounds_real[0, 1], grid_bounds_real[0, 2]
|
|
1822
|
-
x_max, y_max, z_max = grid_bounds_real[1, 0], grid_bounds_real[1, 1], grid_bounds_real[1, 2]
|
|
1823
|
-
|
|
1824
|
-
if fast_path:
|
|
1825
|
-
# Use masked cumulative kernel with chunking to minimize Python overhead
|
|
1826
|
-
precomputed_masks = kwargs.get("precomputed_masks", None)
|
|
1827
|
-
if precomputed_masks is not None:
|
|
1828
|
-
vox_is_tree = precomputed_masks.get("vox_is_tree", (voxel_data == -2))
|
|
1829
|
-
vox_is_opaque = precomputed_masks.get("vox_is_opaque", (voxel_data != 0) & (voxel_data != -2))
|
|
1830
|
-
att = float(precomputed_masks.get("att", np.exp(-tree_k * tree_lad * meshsize)))
|
|
1831
|
-
else:
|
|
1832
|
-
vox_is_tree = (voxel_data == -2)
|
|
1833
|
-
vox_is_opaque = (voxel_data != 0) & (~vox_is_tree)
|
|
1834
|
-
att = float(np.exp(-tree_k * tree_lad * meshsize))
|
|
1835
|
-
|
|
1836
|
-
# Auto-tune chunk size if user didn't pass one
|
|
1837
|
-
time_batch_size = _auto_time_batch_size(n_faces, total_steps, kwargs.get("time_batch_size", None))
|
|
1838
|
-
if progress_report:
|
|
1839
|
-
print(f"Faces: {n_faces:,}, Timesteps: {total_steps:,}, Batch size: {time_batch_size}")
|
|
1840
|
-
|
|
1841
|
-
for start in range(0, total_steps, time_batch_size):
|
|
1842
|
-
end = min(start + time_batch_size, total_steps)
|
|
1843
|
-
# Accumulate Wh/m² over this chunk inside the kernel
|
|
1844
|
-
ch_dir, ch_diff, ch_glob = compute_cumulative_solar_irradiance_faces_masked_timeseries(
|
|
1845
|
-
face_centers64,
|
|
1846
|
-
face_normals64,
|
|
1847
|
-
face_svf64,
|
|
1848
|
-
sun_dirs_arr,
|
|
1849
|
-
DNI_arr,
|
|
1850
|
-
DHI_arr,
|
|
1851
|
-
vox_is_tree,
|
|
1852
|
-
vox_is_opaque,
|
|
1853
|
-
float(meshsize),
|
|
1854
|
-
float(att),
|
|
1855
|
-
float(x_min), float(y_min), float(z_min),
|
|
1856
|
-
float(x_max), float(y_max), float(z_max),
|
|
1857
|
-
float(boundary_epsilon),
|
|
1858
|
-
int(start), int(end),
|
|
1859
|
-
float(time_step_hours)
|
|
1860
|
-
)
|
|
1861
|
-
face_cum_direct += ch_dir
|
|
1862
|
-
face_cum_diffuse += ch_diff
|
|
1863
|
-
face_cum_global += ch_glob
|
|
1864
|
-
|
|
1865
|
-
if progress_report:
|
|
1866
|
-
pct = (end * 100.0) / total_steps
|
|
1867
|
-
print(f"Cumulative irradiance: {end}/{total_steps} ({pct:.1f}%)")
|
|
1868
|
-
else:
|
|
1869
|
-
# Iterate per timestep (fallback)
|
|
1870
|
-
for idx in range(total_steps):
|
|
1871
|
-
DNI = float(DNI_arr[idx])
|
|
1872
|
-
DHI = float(DHI_arr[idx])
|
|
1873
|
-
|
|
1874
|
-
# Skip if sun below horizon
|
|
1875
|
-
if not sun_above_mask[idx]:
|
|
1876
|
-
# Only diffuse term contributes (still based on SVF)
|
|
1877
|
-
if boundary_mask is None:
|
|
1878
|
-
boundary_mask = np.isnan(face_svf)
|
|
1879
|
-
# Accumulate diffuse only
|
|
1880
|
-
face_cum_diffuse += np.nan_to_num(face_svf * DHI) * time_step_hours
|
|
1881
|
-
face_cum_global += np.nan_to_num(face_svf * DHI) * time_step_hours
|
|
1882
|
-
# progress
|
|
1883
|
-
if progress_report:
|
|
1884
|
-
if ((idx + 1) % progress_every == 0) or (idx == total_steps - 1):
|
|
1885
|
-
pct = (idx + 1) * 100.0 / total_steps
|
|
1886
|
-
print(f"Cumulative irradiance: {idx+1}/{total_steps} ({pct:.1f}%)")
|
|
1887
|
-
continue
|
|
1888
|
-
|
|
1889
|
-
# Fallback to wrapper per-timestep
|
|
1890
|
-
irr_mesh = get_building_solar_irradiance(
|
|
1891
|
-
voxel_data,
|
|
1892
|
-
meshsize,
|
|
1893
|
-
building_svf_mesh,
|
|
1894
|
-
float(azimuth_deg_arr[idx]),
|
|
1895
|
-
float(elev_deg_arr[idx]),
|
|
1896
|
-
DNI,
|
|
1897
|
-
DHI,
|
|
1898
|
-
show_plot=False,
|
|
1899
|
-
**instant_kwargs
|
|
1900
|
-
)
|
|
1901
|
-
face_direct = irr_mesh.metadata['direct']
|
|
1902
|
-
face_diffuse = irr_mesh.metadata['diffuse']
|
|
1903
|
-
face_global = irr_mesh.metadata['global']
|
|
1904
|
-
|
|
1905
|
-
# If first time, note boundary mask from NaNs
|
|
1906
|
-
if boundary_mask is None:
|
|
1907
|
-
boundary_mask = np.isnan(face_global)
|
|
1908
|
-
|
|
1909
|
-
# Convert from W/m² to Wh/m² by multiplying time_step_hours
|
|
1910
|
-
face_cum_direct += np.nan_to_num(face_direct) * time_step_hours
|
|
1911
|
-
face_cum_diffuse += np.nan_to_num(face_diffuse) * time_step_hours
|
|
1912
|
-
face_cum_global += np.nan_to_num(face_global) * time_step_hours
|
|
1913
|
-
|
|
1914
|
-
if progress_report and (((idx + 1) % progress_every == 0) or (idx == total_steps - 1)):
|
|
1915
|
-
pct = (idx + 1) * 100.0 / total_steps
|
|
1916
|
-
print(f"Cumulative irradiance: {idx+1}/{total_steps} ({pct:.1f}%)")
|
|
1917
|
-
|
|
1918
|
-
# Reapply NaN for boundary
|
|
1919
|
-
if boundary_mask is not None:
|
|
1920
|
-
face_cum_direct[boundary_mask] = np.nan
|
|
1921
|
-
face_cum_diffuse[boundary_mask] = np.nan
|
|
1922
|
-
face_cum_global[boundary_mask] = np.nan
|
|
1923
|
-
|
|
1924
|
-
# Create a new mesh with cumulative results
|
|
1925
|
-
cumulative_mesh = building_svf_mesh.copy()
|
|
1926
|
-
if not hasattr(cumulative_mesh, 'metadata'):
|
|
1927
|
-
cumulative_mesh.metadata = {}
|
|
1928
|
-
|
|
1929
|
-
# If original mesh had SVF
|
|
1930
|
-
if 'svf' in building_svf_mesh.metadata:
|
|
1931
|
-
cumulative_mesh.metadata['svf'] = building_svf_mesh.metadata['svf']
|
|
1932
|
-
|
|
1933
|
-
cumulative_mesh.metadata['direct'] = face_cum_direct
|
|
1934
|
-
cumulative_mesh.metadata['diffuse'] = face_cum_diffuse
|
|
1935
|
-
cumulative_mesh.metadata['global'] = face_cum_global
|
|
1936
|
-
|
|
1937
|
-
cumulative_mesh.name = "Cumulative Solar Irradiance (Wh/m²)"
|
|
1938
|
-
|
|
1939
|
-
# # Optional OBJ export
|
|
1940
|
-
# obj_export = kwargs.get("obj_export", False)
|
|
1941
|
-
# if obj_export:
|
|
1942
|
-
# # Get export parameters
|
|
1943
|
-
# output_dir = kwargs.get("output_directory", "output")
|
|
1944
|
-
# output_file_name = kwargs.get("output_file_name", "solar_irradiance")
|
|
1945
|
-
|
|
1946
|
-
# # Export the mesh directly
|
|
1947
|
-
# irradiance_mesh.export(f"{output_dir}/{output_file_name}.obj")
|
|
1948
|
-
|
|
1949
|
-
return cumulative_mesh
|
|
1950
|
-
|
|
1951
|
-
def get_building_global_solar_irradiance_using_epw(
|
|
1952
|
-
voxel_data,
|
|
1953
|
-
meshsize,
|
|
1954
|
-
calc_type='instantaneous',
|
|
1955
|
-
direct_normal_irradiance_scaling=1.0,
|
|
1956
|
-
diffuse_irradiance_scaling=1.0,
|
|
1957
|
-
**kwargs
|
|
1958
|
-
):
|
|
1959
|
-
"""
|
|
1960
|
-
Compute global solar irradiance on building surfaces using EPW weather data, either for a single time or cumulatively.
|
|
1961
|
-
|
|
1962
|
-
The function:
|
|
1963
|
-
1. Optionally downloads and reads EPW weather data
|
|
1964
|
-
2. Handles timezone conversions and solar position calculations
|
|
1965
|
-
3. Computes either instantaneous or cumulative irradiance on building surfaces
|
|
1966
|
-
4. Supports visualization and export options
|
|
1967
|
-
|
|
1968
|
-
Args:
|
|
1969
|
-
voxel_data (ndarray): 3D array of voxel values.
|
|
1970
|
-
meshsize (float): Size of each voxel in meters.
|
|
1971
|
-
building_svf_mesh (trimesh.Trimesh): Building mesh with pre-calculated SVF values in metadata.
|
|
1972
|
-
calc_type (str): 'instantaneous' or 'cumulative'.
|
|
1973
|
-
direct_normal_irradiance_scaling (float): Scaling factor for direct normal irradiance.
|
|
1974
|
-
diffuse_irradiance_scaling (float): Scaling factor for diffuse horizontal irradiance.
|
|
1975
|
-
**kwargs: Additional arguments including:
|
|
1976
|
-
- download_nearest_epw (bool): Whether to download nearest EPW file
|
|
1977
|
-
- epw_file_path (str): Path to EPW file
|
|
1978
|
-
- rectangle_vertices (list): List of (lon,lat) coordinates for EPW download
|
|
1979
|
-
- output_dir (str): Directory for EPW download
|
|
1980
|
-
- calc_time (str): Time for instantaneous calculation ('MM-DD HH:MM:SS')
|
|
1981
|
-
- period_start (str): Start time for cumulative calculation ('MM-DD HH:MM:SS')
|
|
1982
|
-
- period_end (str): End time for cumulative calculation ('MM-DD HH:MM:SS')
|
|
1983
|
-
- time_step_hours (float): Time step for cumulative calculation
|
|
1984
|
-
- tree_k (float): Tree extinction coefficient
|
|
1985
|
-
- tree_lad (float): Leaf area density in m^-1
|
|
1986
|
-
- show_each_timestep (bool): Whether to show plots for each timestep
|
|
1987
|
-
- nan_color (str): Color for NaN values in visualization
|
|
1988
|
-
- colormap (str): Matplotlib colormap name
|
|
1989
|
-
- vmin (float): Minimum value for colormap
|
|
1990
|
-
- vmax (float): Maximum value for colormap
|
|
1991
|
-
- obj_export (bool): Whether to export as OBJ file
|
|
1992
|
-
- output_directory (str): Directory for OBJ export
|
|
1993
|
-
- output_file_name (str): Filename for OBJ export
|
|
1994
|
-
- save_mesh (bool): Whether to save the mesh data using pickle
|
|
1995
|
-
- mesh_output_path (str): Path to save the mesh data (if save_mesh is True)
|
|
1996
|
-
|
|
1997
|
-
Returns:
|
|
1998
|
-
trimesh.Trimesh: Building mesh with irradiance values stored in metadata.
|
|
1999
|
-
"""
|
|
2000
|
-
import numpy as np
|
|
2001
|
-
import pytz
|
|
2002
|
-
from datetime import datetime
|
|
2003
|
-
|
|
2004
|
-
# Get EPW file
|
|
2005
|
-
download_nearest_epw = kwargs.get("download_nearest_epw", False)
|
|
2006
|
-
rectangle_vertices = kwargs.get("rectangle_vertices", None)
|
|
2007
|
-
epw_file_path = kwargs.get("epw_file_path", None)
|
|
2008
|
-
building_id_grid = kwargs.get("building_id_grid", None)
|
|
2009
|
-
building_svf_mesh = kwargs.get("building_svf_mesh", None)
|
|
2010
|
-
progress_report = kwargs.get("progress_report", False)
|
|
2011
|
-
fast_path = kwargs.get("fast_path", True)
|
|
2012
|
-
|
|
2013
|
-
# Threading tuning (auto): choose sensible defaults based on hardware
|
|
2014
|
-
desired_threads = kwargs.get("numba_num_threads", None)
|
|
2015
|
-
_configure_num_threads(desired_threads, progress=kwargs.get("progress_report", False))
|
|
2016
|
-
|
|
2017
|
-
if download_nearest_epw:
|
|
2018
|
-
if rectangle_vertices is None:
|
|
2019
|
-
print("rectangle_vertices is required to download nearest EPW file")
|
|
2020
|
-
return None
|
|
2021
|
-
else:
|
|
2022
|
-
# Calculate center point of rectangle
|
|
2023
|
-
lons = [coord[0] for coord in rectangle_vertices]
|
|
2024
|
-
lats = [coord[1] for coord in rectangle_vertices]
|
|
2025
|
-
center_lon = (min(lons) + max(lons)) / 2
|
|
2026
|
-
center_lat = (min(lats) + max(lats)) / 2
|
|
2027
|
-
|
|
2028
|
-
# Optional: specify maximum distance in kilometers
|
|
2029
|
-
max_distance = kwargs.get("max_distance", 100) # None for no limit
|
|
2030
|
-
output_dir = kwargs.get("output_dir", "output")
|
|
2031
|
-
|
|
2032
|
-
epw_file_path, weather_data, metadata = get_nearest_epw_from_climate_onebuilding(
|
|
2033
|
-
longitude=center_lon,
|
|
2034
|
-
latitude=center_lat,
|
|
2035
|
-
output_dir=output_dir,
|
|
2036
|
-
max_distance=max_distance,
|
|
2037
|
-
extract_zip=True,
|
|
2038
|
-
load_data=True,
|
|
2039
|
-
allow_insecure_ssl=kwargs.get("allow_insecure_ssl", False),
|
|
2040
|
-
allow_http_fallback=kwargs.get("allow_http_fallback", False),
|
|
2041
|
-
ssl_verify=kwargs.get("ssl_verify", True)
|
|
2042
|
-
)
|
|
2043
|
-
|
|
2044
|
-
# Read EPW data
|
|
2045
|
-
if epw_file_path is None:
|
|
2046
|
-
raise RuntimeError("EPW file path is None. Set 'epw_file_path' or enable 'download_nearest_epw' and ensure network succeeds.")
|
|
2047
|
-
df, lon, lat, tz, elevation_m = read_epw_for_solar_simulation(epw_file_path)
|
|
2048
|
-
if df.empty:
|
|
2049
|
-
raise ValueError("No data in EPW file.")
|
|
2050
|
-
|
|
2051
|
-
# Step 1: Calculate Sky View Factor for building surfaces (unless provided)
|
|
2052
|
-
if building_svf_mesh is None:
|
|
2053
|
-
if progress_report:
|
|
2054
|
-
print("Processing Sky View Factor for building surfaces...")
|
|
2055
|
-
# Allow passing through specific SVF parameters via kwargs
|
|
2056
|
-
svf_kwargs = {
|
|
2057
|
-
'value_name': 'svf',
|
|
2058
|
-
'target_values': (0,),
|
|
2059
|
-
'inclusion_mode': False,
|
|
2060
|
-
'building_id_grid': building_id_grid,
|
|
2061
|
-
'progress_report': progress_report,
|
|
2062
|
-
'fast_path': fast_path,
|
|
2063
|
-
}
|
|
2064
|
-
# Permit overrides
|
|
2065
|
-
for k in ("N_azimuth","N_elevation","tree_k","tree_lad","debug"):
|
|
2066
|
-
if k in kwargs:
|
|
2067
|
-
svf_kwargs[k] = kwargs[k]
|
|
2068
|
-
building_svf_mesh = get_surface_view_factor(
|
|
2069
|
-
voxel_data,
|
|
2070
|
-
meshsize,
|
|
2071
|
-
**svf_kwargs
|
|
2072
|
-
)
|
|
2073
|
-
|
|
2074
|
-
if progress_report:
|
|
2075
|
-
print(f"SVF ready. Faces: {len(building_svf_mesh.faces):,}")
|
|
2076
|
-
|
|
2077
|
-
# Step 2: Build precomputed caches (geometry, masks, attenuation) for speed
|
|
2078
|
-
precomputed_geometry = {}
|
|
2079
|
-
try:
|
|
2080
|
-
grid_shape = voxel_data.shape
|
|
2081
|
-
grid_bounds_voxel = np.array([[0, 0, 0], [grid_shape[0], grid_shape[1], grid_shape[2]]], dtype=np.float64)
|
|
2082
|
-
grid_bounds_real = grid_bounds_voxel * meshsize
|
|
2083
|
-
boundary_epsilon = meshsize * 0.05
|
|
2084
|
-
precomputed_geometry = {
|
|
2085
|
-
'face_centers': building_svf_mesh.triangles_center,
|
|
2086
|
-
'face_normals': building_svf_mesh.face_normals,
|
|
2087
|
-
'face_svf': building_svf_mesh.metadata['svf'] if ('svf' in building_svf_mesh.metadata) else None,
|
|
2088
|
-
'grid_bounds_real': grid_bounds_real,
|
|
2089
|
-
'boundary_epsilon': boundary_epsilon,
|
|
2090
|
-
}
|
|
2091
|
-
except Exception:
|
|
2092
|
-
# Fallback silently
|
|
2093
|
-
precomputed_geometry = {}
|
|
2094
|
-
|
|
2095
|
-
tree_k = kwargs.get("tree_k", 0.6)
|
|
2096
|
-
tree_lad = kwargs.get("tree_lad", 1.0)
|
|
2097
|
-
precomputed_masks = {
|
|
2098
|
-
'vox_is_tree': (voxel_data == -2),
|
|
2099
|
-
'vox_is_opaque': (voxel_data != 0) & (voxel_data != -2),
|
|
2100
|
-
'att': float(np.exp(-tree_k * tree_lad * meshsize)),
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
|
-
if progress_report:
|
|
2104
|
-
t_cnt = int(np.count_nonzero(precomputed_masks['vox_is_tree']))
|
|
2105
|
-
b_cnt = int(np.count_nonzero(voxel_data == -3)) if hasattr(voxel_data, 'shape') else 0
|
|
2106
|
-
print(f"Precomputed caches: trees={t_cnt:,}, buildings={b_cnt:,}, tree_att_per_voxel={precomputed_masks['att']:.4f}")
|
|
2107
|
-
print(f"Processing Solar Irradiance for building surfaces...")
|
|
2108
|
-
result_mesh = None
|
|
2109
|
-
|
|
2110
|
-
if calc_type == 'instantaneous':
|
|
2111
|
-
calc_time = kwargs.get("calc_time", "01-01 12:00:00")
|
|
2112
|
-
|
|
2113
|
-
# Parse calculation time without year
|
|
2114
|
-
try:
|
|
2115
|
-
calc_dt = datetime.strptime(calc_time, "%m-%d %H:%M:%S")
|
|
2116
|
-
except ValueError as ve:
|
|
2117
|
-
raise ValueError("calc_time must be in format 'MM-DD HH:MM:SS'") from ve
|
|
2118
|
-
|
|
2119
|
-
df_period = df[
|
|
2120
|
-
(df.index.month == calc_dt.month) & (df.index.day == calc_dt.day) & (df.index.hour == calc_dt.hour)
|
|
2121
|
-
]
|
|
2122
|
-
|
|
2123
|
-
if df_period.empty:
|
|
2124
|
-
raise ValueError("No EPW data at the specified time.")
|
|
2125
|
-
|
|
2126
|
-
# Prepare timezone conversion
|
|
2127
|
-
offset_minutes = int(tz * 60)
|
|
2128
|
-
local_tz = pytz.FixedOffset(offset_minutes)
|
|
2129
|
-
df_period_local = df_period.copy()
|
|
2130
|
-
df_period_local.index = df_period_local.index.tz_localize(local_tz)
|
|
2131
|
-
df_period_utc = df_period_local.tz_convert(pytz.UTC)
|
|
2132
|
-
|
|
2133
|
-
# Compute solar positions
|
|
2134
|
-
solar_positions = get_solar_positions_astral(df_period_utc.index, lon, lat)
|
|
2135
|
-
|
|
2136
|
-
# Scale irradiance values
|
|
2137
|
-
direct_normal_irradiance = df_period_utc.iloc[0]['DNI'] * direct_normal_irradiance_scaling
|
|
2138
|
-
diffuse_irradiance = df_period_utc.iloc[0]['DHI'] * diffuse_irradiance_scaling
|
|
2139
|
-
|
|
2140
|
-
# Get solar position
|
|
2141
|
-
azimuth_degrees = solar_positions.iloc[0]['azimuth']
|
|
2142
|
-
elevation_degrees = solar_positions.iloc[0]['elevation']
|
|
2143
|
-
|
|
2144
|
-
if progress_report:
|
|
2145
|
-
print(f"Time: {df_period_local.index[0].strftime('%Y-%m-%d %H:%M:%S')}")
|
|
2146
|
-
print(f"Sun position: Azimuth {azimuth_degrees:.1f}°, Elevation {elevation_degrees:.1f}°")
|
|
2147
|
-
print(f"DNI: {direct_normal_irradiance:.1f} W/m², DHI: {diffuse_irradiance:.1f} W/m²")
|
|
2148
|
-
|
|
2149
|
-
# Skip if sun is below horizon
|
|
2150
|
-
if elevation_degrees <= 0:
|
|
2151
|
-
if progress_report:
|
|
2152
|
-
print("Sun is below horizon, skipping calculation.")
|
|
2153
|
-
result_mesh = building_svf_mesh.copy()
|
|
2154
|
-
else:
|
|
2155
|
-
# Compute irradiance
|
|
2156
|
-
_call_kwargs = kwargs.copy()
|
|
2157
|
-
if 'progress_report' in _call_kwargs:
|
|
2158
|
-
_call_kwargs.pop('progress_report')
|
|
2159
|
-
result_mesh = get_building_solar_irradiance(
|
|
2160
|
-
voxel_data,
|
|
2161
|
-
meshsize,
|
|
2162
|
-
building_svf_mesh,
|
|
2163
|
-
azimuth_degrees,
|
|
2164
|
-
elevation_degrees,
|
|
2165
|
-
direct_normal_irradiance,
|
|
2166
|
-
diffuse_irradiance,
|
|
2167
|
-
progress_report=progress_report,
|
|
2168
|
-
fast_path=fast_path,
|
|
2169
|
-
precomputed_geometry=precomputed_geometry,
|
|
2170
|
-
precomputed_masks=precomputed_masks,
|
|
2171
|
-
**_call_kwargs
|
|
2172
|
-
)
|
|
2173
|
-
|
|
2174
|
-
elif calc_type == 'cumulative':
|
|
2175
|
-
# Set default parameters
|
|
2176
|
-
period_start = kwargs.get("period_start", "01-01 00:00:00")
|
|
2177
|
-
period_end = kwargs.get("period_end", "12-31 23:59:59")
|
|
2178
|
-
time_step_hours = kwargs.get("time_step_hours", 1.0)
|
|
2179
|
-
|
|
2180
|
-
# Parse start and end times without year
|
|
2181
|
-
try:
|
|
2182
|
-
start_dt = datetime.strptime(period_start, "%m-%d %H:%M:%S")
|
|
2183
|
-
end_dt = datetime.strptime(period_end, "%m-%d %H:%M:%S")
|
|
2184
|
-
except ValueError as ve:
|
|
2185
|
-
raise ValueError("Time must be in format 'MM-DD HH:MM:SS'") from ve
|
|
2186
|
-
|
|
2187
|
-
# Create local timezone
|
|
2188
|
-
offset_minutes = int(tz * 60)
|
|
2189
|
-
local_tz = pytz.FixedOffset(offset_minutes)
|
|
2190
|
-
|
|
2191
|
-
# Filter weather data by month, day, hour
|
|
2192
|
-
df_period = df[
|
|
2193
|
-
((df.index.month > start_dt.month) |
|
|
2194
|
-
((df.index.month == start_dt.month) & (df.index.day >= start_dt.day) &
|
|
2195
|
-
(df.index.hour >= start_dt.hour))) &
|
|
2196
|
-
((df.index.month < end_dt.month) |
|
|
2197
|
-
((df.index.month == end_dt.month) & (df.index.day <= end_dt.day) &
|
|
2198
|
-
(df.index.hour <= end_dt.hour)))
|
|
2199
|
-
]
|
|
2200
|
-
|
|
2201
|
-
if df_period.empty:
|
|
2202
|
-
raise ValueError("No weather data available for the specified period.")
|
|
2203
|
-
|
|
2204
|
-
# Convert to local timezone and then to UTC for solar position calculation
|
|
2205
|
-
df_period_local = df_period.copy()
|
|
2206
|
-
df_period_local.index = df_period_local.index.tz_localize(local_tz)
|
|
2207
|
-
df_period_utc = df_period_local.tz_convert(pytz.UTC)
|
|
2208
|
-
|
|
2209
|
-
# Get solar positions for all times
|
|
2210
|
-
solar_positions = get_solar_positions_astral(df_period_utc.index, lon, lat)
|
|
2211
|
-
|
|
2212
|
-
# Create a copy of kwargs without time_step_hours to avoid duplicate argument
|
|
2213
|
-
kwargs_copy = kwargs.copy()
|
|
2214
|
-
if 'time_step_hours' in kwargs_copy:
|
|
2215
|
-
del kwargs_copy['time_step_hours']
|
|
2216
|
-
|
|
2217
|
-
# Get cumulative irradiance - adapt to match expected function signature
|
|
2218
|
-
if progress_report:
|
|
2219
|
-
print(f"Calculating cumulative irradiance from {period_start} to {period_end}...")
|
|
2220
|
-
result_mesh = get_cumulative_building_solar_irradiance(
|
|
2221
|
-
voxel_data,
|
|
2222
|
-
meshsize,
|
|
2223
|
-
building_svf_mesh,
|
|
2224
|
-
df, lon, lat, tz, # Pass only the required 7 positional arguments
|
|
2225
|
-
period_start=period_start,
|
|
2226
|
-
period_end=period_end,
|
|
2227
|
-
time_step_hours=time_step_hours,
|
|
2228
|
-
direct_normal_irradiance_scaling=direct_normal_irradiance_scaling,
|
|
2229
|
-
diffuse_irradiance_scaling=diffuse_irradiance_scaling,
|
|
2230
|
-
progress_report=progress_report,
|
|
2231
|
-
fast_path=fast_path,
|
|
2232
|
-
precomputed_solar_positions=solar_positions,
|
|
2233
|
-
precomputed_geometry=precomputed_geometry,
|
|
2234
|
-
precomputed_masks=precomputed_masks,
|
|
2235
|
-
colormap=kwargs.get('colormap', 'jet'),
|
|
2236
|
-
show_each_timestep=kwargs.get('show_each_timestep', False),
|
|
2237
|
-
obj_export=kwargs.get('obj_export', False),
|
|
2238
|
-
output_directory=kwargs.get('output_directory', 'output'),
|
|
2239
|
-
output_file_name=kwargs.get('output_file_name', 'cumulative_solar')
|
|
2240
|
-
)
|
|
2241
|
-
|
|
2242
|
-
else:
|
|
2243
|
-
raise ValueError("calc_type must be either 'instantaneous' or 'cumulative'")
|
|
2244
|
-
|
|
2245
|
-
# Save mesh data if requested
|
|
2246
|
-
save_mesh = kwargs.get("save_mesh", False)
|
|
2247
|
-
if save_mesh:
|
|
2248
|
-
mesh_output_path = kwargs.get("mesh_output_path", None)
|
|
2249
|
-
if mesh_output_path is None:
|
|
2250
|
-
# Generate default path if none provided
|
|
2251
|
-
output_directory = kwargs.get("output_directory", "output")
|
|
2252
|
-
output_file_name = kwargs.get("output_file_name", f"{calc_type}_solar_irradiance")
|
|
2253
|
-
mesh_output_path = f"{output_directory}/{output_file_name}.pkl"
|
|
2254
|
-
|
|
2255
|
-
save_irradiance_mesh(result_mesh, mesh_output_path)
|
|
2256
|
-
print(f"Saved irradiance mesh data to: {mesh_output_path}")
|
|
2257
|
-
|
|
2258
|
-
return result_mesh
|
|
2259
|
-
|
|
2260
|
-
def save_irradiance_mesh(irradiance_mesh, output_file_path):
|
|
2261
|
-
"""
|
|
2262
|
-
Save the irradiance mesh data to a file using pickle serialization.
|
|
2263
|
-
|
|
2264
|
-
This function provides persistent storage for computed irradiance results,
|
|
2265
|
-
enabling reuse of expensive calculations and sharing of results between
|
|
2266
|
-
analysis sessions. The mesh data includes all geometry, irradiance values,
|
|
2267
|
-
and metadata required for visualization and further analysis.
|
|
2268
|
-
|
|
2269
|
-
Serialization Benefits:
|
|
2270
|
-
- Preserves complete mesh structure with all computed data
|
|
2271
|
-
- Enables offline analysis and visualization workflows
|
|
2272
|
-
- Supports sharing results between different tools and users
|
|
2273
|
-
- Avoids recomputation of expensive irradiance calculations
|
|
2274
|
-
|
|
2275
|
-
Data Preservation:
|
|
2276
|
-
- All mesh geometry (vertices, faces, normals)
|
|
2277
|
-
- Computed irradiance values (direct, diffuse, global)
|
|
2278
|
-
- Sky View Factor data and other metadata
|
|
2279
|
-
- Material properties and visualization settings
|
|
2280
|
-
|
|
2281
|
-
Args:
|
|
2282
|
-
irradiance_mesh (trimesh.Trimesh): Mesh with irradiance data in metadata
|
|
2283
|
-
Should contain computed irradiance results
|
|
2284
|
-
output_file_path (str): Path to save the mesh data file
|
|
2285
|
-
Recommended extension: .pkl for clarity
|
|
2286
|
-
|
|
2287
|
-
Note:
|
|
2288
|
-
The function automatically creates the output directory if it doesn't exist.
|
|
2289
|
-
Use pickle format for maximum compatibility with Python data structures.
|
|
2290
|
-
"""
|
|
2291
|
-
import pickle
|
|
2292
|
-
import os
|
|
2293
|
-
|
|
2294
|
-
# Create output directory structure if it doesn't exist
|
|
2295
|
-
os.makedirs(os.path.dirname(output_file_path), exist_ok=True)
|
|
2296
|
-
|
|
2297
|
-
# Serialize mesh data using pickle for complete data preservation
|
|
2298
|
-
with open(output_file_path, 'wb') as f:
|
|
2299
|
-
pickle.dump(irradiance_mesh, f)
|
|
2300
|
-
|
|
2301
|
-
def load_irradiance_mesh(input_file_path):
|
|
2302
|
-
"""
|
|
2303
|
-
Load previously saved irradiance mesh data from a file.
|
|
2304
|
-
|
|
2305
|
-
This function restores complete mesh data including geometry, computed
|
|
2306
|
-
irradiance values, and all associated metadata. It enables continuation
|
|
2307
|
-
of analysis workflows and reuse of expensive computation results.
|
|
2308
|
-
|
|
2309
|
-
Restoration Capabilities:
|
|
2310
|
-
- Complete mesh geometry with all topological information
|
|
2311
|
-
- All computed irradiance data (direct, diffuse, global components)
|
|
2312
|
-
- Sky View Factor values and analysis metadata
|
|
2313
|
-
- Visualization settings and material properties
|
|
2314
|
-
|
|
2315
|
-
Workflow Integration:
|
|
2316
|
-
- Load results from previous analysis sessions
|
|
2317
|
-
- Share computed data between team members
|
|
2318
|
-
- Perform post-processing and visualization
|
|
2319
|
-
- Compare results from different scenarios
|
|
2320
|
-
|
|
2321
|
-
Args:
|
|
2322
|
-
input_file_path (str): Path to the saved mesh data file
|
|
2323
|
-
Should be a file created by save_irradiance_mesh()
|
|
2324
|
-
|
|
2325
|
-
Returns:
|
|
2326
|
-
trimesh.Trimesh: Complete mesh with all irradiance data in metadata
|
|
2327
|
-
Ready for visualization, analysis, or further processing
|
|
2328
|
-
|
|
2329
|
-
Note:
|
|
2330
|
-
The loaded mesh maintains all original data structure and can be used
|
|
2331
|
-
immediately for visualization or additional analysis operations.
|
|
2332
|
-
"""
|
|
2333
|
-
import pickle
|
|
2334
|
-
|
|
2335
|
-
# Deserialize mesh data preserving all original structure
|
|
2336
|
-
with open(input_file_path, 'rb') as f:
|
|
2337
|
-
irradiance_mesh = pickle.load(f)
|
|
2338
|
-
|
|
2339
|
-
return irradiance_mesh
|