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 CHANGED
@@ -1,2 +1,3 @@
1
1
  from .view import *
2
2
  from .solar import *
3
+ from .utils import *
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, view_height_voxel, hit_values, inclusion_mode):
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 binary map of direct solar irradiation: 1.0 if cell is sunlit, 0.0 if shaded.
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 (here we only use (0,)).
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 where 1.0 = sunlit, 0.0 = shaded, NaN = invalid observer.
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
- # Check if standing on building or vegetation
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, hit_values, inclusion_mode)
51
- irradiance_map[x, y] = 0.0 if hit else 1.0
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
- def get_direct_solar_irradiance_map(voxel_data, meshsize, azimuth_degrees_ori, elevation_degrees, direct_normal_irradiance, **kwargs):
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
- view_height_voxel = int(view_point_height / meshsize)
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 with the adjusted formula
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
- binary_map = compute_direct_solar_irradiance_map_binary(
81
- voxel_data, sun_direction, view_height_voxel, hit_values, inclusion_mode
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 = binary_map * direct_normal_irradiance * sin_elev
100
+ direct_map = transmittance_map * direct_normal_irradiance * sin_elev
86
101
 
87
- # Visualization
88
- cmap = plt.cm.get_cmap(colormap).copy()
89
- cmap.set_bad(color='lightgray')
90
- plt.figure(figsize=(10, 8))
91
- plt.title("Horizontal Direct Solar Irradiance Map (0° = North)")
92
- plt.imshow(direct_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
93
- plt.colorbar(label='Direct Solar Irradiance (W/m²)')
94
- plt.show()
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
- # SVF computation does not require mode/hit_values/inclusion_mode,
138
- # it's already defined to consider all non-empty voxels as obstacles internally.
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
- colormap = kwargs.get("colormap", 'viridis')
147
- vmin = kwargs.get("vmin", 0.0)
148
- vmax = kwargs.get("vmax", diffuse_irradiance)
149
- cmap = plt.cm.get_cmap(colormap).copy()
150
- cmap.set_bad(color='lightgray')
151
- plt.figure(figsize=(10, 8))
152
- plt.title("Diffuse Solar Irradiance Map")
153
- plt.imshow(diffuse_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
154
- plt.colorbar(label='Diffuse Solar Irradiance (W/m²)')
155
- plt.show()
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
- **kwargs
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
- **kwargs
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
- cmap = plt.cm.get_cmap(colormap).copy()
234
- cmap.set_bad(color='lightgray')
235
- plt.figure(figsize=(10, 8))
236
- plt.title("Global Solar Irradiance Map")
237
- plt.imshow(global_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
238
- plt.colorbar(label='Global Solar Irradiance (W/m²)')
239
- plt.show()
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
voxcity/sim/utils.py ADDED
@@ -0,0 +1,6 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ from datetime import datetime
4
+
5
+ def dummy_function(test_string):
6
+ return test_string