voxcity 0.3.2__py3-none-any.whl → 0.3.4__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/solar.py CHANGED
@@ -16,10 +16,16 @@ def compute_direct_solar_irradiance_map_binary(voxel_data, sun_direction, view_p
16
16
  """
17
17
  Compute a map of direct solar irradiation accounting for tree transmittance.
18
18
 
19
+ The function:
20
+ 1. Places observers at valid locations (empty voxels above ground)
21
+ 2. Casts rays from each observer in the sun direction
22
+ 3. Computes transmittance through trees using Beer-Lambert law
23
+ 4. Returns a 2D map of transmittance values
24
+
19
25
  Args:
20
26
  voxel_data (ndarray): 3D array of voxel values.
21
27
  sun_direction (tuple): Direction vector of the sun.
22
- view_height_voxel (int): Observer height in voxel units.
28
+ view_point_height (float): Observer height in meters.
23
29
  hit_values (tuple): Values considered non-obstacles if inclusion_mode=False.
24
30
  meshsize (float): Size of each voxel in meters.
25
31
  tree_k (float): Tree extinction coefficient.
@@ -27,7 +33,7 @@ def compute_direct_solar_irradiance_map_binary(voxel_data, sun_direction, view_p
27
33
  inclusion_mode (bool): False here, meaning any voxel not in hit_values is an obstacle.
28
34
 
29
35
  Returns:
30
- ndarray: 2D array of transmittance values (0.0-1.0), NaN = invalid observer.
36
+ ndarray: 2D array of transmittance values (0.0-1.0), NaN = invalid observer position.
31
37
  """
32
38
 
33
39
  view_height_voxel = int(view_point_height / meshsize)
@@ -35,18 +41,22 @@ def compute_direct_solar_irradiance_map_binary(voxel_data, sun_direction, view_p
35
41
  nx, ny, nz = voxel_data.shape
36
42
  irradiance_map = np.full((nx, ny), np.nan, dtype=np.float64)
37
43
 
38
- # Normalize sun direction
44
+ # Normalize sun direction vector for ray tracing
39
45
  sd = np.array(sun_direction, dtype=np.float64)
40
46
  sd_len = np.sqrt(sd[0]**2 + sd[1]**2 + sd[2]**2)
41
47
  if sd_len == 0.0:
42
48
  return np.flipud(irradiance_map)
43
49
  sd /= sd_len
44
50
 
51
+ # Process each x,y position in parallel
45
52
  for x in prange(nx):
46
53
  for y in range(ny):
47
54
  found_observer = False
55
+ # Search upward for valid observer position
48
56
  for z in range(1, nz):
57
+ # Check if current voxel is empty/tree and voxel below is solid
49
58
  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
50
60
  if voxel_data[x, y, z - 1] in (-30, -3, -2):
51
61
  irradiance_map[x, y] = np.nan
52
62
  found_observer = True
@@ -62,12 +72,43 @@ def compute_direct_solar_irradiance_map_binary(voxel_data, sun_direction, view_p
62
72
  if not found_observer:
63
73
  irradiance_map[x, y] = np.nan
64
74
 
75
+ # Flip map vertically to match visualization conventions
65
76
  return np.flipud(irradiance_map)
66
77
 
67
78
  def get_direct_solar_irradiance_map(voxel_data, meshsize, azimuth_degrees_ori, elevation_degrees,
68
79
  direct_normal_irradiance, show_plot=False, **kwargs):
69
80
  """
70
81
  Compute direct solar irradiance map with tree transmittance.
82
+
83
+ 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
88
+
89
+ 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.
96
+ **kwargs: Additional arguments including:
97
+ - 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
101
+ - tree_k (float): Tree extinction coefficient (default: 0.6)
102
+ - 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
109
+
110
+ Returns:
111
+ ndarray: 2D array of direct solar irradiance values (W/m²).
71
112
  """
72
113
  view_point_height = kwargs.get("view_point_height", 1.5)
73
114
  colormap = kwargs.get("colormap", 'magma')
@@ -78,7 +119,8 @@ def get_direct_solar_irradiance_map(voxel_data, meshsize, azimuth_degrees_ori, e
78
119
  tree_k = kwargs.get("tree_k", 0.6)
79
120
  tree_lad = kwargs.get("tree_lad", 1.0)
80
121
 
81
- # Convert angles to direction
122
+ # Convert sun angles to direction vector
123
+ # Note: azimuth is adjusted by 180° to match coordinate system
82
124
  azimuth_degrees = 180 - azimuth_degrees_ori
83
125
  azimuth_radians = np.deg2rad(azimuth_degrees)
84
126
  elevation_radians = np.deg2rad(elevation_degrees)
@@ -91,14 +133,17 @@ def get_direct_solar_irradiance_map(voxel_data, meshsize, azimuth_degrees_ori, e
91
133
  hit_values = (0,)
92
134
  inclusion_mode = False
93
135
 
136
+ # Compute transmittance map
94
137
  transmittance_map = compute_direct_solar_irradiance_map_binary(
95
138
  voxel_data, sun_direction, view_point_height, hit_values,
96
139
  meshsize, tree_k, tree_lad, inclusion_mode
97
140
  )
98
141
 
142
+ # Scale by direct normal irradiance and sun elevation
99
143
  sin_elev = dz
100
144
  direct_map = transmittance_map * direct_normal_irradiance * sin_elev
101
145
 
146
+ # Optional visualization
102
147
  if show_plot:
103
148
  cmap = plt.cm.get_cmap(colormap).copy()
104
149
  cmap.set_bad(color='lightgray')
@@ -135,6 +180,33 @@ def get_direct_solar_irradiance_map(voxel_data, meshsize, azimuth_degrees_ori, e
135
180
  def get_diffuse_solar_irradiance_map(voxel_data, meshsize, diffuse_irradiance=1.0, show_plot=False, **kwargs):
136
181
  """
137
182
  Compute diffuse solar irradiance map using the Sky View Factor (SVF) with tree transmittance.
183
+
184
+ The function:
185
+ 1. Computes SVF map accounting for tree transmittance
186
+ 2. Scales SVF by diffuse horizontal irradiance
187
+ 3. Optionally visualizes and exports results
188
+
189
+ Args:
190
+ voxel_data (ndarray): 3D array of voxel values.
191
+ meshsize (float): Size of each voxel in meters.
192
+ diffuse_irradiance (float): Diffuse horizontal irradiance in W/m².
193
+ show_plot (bool): Whether to display visualization.
194
+ **kwargs: Additional arguments including:
195
+ - view_point_height (float): Observer height in meters (default: 1.5)
196
+ - colormap (str): Matplotlib colormap name (default: 'magma')
197
+ - vmin (float): Minimum value for colormap
198
+ - vmax (float): Maximum value for colormap
199
+ - tree_k (float): Tree extinction coefficient
200
+ - tree_lad (float): Leaf area density in m^-1
201
+ - obj_export (bool): Whether to export as OBJ file
202
+ - output_directory (str): Directory for OBJ export
203
+ - output_file_name (str): Filename for OBJ export
204
+ - dem_grid (ndarray): DEM grid for OBJ export
205
+ - num_colors (int): Number of colors for OBJ export
206
+ - alpha (float): Alpha value for OBJ export
207
+
208
+ Returns:
209
+ ndarray: 2D array of diffuse solar irradiance values (W/m²).
138
210
  """
139
211
 
140
212
  view_point_height = kwargs.get("view_point_height", 1.5)
@@ -152,6 +224,7 @@ def get_diffuse_solar_irradiance_map(voxel_data, meshsize, diffuse_irradiance=1.
152
224
  SVF_map = get_sky_view_factor_map(voxel_data, meshsize, **svf_kwargs)
153
225
  diffuse_map = SVF_map * diffuse_irradiance
154
226
 
227
+ # Optional visualization
155
228
  if show_plot:
156
229
  vmin = kwargs.get("vmin", 0.0)
157
230
  vmax = kwargs.get("vmax", diffuse_irradiance)
@@ -201,18 +274,35 @@ def get_global_solar_irradiance_map(
201
274
  """
202
275
  Compute global solar irradiance (direct + diffuse) on a horizontal plane at each valid observer location.
203
276
 
204
- No mode/hit_values/inclusion_mode needed. Uses the updated direct and diffuse functions.
277
+ The function:
278
+ 1. Computes direct solar irradiance map
279
+ 2. Computes diffuse solar irradiance map
280
+ 3. Combines maps and optionally visualizes/exports results
205
281
 
206
282
  Args:
207
283
  voxel_data (ndarray): 3D voxel array.
208
284
  meshsize (float): Voxel size in meters.
209
- azimuth_degrees (float): Sun azimuth angle in degrees.
210
- elevation_degrees (float): Sun elevation angle in degrees.
211
- direct_normal_irradiance (float): DNI in W/m².
212
- diffuse_irradiance (float): Diffuse irradiance in W/m².
285
+ azimuth_degrees (float): Sun azimuth angle in degrees (0° = North, 90° = East).
286
+ elevation_degrees (float): Sun elevation angle in degrees above horizon.
287
+ direct_normal_irradiance (float): Direct normal irradiance in W/m².
288
+ diffuse_irradiance (float): Diffuse horizontal irradiance in W/m².
289
+ show_plot (bool): Whether to display visualization.
290
+ **kwargs: Additional arguments including:
291
+ - view_point_height (float): Observer height in meters (default: 1.5)
292
+ - colormap (str): Matplotlib colormap name (default: 'magma')
293
+ - vmin (float): Minimum value for colormap
294
+ - vmax (float): Maximum value for colormap
295
+ - tree_k (float): Tree extinction coefficient
296
+ - tree_lad (float): Leaf area density in m^-1
297
+ - obj_export (bool): Whether to export as OBJ file
298
+ - output_directory (str): Directory for OBJ export
299
+ - output_file_name (str): Filename for OBJ export
300
+ - dem_grid (ndarray): DEM grid for OBJ export
301
+ - num_colors (int): Number of colors for OBJ export
302
+ - alpha (float): Alpha value for OBJ export
213
303
 
214
304
  Returns:
215
- ndarray: 2D array of global solar irradiance (W/m²).
305
+ ndarray: 2D array of global solar irradiance values (W/m²).
216
306
  """
217
307
 
218
308
  colormap = kwargs.get("colormap", 'magma')
@@ -242,12 +332,13 @@ def get_global_solar_irradiance_map(
242
332
  **direct_diffuse_kwargs
243
333
  )
244
334
 
245
- # Sum the two
335
+ # Sum the two components
246
336
  global_map = direct_map + diffuse_map
247
337
 
248
338
  vmin = kwargs.get("vmin", np.nanmin(global_map))
249
339
  vmax = kwargs.get("vmax", np.nanmax(global_map))
250
340
 
341
+ # Optional visualization
251
342
  if show_plot:
252
343
  cmap = plt.cm.get_cmap(colormap).copy()
253
344
  cmap.set_bad(color='lightgray')
@@ -283,10 +374,22 @@ def get_global_solar_irradiance_map(
283
374
 
284
375
  return global_map
285
376
 
286
- def get_solar_positions_astral(times, lat, lon):
377
+ def get_solar_positions_astral(times, lon, lat):
287
378
  """
288
379
  Compute solar azimuth and elevation using Astral for given times and location.
289
- Times must be timezone-aware.
380
+
381
+ The function:
382
+ 1. Creates an Astral observer at the specified location
383
+ 2. Computes sun position for each timestamp
384
+ 3. Returns DataFrame with azimuth and elevation angles
385
+
386
+ Args:
387
+ times (DatetimeIndex): Array of timezone-aware datetime objects.
388
+ lon (float): Longitude in degrees.
389
+ lat (float): Latitude in degrees.
390
+
391
+ Returns:
392
+ DataFrame: DataFrame with columns 'azimuth' and 'elevation' containing solar positions.
290
393
  """
291
394
  observer = Observer(latitude=lat, longitude=lon)
292
395
  df_pos = pd.DataFrame(index=times, columns=['azimuth', 'elevation'], dtype=float)
@@ -303,33 +406,49 @@ def get_solar_positions_astral(times, lat, lon):
303
406
  def get_cumulative_global_solar_irradiance(
304
407
  voxel_data,
305
408
  meshsize,
306
- df, lat, lon, tz,
409
+ df, lon, lat, tz,
307
410
  direct_normal_irradiance_scaling=1.0,
308
411
  diffuse_irradiance_scaling=1.0,
309
412
  **kwargs
310
413
  ):
311
414
  """
312
- Compute cumulative global solar irradiance over a specified period using data from an EPW file,
313
- accounting for tree transmittance.
415
+ Compute cumulative global solar irradiance over a specified period using data from an EPW file.
416
+
417
+ The function:
418
+ 1. Filters EPW data for specified time period
419
+ 2. Computes sun positions for each timestep
420
+ 3. Calculates and accumulates global irradiance maps
421
+ 4. Handles tree transmittance and visualization
314
422
 
315
423
  Args:
316
424
  voxel_data (ndarray): 3D array of voxel values.
317
425
  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.
426
+ df (DataFrame): EPW weather data.
427
+ lon (float): Longitude in degrees.
428
+ lat (float): Latitude in degrees.
429
+ tz (float): Timezone offset in hours.
430
+ direct_normal_irradiance_scaling (float): Scaling factor for direct normal irradiance.
431
+ diffuse_irradiance_scaling (float): Scaling factor for diffuse horizontal irradiance.
322
432
  **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
433
+ - view_point_height (float): Observer height in meters (default: 1.5)
434
+ - start_time (str): Start time in format 'MM-DD HH:MM:SS'
435
+ - end_time (str): End time in format 'MM-DD HH:MM:SS'
436
+ - tree_k (float): Tree extinction coefficient
437
+ - tree_lad (float): Leaf area density in m^-1
328
438
  - show_plot (bool): Whether to show final plot
329
439
  - show_each_timestep (bool): Whether to show plots for each timestep
440
+ - colormap (str): Matplotlib colormap name
441
+ - vmin (float): Minimum value for colormap
442
+ - vmax (float): Maximum value for colormap
443
+ - obj_export (bool): Whether to export as OBJ file
444
+ - output_directory (str): Directory for OBJ export
445
+ - output_file_name (str): Filename for OBJ export
446
+ - dem_grid (ndarray): DEM grid for OBJ export
447
+ - num_colors (int): Number of colors for OBJ export
448
+ - alpha (float): Alpha value for OBJ export
330
449
 
331
450
  Returns:
332
- ndarray: 2D array of cumulative global solar irradiance (W/m²·hour).
451
+ ndarray: 2D array of cumulative global solar irradiance values (W/m²·hour).
333
452
  """
334
453
  view_point_height = kwargs.get("view_point_height", 1.5)
335
454
  colormap = kwargs.get("colormap", 'magma')
@@ -346,20 +465,23 @@ def get_cumulative_global_solar_irradiance(
346
465
  except ValueError as ve:
347
466
  raise ValueError("start_time and end_time must be in format 'MM-DD HH:MM:SS'") from ve
348
467
 
349
- # Add hour of year column and filter data as before...
468
+ # Add hour of year column and filter data
350
469
  df['hour_of_year'] = (df.index.dayofyear - 1) * 24 + df.index.hour + 1
351
470
 
471
+ # Convert dates to day of year and hour
352
472
  start_doy = datetime(2000, start_dt.month, start_dt.day).timetuple().tm_yday
353
473
  end_doy = datetime(2000, end_dt.month, end_dt.day).timetuple().tm_yday
354
474
 
355
475
  start_hour = (start_doy - 1) * 24 + start_dt.hour + 1
356
476
  end_hour = (end_doy - 1) * 24 + end_dt.hour + 1
357
477
 
478
+ # Handle period crossing year boundary
358
479
  if start_hour <= end_hour:
359
480
  df_period = df[(df['hour_of_year'] >= start_hour) & (df['hour_of_year'] <= end_hour)]
360
481
  else:
361
482
  df_period = df[(df['hour_of_year'] >= start_hour) | (df['hour_of_year'] <= end_hour)]
362
483
 
484
+ # Filter by minutes within start/end hours
363
485
  df_period = df_period[
364
486
  ((df_period.index.hour != start_dt.hour) | (df_period.index.minute >= start_dt.minute)) &
365
487
  ((df_period.index.hour != end_dt.hour) | (df_period.index.minute <= end_dt.minute))
@@ -368,15 +490,15 @@ def get_cumulative_global_solar_irradiance(
368
490
  if df_period.empty:
369
491
  raise ValueError("No EPW data in the specified period.")
370
492
 
371
- # Prepare timezone conversion
493
+ # Handle timezone conversion
372
494
  offset_minutes = int(tz * 60)
373
495
  local_tz = pytz.FixedOffset(offset_minutes)
374
496
  df_period_local = df_period.copy()
375
497
  df_period_local.index = df_period_local.index.tz_localize(local_tz)
376
498
  df_period_utc = df_period_local.tz_convert(pytz.UTC)
377
499
 
378
- # Compute solar positions
379
- solar_positions = get_solar_positions_astral(df_period_utc.index, lat, lon)
500
+ # Compute solar positions for period
501
+ solar_positions = get_solar_positions_astral(df_period_utc.index, lon, lat)
380
502
 
381
503
  # Create kwargs for diffuse calculation
382
504
  diffuse_kwargs = kwargs.copy()
@@ -393,7 +515,7 @@ def get_cumulative_global_solar_irradiance(
393
515
  **diffuse_kwargs
394
516
  )
395
517
 
396
- # Initialize maps
518
+ # Initialize accumulation maps
397
519
  cumulative_map = np.zeros((voxel_data.shape[0], voxel_data.shape[1]))
398
520
  mask_map = np.ones((voxel_data.shape[0], voxel_data.shape[1]), dtype=bool)
399
521
 
@@ -405,13 +527,14 @@ def get_cumulative_global_solar_irradiance(
405
527
  'obj_export': False
406
528
  })
407
529
 
408
- # Iterate through each time step
530
+ # Process each timestep
409
531
  for idx, (time_utc, row) in enumerate(df_period_utc.iterrows()):
532
+ # Get scaled irradiance values
410
533
  DNI = row['DNI'] * direct_normal_irradiance_scaling
411
534
  DHI = row['DHI'] * diffuse_irradiance_scaling
412
535
  time_local = df_period_local.index[idx]
413
536
 
414
- # Get solar position
537
+ # Get solar position for timestep
415
538
  solpos = solar_positions.loc[time_utc]
416
539
  azimuth_degrees = solpos['azimuth']
417
540
  elevation_degrees = solpos['elevation']
@@ -426,13 +549,13 @@ def get_cumulative_global_solar_irradiance(
426
549
  **direct_kwargs
427
550
  )
428
551
 
429
- # Scale base_diffuse_map by actual DHI
552
+ # Scale base diffuse map by actual DHI
430
553
  diffuse_map = base_diffuse_map * DHI
431
554
 
432
- # Combine direct and diffuse
555
+ # Combine direct and diffuse components
433
556
  global_map = direct_map + diffuse_map
434
557
 
435
- # Update mask_map
558
+ # Update valid pixel mask
436
559
  mask_map &= ~np.isnan(global_map)
437
560
 
438
561
  # Replace NaN with 0 for accumulation
@@ -453,7 +576,7 @@ def get_cumulative_global_solar_irradiance(
453
576
  plt.colorbar(label='Global Solar Irradiance (W/m²)')
454
577
  plt.show()
455
578
 
456
- # Apply mask
579
+ # Apply mask to final result
457
580
  cumulative_map[~mask_map] = np.nan
458
581
 
459
582
  # Final visualization
@@ -506,27 +629,38 @@ def get_global_solar_irradiance_using_epw(
506
629
  **kwargs
507
630
  ):
508
631
  """
509
- Compute cumulative global solar irradiance over a specified period using data from an EPW file,
510
- accounting for tree transmittance.
632
+ Compute global solar irradiance using EPW weather data, either for a single time or cumulatively over a period.
511
633
 
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)
634
+ The function:
635
+ 1. Optionally downloads and reads EPW weather data
636
+ 2. Handles timezone conversions and solar position calculations
637
+ 3. Computes either instantaneous or cumulative irradiance maps
638
+ 4. Supports visualization and export options
639
+
640
+ Args:
641
+ voxel_data (ndarray): 3D array of voxel values.
642
+ meshsize (float): Size of each voxel in meters.
643
+ calc_type (str): 'instantaneous' or 'cumulative'.
644
+ direct_normal_irradiance_scaling (float): Scaling factor for direct normal irradiance.
645
+ diffuse_irradiance_scaling (float): Scaling factor for diffuse horizontal irradiance.
646
+ **kwargs: Additional arguments including:
523
647
  - download_nearest_epw (bool): Whether to download nearest EPW file
524
648
  - 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
649
+ - rectangle_vertices (list): List of (lat,lon) coordinates for EPW download
650
+ - output_dir (str): Directory for EPW download
651
+ - calc_time (str): Time for instantaneous calculation ('MM-DD HH:MM:SS')
652
+ - start_time (str): Start time for cumulative calculation
653
+ - end_time (str): End time for cumulative calculation
654
+ - view_point_height (float): Observer height in meters
655
+ - tree_k (float): Tree extinction coefficient
656
+ - tree_lad (float): Leaf area density in m^-1
657
+ - show_plot (bool): Whether to show visualization
658
+ - show_each_timestep (bool): Whether to show timestep plots
659
+ - colormap (str): Matplotlib colormap name
660
+ - obj_export (bool): Whether to export as OBJ file
527
661
 
528
662
  Returns:
529
- ndarray: 2D array of cumulative global solar irradiance (W/m²·hour).
663
+ ndarray: 2D array of solar irradiance values (W/m²).
530
664
  """
531
665
  view_point_height = kwargs.get("view_point_height", 1.5)
532
666
  colormap = kwargs.get("colormap", 'magma')