voxcity 0.5.13__py3-none-any.whl → 0.5.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of voxcity might be problematic. Click here for more details.

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