voxcity 0.3.0__py3-none-any.whl → 0.3.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.
Potentially problematic release.
This version of voxcity might be problematic. Click here for more details.
- voxcity/sim/__init_.py +1 -0
- voxcity/sim/solar.py +425 -72
- voxcity/sim/utils.py +6 -0
- voxcity/sim/view.py +114 -108
- voxcity/utils/__init_.py +2 -1
- voxcity/utils/weather.py +523 -0
- voxcity/voxcity.py +4 -1
- {voxcity-0.3.0.dist-info → voxcity-0.3.2.dist-info}/METADATA +2 -1
- {voxcity-0.3.0.dist-info → voxcity-0.3.2.dist-info}/RECORD +13 -11
- {voxcity-0.3.0.dist-info → voxcity-0.3.2.dist-info}/AUTHORS.rst +0 -0
- {voxcity-0.3.0.dist-info → voxcity-0.3.2.dist-info}/LICENSE +0 -0
- {voxcity-0.3.0.dist-info → voxcity-0.3.2.dist-info}/WHEEL +0 -0
- {voxcity-0.3.0.dist-info → voxcity-0.3.2.dist-info}/top_level.txt +0 -0
voxcity/sim/__init_.py
CHANGED
voxcity/sim/solar.py
CHANGED
|
@@ -1,24 +1,37 @@
|
|
|
1
1
|
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
2
3
|
import matplotlib.pyplot as plt
|
|
3
4
|
from numba import njit, prange
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
import pytz
|
|
7
|
+
from astral import Observer
|
|
8
|
+
from astral.sun import elevation, azimuth
|
|
4
9
|
|
|
5
10
|
from .view import trace_ray_generic, compute_vi_map_generic, get_sky_view_factor_map
|
|
11
|
+
from ..utils.weather import get_nearest_epw_from_climate_onebuilding, read_epw_for_solar_simulation
|
|
12
|
+
from ..file.obj import grid_to_obj, export_obj
|
|
6
13
|
|
|
7
14
|
@njit(parallel=True)
|
|
8
|
-
def compute_direct_solar_irradiance_map_binary(voxel_data, sun_direction,
|
|
15
|
+
def compute_direct_solar_irradiance_map_binary(voxel_data, sun_direction, view_point_height, hit_values, meshsize, tree_k, tree_lad, inclusion_mode):
|
|
9
16
|
"""
|
|
10
|
-
Compute a
|
|
17
|
+
Compute a map of direct solar irradiation accounting for tree transmittance.
|
|
11
18
|
|
|
12
19
|
Args:
|
|
13
20
|
voxel_data (ndarray): 3D array of voxel values.
|
|
14
21
|
sun_direction (tuple): Direction vector of the sun.
|
|
15
22
|
view_height_voxel (int): Observer height in voxel units.
|
|
16
|
-
hit_values (tuple): Values considered non-obstacles if inclusion_mode=False
|
|
23
|
+
hit_values (tuple): Values considered non-obstacles if inclusion_mode=False.
|
|
24
|
+
meshsize (float): Size of each voxel in meters.
|
|
25
|
+
tree_k (float): Tree extinction coefficient.
|
|
26
|
+
tree_lad (float): Leaf area density in m^-1.
|
|
17
27
|
inclusion_mode (bool): False here, meaning any voxel not in hit_values is an obstacle.
|
|
18
28
|
|
|
19
29
|
Returns:
|
|
20
|
-
ndarray: 2D array
|
|
30
|
+
ndarray: 2D array of transmittance values (0.0-1.0), NaN = invalid observer.
|
|
21
31
|
"""
|
|
32
|
+
|
|
33
|
+
view_height_voxel = int(view_point_height / meshsize)
|
|
34
|
+
|
|
22
35
|
nx, ny, nz = voxel_data.shape
|
|
23
36
|
irradiance_map = np.full((nx, ny), np.nan, dtype=np.float64)
|
|
24
37
|
|
|
@@ -32,23 +45,18 @@ def compute_direct_solar_irradiance_map_binary(voxel_data, sun_direction, view_h
|
|
|
32
45
|
for x in prange(nx):
|
|
33
46
|
for y in range(ny):
|
|
34
47
|
found_observer = False
|
|
35
|
-
# Find lowest empty voxel above ground
|
|
36
48
|
for z in range(1, nz):
|
|
37
|
-
# Check if this position is a valid observer location:
|
|
38
|
-
# voxel_data[x, y, z] in (0, -2) means it's air or ground-air interface (open)
|
|
39
|
-
# voxel_data[x, y, z-1] not in (0, -2) means below it is some ground or structure
|
|
40
49
|
if voxel_data[x, y, z] in (0, -2) and voxel_data[x, y, z - 1] not in (0, -2):
|
|
41
|
-
|
|
42
|
-
if voxel_data[x, y, z - 1] in (-3, 7, 8, 9):
|
|
43
|
-
# Invalid observer location
|
|
50
|
+
if voxel_data[x, y, z - 1] in (-30, -3, -2):
|
|
44
51
|
irradiance_map[x, y] = np.nan
|
|
45
52
|
found_observer = True
|
|
46
53
|
break
|
|
47
54
|
else:
|
|
48
55
|
# Place observer and cast a ray in sun direction
|
|
49
56
|
observer_location = np.array([x, y, z + view_height_voxel], dtype=np.float64)
|
|
50
|
-
hit = trace_ray_generic(voxel_data, observer_location, sd,
|
|
51
|
-
|
|
57
|
+
hit, transmittance = trace_ray_generic(voxel_data, observer_location, sd,
|
|
58
|
+
hit_values, meshsize, tree_k, tree_lad, inclusion_mode)
|
|
59
|
+
irradiance_map[x, y] = transmittance if not hit else 0.0
|
|
52
60
|
found_observer = True
|
|
53
61
|
break
|
|
54
62
|
if not found_observer:
|
|
@@ -56,15 +64,21 @@ def compute_direct_solar_irradiance_map_binary(voxel_data, sun_direction, view_h
|
|
|
56
64
|
|
|
57
65
|
return np.flipud(irradiance_map)
|
|
58
66
|
|
|
59
|
-
|
|
60
|
-
|
|
67
|
+
def get_direct_solar_irradiance_map(voxel_data, meshsize, azimuth_degrees_ori, elevation_degrees,
|
|
68
|
+
direct_normal_irradiance, show_plot=False, **kwargs):
|
|
69
|
+
"""
|
|
70
|
+
Compute direct solar irradiance map with tree transmittance.
|
|
71
|
+
"""
|
|
61
72
|
view_point_height = kwargs.get("view_point_height", 1.5)
|
|
62
|
-
|
|
63
|
-
colormap = kwargs.get("colormap", 'viridis')
|
|
73
|
+
colormap = kwargs.get("colormap", 'magma')
|
|
64
74
|
vmin = kwargs.get("vmin", 0.0)
|
|
65
75
|
vmax = kwargs.get("vmax", direct_normal_irradiance)
|
|
76
|
+
|
|
77
|
+
# Get tree transmittance parameters
|
|
78
|
+
tree_k = kwargs.get("tree_k", 0.6)
|
|
79
|
+
tree_lad = kwargs.get("tree_lad", 1.0)
|
|
66
80
|
|
|
67
|
-
# Convert angles to direction
|
|
81
|
+
# Convert angles to direction
|
|
68
82
|
azimuth_degrees = 180 - azimuth_degrees_ori
|
|
69
83
|
azimuth_radians = np.deg2rad(azimuth_degrees)
|
|
70
84
|
elevation_radians = np.deg2rad(elevation_degrees)
|
|
@@ -73,30 +87,30 @@ def get_direct_solar_irradiance_map(voxel_data, meshsize, azimuth_degrees_ori, e
|
|
|
73
87
|
dz = np.sin(elevation_radians)
|
|
74
88
|
sun_direction = (dx, dy, dz)
|
|
75
89
|
|
|
76
|
-
# All non-zero voxels are obstacles
|
|
90
|
+
# All non-zero voxels are obstacles except for trees which have transmittance
|
|
77
91
|
hit_values = (0,)
|
|
78
92
|
inclusion_mode = False
|
|
79
93
|
|
|
80
|
-
|
|
81
|
-
voxel_data, sun_direction,
|
|
94
|
+
transmittance_map = compute_direct_solar_irradiance_map_binary(
|
|
95
|
+
voxel_data, sun_direction, view_point_height, hit_values,
|
|
96
|
+
meshsize, tree_k, tree_lad, inclusion_mode
|
|
82
97
|
)
|
|
83
98
|
|
|
84
99
|
sin_elev = dz
|
|
85
|
-
direct_map =
|
|
100
|
+
direct_map = transmittance_map * direct_normal_irradiance * sin_elev
|
|
86
101
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
102
|
+
if show_plot:
|
|
103
|
+
cmap = plt.cm.get_cmap(colormap).copy()
|
|
104
|
+
cmap.set_bad(color='lightgray')
|
|
105
|
+
plt.figure(figsize=(10, 8))
|
|
106
|
+
plt.title("Horizontal Direct Solar Irradiance Map (0° = North)")
|
|
107
|
+
plt.imshow(direct_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
|
|
108
|
+
plt.colorbar(label='Direct Solar Irradiance (W/m²)')
|
|
109
|
+
plt.show()
|
|
95
110
|
|
|
96
111
|
# Optional OBJ export
|
|
97
112
|
obj_export = kwargs.get("obj_export", False)
|
|
98
113
|
if obj_export:
|
|
99
|
-
from ..file.obj import grid_to_obj
|
|
100
114
|
dem_grid = kwargs.get("dem_grid", np.zeros_like(direct_map))
|
|
101
115
|
output_dir = kwargs.get("output_directory", "output")
|
|
102
116
|
output_file_name = kwargs.get("output_file_name", "direct_solar_irradiance")
|
|
@@ -118,52 +132,45 @@ def get_direct_solar_irradiance_map(voxel_data, meshsize, azimuth_degrees_ori, e
|
|
|
118
132
|
|
|
119
133
|
return direct_map
|
|
120
134
|
|
|
121
|
-
|
|
122
|
-
def get_diffuse_solar_irradiance_map(voxel_data, meshsize, diffuse_irradiance=1.0, **kwargs):
|
|
135
|
+
def get_diffuse_solar_irradiance_map(voxel_data, meshsize, diffuse_irradiance=1.0, show_plot=False, **kwargs):
|
|
123
136
|
"""
|
|
124
|
-
Compute diffuse solar irradiance map using the Sky View Factor (SVF).
|
|
125
|
-
Diffuse = SVF * diffuse_irradiance.
|
|
126
|
-
|
|
127
|
-
No mode or hit_values needed since this calculation relies on the SVF which is internally computed.
|
|
128
|
-
|
|
129
|
-
Args:
|
|
130
|
-
voxel_data (ndarray): 3D voxel array.
|
|
131
|
-
meshsize (float): Voxel size in meters.
|
|
132
|
-
diffuse_irradiance (float): Diffuse irradiance in W/m².
|
|
133
|
-
|
|
134
|
-
Returns:
|
|
135
|
-
ndarray: 2D array of diffuse solar irradiance (W/m²).
|
|
137
|
+
Compute diffuse solar irradiance map using the Sky View Factor (SVF) with tree transmittance.
|
|
136
138
|
"""
|
|
137
|
-
|
|
138
|
-
|
|
139
|
+
|
|
140
|
+
view_point_height = kwargs.get("view_point_height", 1.5)
|
|
141
|
+
colormap = kwargs.get("colormap", 'magma')
|
|
142
|
+
vmin = kwargs.get("vmin", 0.0)
|
|
143
|
+
vmax = kwargs.get("vmax", diffuse_irradiance)
|
|
144
|
+
|
|
145
|
+
# Pass tree transmittance parameters to SVF calculation
|
|
139
146
|
svf_kwargs = kwargs.copy()
|
|
140
147
|
svf_kwargs["colormap"] = "BuPu_r"
|
|
141
148
|
svf_kwargs["vmin"] = 0
|
|
142
149
|
svf_kwargs["vmax"] = 1
|
|
150
|
+
|
|
151
|
+
# SVF calculation now handles tree transmittance internally
|
|
143
152
|
SVF_map = get_sky_view_factor_map(voxel_data, meshsize, **svf_kwargs)
|
|
144
153
|
diffuse_map = SVF_map * diffuse_irradiance
|
|
145
154
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
155
|
+
if show_plot:
|
|
156
|
+
vmin = kwargs.get("vmin", 0.0)
|
|
157
|
+
vmax = kwargs.get("vmax", diffuse_irradiance)
|
|
158
|
+
cmap = plt.cm.get_cmap(colormap).copy()
|
|
159
|
+
cmap.set_bad(color='lightgray')
|
|
160
|
+
plt.figure(figsize=(10, 8))
|
|
161
|
+
plt.title("Diffuse Solar Irradiance Map")
|
|
162
|
+
plt.imshow(diffuse_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
|
|
163
|
+
plt.colorbar(label='Diffuse Solar Irradiance (W/m²)')
|
|
164
|
+
plt.show()
|
|
156
165
|
|
|
157
166
|
# Optional OBJ export
|
|
158
167
|
obj_export = kwargs.get("obj_export", False)
|
|
159
168
|
if obj_export:
|
|
160
|
-
from ..file.obj import grid_to_obj
|
|
161
169
|
dem_grid = kwargs.get("dem_grid", np.zeros_like(diffuse_map))
|
|
162
170
|
output_dir = kwargs.get("output_directory", "output")
|
|
163
171
|
output_file_name = kwargs.get("output_file_name", "diffuse_solar_irradiance")
|
|
164
172
|
num_colors = kwargs.get("num_colors", 10)
|
|
165
173
|
alpha = kwargs.get("alpha", 1.0)
|
|
166
|
-
view_point_height = kwargs.get("view_point_height", 1.5)
|
|
167
174
|
grid_to_obj(
|
|
168
175
|
diffuse_map,
|
|
169
176
|
dem_grid,
|
|
@@ -188,6 +195,7 @@ def get_global_solar_irradiance_map(
|
|
|
188
195
|
elevation_degrees,
|
|
189
196
|
direct_normal_irradiance,
|
|
190
197
|
diffuse_irradiance,
|
|
198
|
+
show_plot=False,
|
|
191
199
|
**kwargs
|
|
192
200
|
):
|
|
193
201
|
"""
|
|
@@ -205,7 +213,17 @@ def get_global_solar_irradiance_map(
|
|
|
205
213
|
|
|
206
214
|
Returns:
|
|
207
215
|
ndarray: 2D array of global solar irradiance (W/m²).
|
|
208
|
-
"""
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
colormap = kwargs.get("colormap", 'magma')
|
|
219
|
+
|
|
220
|
+
# Create kwargs for diffuse calculation
|
|
221
|
+
direct_diffuse_kwargs = kwargs.copy()
|
|
222
|
+
direct_diffuse_kwargs.update({
|
|
223
|
+
'show_plot': False,
|
|
224
|
+
'obj_export': False
|
|
225
|
+
})
|
|
226
|
+
|
|
209
227
|
# Compute direct irradiance map (no mode/hit_values/inclusion_mode needed)
|
|
210
228
|
direct_map = get_direct_solar_irradiance_map(
|
|
211
229
|
voxel_data,
|
|
@@ -213,7 +231,7 @@ def get_global_solar_irradiance_map(
|
|
|
213
231
|
azimuth_degrees,
|
|
214
232
|
elevation_degrees,
|
|
215
233
|
direct_normal_irradiance,
|
|
216
|
-
**
|
|
234
|
+
**direct_diffuse_kwargs
|
|
217
235
|
)
|
|
218
236
|
|
|
219
237
|
# Compute diffuse irradiance map
|
|
@@ -221,27 +239,27 @@ def get_global_solar_irradiance_map(
|
|
|
221
239
|
voxel_data,
|
|
222
240
|
meshsize,
|
|
223
241
|
diffuse_irradiance=diffuse_irradiance,
|
|
224
|
-
**
|
|
242
|
+
**direct_diffuse_kwargs
|
|
225
243
|
)
|
|
226
244
|
|
|
227
245
|
# Sum the two
|
|
228
246
|
global_map = direct_map + diffuse_map
|
|
229
247
|
|
|
230
|
-
colormap = kwargs.get("colormap", 'viridis')
|
|
231
248
|
vmin = kwargs.get("vmin", np.nanmin(global_map))
|
|
232
249
|
vmax = kwargs.get("vmax", np.nanmax(global_map))
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
250
|
+
|
|
251
|
+
if show_plot:
|
|
252
|
+
cmap = plt.cm.get_cmap(colormap).copy()
|
|
253
|
+
cmap.set_bad(color='lightgray')
|
|
254
|
+
plt.figure(figsize=(10, 8))
|
|
255
|
+
plt.title("Global Solar Irradiance Map")
|
|
256
|
+
plt.imshow(global_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
|
|
257
|
+
plt.colorbar(label='Global Solar Irradiance (W/m²)')
|
|
258
|
+
plt.show()
|
|
240
259
|
|
|
241
260
|
# Optional OBJ export
|
|
242
261
|
obj_export = kwargs.get("obj_export", False)
|
|
243
262
|
if obj_export:
|
|
244
|
-
from ..file.obj import grid_to_obj
|
|
245
263
|
dem_grid = kwargs.get("dem_grid", np.zeros_like(global_map))
|
|
246
264
|
output_dir = kwargs.get("output_directory", "output")
|
|
247
265
|
output_file_name = kwargs.get("output_file_name", "global_solar_irradiance")
|
|
@@ -263,4 +281,339 @@ def get_global_solar_irradiance_map(
|
|
|
263
281
|
vmax=vmax
|
|
264
282
|
)
|
|
265
283
|
|
|
266
|
-
return global_map
|
|
284
|
+
return global_map
|
|
285
|
+
|
|
286
|
+
def get_solar_positions_astral(times, lat, lon):
|
|
287
|
+
"""
|
|
288
|
+
Compute solar azimuth and elevation using Astral for given times and location.
|
|
289
|
+
Times must be timezone-aware.
|
|
290
|
+
"""
|
|
291
|
+
observer = Observer(latitude=lat, longitude=lon)
|
|
292
|
+
df_pos = pd.DataFrame(index=times, columns=['azimuth', 'elevation'], dtype=float)
|
|
293
|
+
|
|
294
|
+
for t in times:
|
|
295
|
+
# t is already timezone-aware; no need to replace tzinfo
|
|
296
|
+
el = elevation(observer=observer, dateandtime=t)
|
|
297
|
+
az = azimuth(observer=observer, dateandtime=t)
|
|
298
|
+
df_pos.at[t, 'elevation'] = el
|
|
299
|
+
df_pos.at[t, 'azimuth'] = az
|
|
300
|
+
|
|
301
|
+
return df_pos
|
|
302
|
+
|
|
303
|
+
def get_cumulative_global_solar_irradiance(
|
|
304
|
+
voxel_data,
|
|
305
|
+
meshsize,
|
|
306
|
+
df, lat, lon, tz,
|
|
307
|
+
direct_normal_irradiance_scaling=1.0,
|
|
308
|
+
diffuse_irradiance_scaling=1.0,
|
|
309
|
+
**kwargs
|
|
310
|
+
):
|
|
311
|
+
"""
|
|
312
|
+
Compute cumulative global solar irradiance over a specified period using data from an EPW file,
|
|
313
|
+
accounting for tree transmittance.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
voxel_data (ndarray): 3D array of voxel values.
|
|
317
|
+
meshsize (float): Size of each voxel in meters.
|
|
318
|
+
start_time (str): Start time in format 'MM-DD HH:MM:SS' (no year).
|
|
319
|
+
end_time (str): End time in format 'MM-DD HH:MM:SS' (no year).
|
|
320
|
+
direct_normal_irradiance_scaling (float): Scaling factor for DNI.
|
|
321
|
+
diffuse_irradiance_scaling (float): Scaling factor for DHI.
|
|
322
|
+
**kwargs: Additional arguments including:
|
|
323
|
+
- view_point_height (float): Observer height in meters
|
|
324
|
+
- tree_k (float): Tree extinction coefficient (default: 0.5)
|
|
325
|
+
- tree_lad (float): Leaf area density in m^-1 (default: 1.0)
|
|
326
|
+
- download_nearest_epw (bool): Whether to download nearest EPW file
|
|
327
|
+
- epw_file_path (str): Path to EPW file
|
|
328
|
+
- show_plot (bool): Whether to show final plot
|
|
329
|
+
- show_each_timestep (bool): Whether to show plots for each timestep
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
ndarray: 2D array of cumulative global solar irradiance (W/m²·hour).
|
|
333
|
+
"""
|
|
334
|
+
view_point_height = kwargs.get("view_point_height", 1.5)
|
|
335
|
+
colormap = kwargs.get("colormap", 'magma')
|
|
336
|
+
start_time = kwargs.get("start_time", "01-01 05:00:00")
|
|
337
|
+
end_time = kwargs.get("end_time", "01-01 20:00:00")
|
|
338
|
+
|
|
339
|
+
if df.empty:
|
|
340
|
+
raise ValueError("No data in EPW file.")
|
|
341
|
+
|
|
342
|
+
# Parse start and end times without year
|
|
343
|
+
try:
|
|
344
|
+
start_dt = datetime.strptime(start_time, "%m-%d %H:%M:%S")
|
|
345
|
+
end_dt = datetime.strptime(end_time, "%m-%d %H:%M:%S")
|
|
346
|
+
except ValueError as ve:
|
|
347
|
+
raise ValueError("start_time and end_time must be in format 'MM-DD HH:MM:SS'") from ve
|
|
348
|
+
|
|
349
|
+
# Add hour of year column and filter data as before...
|
|
350
|
+
df['hour_of_year'] = (df.index.dayofyear - 1) * 24 + df.index.hour + 1
|
|
351
|
+
|
|
352
|
+
start_doy = datetime(2000, start_dt.month, start_dt.day).timetuple().tm_yday
|
|
353
|
+
end_doy = datetime(2000, end_dt.month, end_dt.day).timetuple().tm_yday
|
|
354
|
+
|
|
355
|
+
start_hour = (start_doy - 1) * 24 + start_dt.hour + 1
|
|
356
|
+
end_hour = (end_doy - 1) * 24 + end_dt.hour + 1
|
|
357
|
+
|
|
358
|
+
if start_hour <= end_hour:
|
|
359
|
+
df_period = df[(df['hour_of_year'] >= start_hour) & (df['hour_of_year'] <= end_hour)]
|
|
360
|
+
else:
|
|
361
|
+
df_period = df[(df['hour_of_year'] >= start_hour) | (df['hour_of_year'] <= end_hour)]
|
|
362
|
+
|
|
363
|
+
df_period = df_period[
|
|
364
|
+
((df_period.index.hour != start_dt.hour) | (df_period.index.minute >= start_dt.minute)) &
|
|
365
|
+
((df_period.index.hour != end_dt.hour) | (df_period.index.minute <= end_dt.minute))
|
|
366
|
+
]
|
|
367
|
+
|
|
368
|
+
if df_period.empty:
|
|
369
|
+
raise ValueError("No EPW data in the specified period.")
|
|
370
|
+
|
|
371
|
+
# Prepare timezone conversion
|
|
372
|
+
offset_minutes = int(tz * 60)
|
|
373
|
+
local_tz = pytz.FixedOffset(offset_minutes)
|
|
374
|
+
df_period_local = df_period.copy()
|
|
375
|
+
df_period_local.index = df_period_local.index.tz_localize(local_tz)
|
|
376
|
+
df_period_utc = df_period_local.tz_convert(pytz.UTC)
|
|
377
|
+
|
|
378
|
+
# Compute solar positions
|
|
379
|
+
solar_positions = get_solar_positions_astral(df_period_utc.index, lat, lon)
|
|
380
|
+
|
|
381
|
+
# Create kwargs for diffuse calculation
|
|
382
|
+
diffuse_kwargs = kwargs.copy()
|
|
383
|
+
diffuse_kwargs.update({
|
|
384
|
+
'show_plot': False,
|
|
385
|
+
'obj_export': False
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
# Compute base diffuse map once with diffuse_irradiance=1.0
|
|
389
|
+
base_diffuse_map = get_diffuse_solar_irradiance_map(
|
|
390
|
+
voxel_data,
|
|
391
|
+
meshsize,
|
|
392
|
+
diffuse_irradiance=1.0,
|
|
393
|
+
**diffuse_kwargs
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# Initialize maps
|
|
397
|
+
cumulative_map = np.zeros((voxel_data.shape[0], voxel_data.shape[1]))
|
|
398
|
+
mask_map = np.ones((voxel_data.shape[0], voxel_data.shape[1]), dtype=bool)
|
|
399
|
+
|
|
400
|
+
# Create kwargs for direct calculation
|
|
401
|
+
direct_kwargs = kwargs.copy()
|
|
402
|
+
direct_kwargs.update({
|
|
403
|
+
'show_plot': False,
|
|
404
|
+
'view_point_height': view_point_height,
|
|
405
|
+
'obj_export': False
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
# Iterate through each time step
|
|
409
|
+
for idx, (time_utc, row) in enumerate(df_period_utc.iterrows()):
|
|
410
|
+
DNI = row['DNI'] * direct_normal_irradiance_scaling
|
|
411
|
+
DHI = row['DHI'] * diffuse_irradiance_scaling
|
|
412
|
+
time_local = df_period_local.index[idx]
|
|
413
|
+
|
|
414
|
+
# Get solar position
|
|
415
|
+
solpos = solar_positions.loc[time_utc]
|
|
416
|
+
azimuth_degrees = solpos['azimuth']
|
|
417
|
+
elevation_degrees = solpos['elevation']
|
|
418
|
+
|
|
419
|
+
# Compute direct irradiance map with transmittance
|
|
420
|
+
direct_map = get_direct_solar_irradiance_map(
|
|
421
|
+
voxel_data,
|
|
422
|
+
meshsize,
|
|
423
|
+
azimuth_degrees,
|
|
424
|
+
elevation_degrees,
|
|
425
|
+
direct_normal_irradiance=DNI,
|
|
426
|
+
**direct_kwargs
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
# Scale base_diffuse_map by actual DHI
|
|
430
|
+
diffuse_map = base_diffuse_map * DHI
|
|
431
|
+
|
|
432
|
+
# Combine direct and diffuse
|
|
433
|
+
global_map = direct_map + diffuse_map
|
|
434
|
+
|
|
435
|
+
# Update mask_map
|
|
436
|
+
mask_map &= ~np.isnan(global_map)
|
|
437
|
+
|
|
438
|
+
# Replace NaN with 0 for accumulation
|
|
439
|
+
global_map_filled = np.nan_to_num(global_map, nan=0.0)
|
|
440
|
+
cumulative_map += global_map_filled
|
|
441
|
+
|
|
442
|
+
# Optional timestep visualization
|
|
443
|
+
show_each_timestep = kwargs.get("show_each_timestep", False)
|
|
444
|
+
if show_each_timestep:
|
|
445
|
+
colormap = kwargs.get("colormap", 'viridis')
|
|
446
|
+
vmin = kwargs.get("vmin", 0.0)
|
|
447
|
+
vmax = kwargs.get("vmax", max(direct_normal_irradiance_scaling, diffuse_irradiance_scaling) * 1000)
|
|
448
|
+
cmap = plt.cm.get_cmap(colormap).copy()
|
|
449
|
+
cmap.set_bad(color='lightgray')
|
|
450
|
+
plt.figure(figsize=(8, 6))
|
|
451
|
+
plt.title(f"Global Solar Irradiance at {time_local.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
452
|
+
plt.imshow(global_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
|
|
453
|
+
plt.colorbar(label='Global Solar Irradiance (W/m²)')
|
|
454
|
+
plt.show()
|
|
455
|
+
|
|
456
|
+
# Apply mask
|
|
457
|
+
cumulative_map[~mask_map] = np.nan
|
|
458
|
+
|
|
459
|
+
# Final visualization
|
|
460
|
+
show_plot = kwargs.get("show_plot", True)
|
|
461
|
+
if show_plot:
|
|
462
|
+
colormap = kwargs.get("colormap", 'magma')
|
|
463
|
+
vmin = kwargs.get("vmin", np.nanmin(cumulative_map))
|
|
464
|
+
vmax = kwargs.get("vmax", np.nanmax(cumulative_map))
|
|
465
|
+
cmap = plt.cm.get_cmap(colormap).copy()
|
|
466
|
+
cmap.set_bad(color='lightgray')
|
|
467
|
+
plt.figure(figsize=(8, 6))
|
|
468
|
+
plt.title("Cumulative Global Solar Irradiance Map")
|
|
469
|
+
plt.imshow(cumulative_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
|
|
470
|
+
plt.colorbar(label='Cumulative Global Solar Irradiance (W/m²·hour)')
|
|
471
|
+
plt.show()
|
|
472
|
+
|
|
473
|
+
# Optional OBJ export
|
|
474
|
+
obj_export = kwargs.get("obj_export", False)
|
|
475
|
+
if obj_export:
|
|
476
|
+
colormap = kwargs.get("colormap", "magma")
|
|
477
|
+
vmin = kwargs.get("vmin", np.nanmin(cumulative_map))
|
|
478
|
+
vmax = kwargs.get("vmax", np.nanmax(cumulative_map))
|
|
479
|
+
dem_grid = kwargs.get("dem_grid", np.zeros_like(cumulative_map))
|
|
480
|
+
output_dir = kwargs.get("output_directory", "output")
|
|
481
|
+
output_file_name = kwargs.get("output_file_name", "cummurative_global_solar_irradiance")
|
|
482
|
+
num_colors = kwargs.get("num_colors", 10)
|
|
483
|
+
alpha = kwargs.get("alpha", 1.0)
|
|
484
|
+
grid_to_obj(
|
|
485
|
+
cumulative_map,
|
|
486
|
+
dem_grid,
|
|
487
|
+
output_dir,
|
|
488
|
+
output_file_name,
|
|
489
|
+
meshsize,
|
|
490
|
+
view_point_height,
|
|
491
|
+
colormap_name=colormap,
|
|
492
|
+
num_colors=num_colors,
|
|
493
|
+
alpha=alpha,
|
|
494
|
+
vmin=vmin,
|
|
495
|
+
vmax=vmax
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
return cumulative_map
|
|
499
|
+
|
|
500
|
+
def get_global_solar_irradiance_using_epw(
|
|
501
|
+
voxel_data,
|
|
502
|
+
meshsize,
|
|
503
|
+
calc_type='instantaneous',
|
|
504
|
+
direct_normal_irradiance_scaling=1.0,
|
|
505
|
+
diffuse_irradiance_scaling=1.0,
|
|
506
|
+
**kwargs
|
|
507
|
+
):
|
|
508
|
+
"""
|
|
509
|
+
Compute cumulative global solar irradiance over a specified period using data from an EPW file,
|
|
510
|
+
accounting for tree transmittance.
|
|
511
|
+
|
|
512
|
+
voxel_data, # 3D voxel grid representing the urban environment
|
|
513
|
+
meshsize, # Size of each grid cell in meters
|
|
514
|
+
azimuth_degrees, # Sun's azimuth angle
|
|
515
|
+
elevation_degrees, # Sun's elevation angle
|
|
516
|
+
direct_normal_irradiance, # Direct Normal Irradiance value
|
|
517
|
+
diffuse_irradiance, # Diffuse irradiance value
|
|
518
|
+
show_plot=True, # Display visualization of results
|
|
519
|
+
**kwargs
|
|
520
|
+
)
|
|
521
|
+
if type == 'cummulative':
|
|
522
|
+
- tree_lad (float): Leaf area density in m^-1 (default: 1.0)
|
|
523
|
+
- download_nearest_epw (bool): Whether to download nearest EPW file
|
|
524
|
+
- epw_file_path (str): Path to EPW file
|
|
525
|
+
- show_plot (bool): Whether to show final plot
|
|
526
|
+
- show_each_timestep (bool): Whether to show plots for each timestep
|
|
527
|
+
|
|
528
|
+
Returns:
|
|
529
|
+
ndarray: 2D array of cumulative global solar irradiance (W/m²·hour).
|
|
530
|
+
"""
|
|
531
|
+
view_point_height = kwargs.get("view_point_height", 1.5)
|
|
532
|
+
colormap = kwargs.get("colormap", 'magma')
|
|
533
|
+
|
|
534
|
+
# Get EPW file
|
|
535
|
+
download_nearest_epw = kwargs.get("download_nearest_epw", False)
|
|
536
|
+
rectangle_vertices = kwargs.get("rectangle_vertices", None)
|
|
537
|
+
epw_file_path = kwargs.get("epw_file_path", None)
|
|
538
|
+
if download_nearest_epw:
|
|
539
|
+
if rectangle_vertices is None:
|
|
540
|
+
print("rectangle_vertices is required to download nearest EPW file")
|
|
541
|
+
return None
|
|
542
|
+
else:
|
|
543
|
+
# Calculate center point of rectangle
|
|
544
|
+
lats = [coord[0] for coord in rectangle_vertices]
|
|
545
|
+
lons = [coord[1] for coord in rectangle_vertices]
|
|
546
|
+
center_lat = (min(lats) + max(lats)) / 2
|
|
547
|
+
center_lon = (min(lons) + max(lons)) / 2
|
|
548
|
+
target_point = (center_lat, center_lon)
|
|
549
|
+
|
|
550
|
+
# Optional: specify maximum distance in kilometers
|
|
551
|
+
max_distance = 100 # None for no limit
|
|
552
|
+
|
|
553
|
+
output_dir = kwargs.get("output_dir", "output")
|
|
554
|
+
|
|
555
|
+
epw_file_path, weather_data, metadata = get_nearest_epw_from_climate_onebuilding(
|
|
556
|
+
latitude=center_lat,
|
|
557
|
+
longitude=center_lon,
|
|
558
|
+
output_dir=output_dir,
|
|
559
|
+
max_distance=max_distance,
|
|
560
|
+
extract_zip=True,
|
|
561
|
+
load_data=True
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
# Read EPW data
|
|
565
|
+
df, lat, lon, tz, elevation_m = read_epw_for_solar_simulation(epw_file_path)
|
|
566
|
+
if df.empty:
|
|
567
|
+
raise ValueError("No data in EPW file.")
|
|
568
|
+
|
|
569
|
+
if calc_type == 'instantaneous':
|
|
570
|
+
if df.empty:
|
|
571
|
+
raise ValueError("No data in EPW file.")
|
|
572
|
+
|
|
573
|
+
calc_time = kwargs.get("calc_time", "01-01 12:00:00")
|
|
574
|
+
|
|
575
|
+
# Parse start and end times without year
|
|
576
|
+
try:
|
|
577
|
+
calc_dt = datetime.strptime(calc_time, "%m-%d %H:%M:%S")
|
|
578
|
+
except ValueError as ve:
|
|
579
|
+
raise ValueError("calc_time must be in format 'MM-DD HH:MM:SS'") from ve
|
|
580
|
+
|
|
581
|
+
df_period = df[
|
|
582
|
+
(df.index.month == calc_dt.month) & (df.index.day == calc_dt.day) & (df.index.hour == calc_dt.hour)
|
|
583
|
+
]
|
|
584
|
+
|
|
585
|
+
if df_period.empty:
|
|
586
|
+
raise ValueError("No EPW data at the specified time.")
|
|
587
|
+
|
|
588
|
+
# Prepare timezone conversion
|
|
589
|
+
offset_minutes = int(tz * 60)
|
|
590
|
+
local_tz = pytz.FixedOffset(offset_minutes)
|
|
591
|
+
df_period_local = df_period.copy()
|
|
592
|
+
df_period_local.index = df_period_local.index.tz_localize(local_tz)
|
|
593
|
+
df_period_utc = df_period_local.tz_convert(pytz.UTC)
|
|
594
|
+
|
|
595
|
+
# Compute solar positions
|
|
596
|
+
solar_positions = get_solar_positions_astral(df_period_utc.index, lat, lon)
|
|
597
|
+
direct_normal_irradiance = df_period_utc.iloc[0]['DNI']
|
|
598
|
+
diffuse_irradiance = df_period_utc.iloc[0]['DHI']
|
|
599
|
+
azimuth_degrees = solar_positions.iloc[0]['azimuth']
|
|
600
|
+
elevation_degrees = solar_positions.iloc[0]['elevation']
|
|
601
|
+
solar_map = get_global_solar_irradiance_map(
|
|
602
|
+
voxel_data, # 3D voxel grid representing the urban environment
|
|
603
|
+
meshsize, # Size of each grid cell in meters
|
|
604
|
+
azimuth_degrees, # Sun's azimuth angle
|
|
605
|
+
elevation_degrees, # Sun's elevation angle
|
|
606
|
+
direct_normal_irradiance, # Direct Normal Irradiance value
|
|
607
|
+
diffuse_irradiance, # Diffuse irradiance value
|
|
608
|
+
show_plot=True, # Display visualization of results
|
|
609
|
+
**kwargs
|
|
610
|
+
)
|
|
611
|
+
if calc_type == 'cumulative':
|
|
612
|
+
solar_map = get_cumulative_global_solar_irradiance(
|
|
613
|
+
voxel_data,
|
|
614
|
+
meshsize,
|
|
615
|
+
df, lat, lon, tz,
|
|
616
|
+
**kwargs
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
return solar_map
|