voxcity 0.3.0__tar.gz → 0.3.2__tar.gz

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.

Files changed (52) hide show
  1. {voxcity-0.3.0 → voxcity-0.3.2}/PKG-INFO +2 -1
  2. {voxcity-0.3.0 → voxcity-0.3.2}/pyproject.toml +3 -2
  3. {voxcity-0.3.0 → voxcity-0.3.2}/src/voxcity/sim/__init_.py +1 -0
  4. voxcity-0.3.2/src/voxcity/sim/solar.py +619 -0
  5. voxcity-0.3.2/src/voxcity/sim/utils.py +6 -0
  6. {voxcity-0.3.0 → voxcity-0.3.2}/src/voxcity/sim/view.py +114 -108
  7. voxcity-0.3.2/src/voxcity/utils/__init_.py +3 -0
  8. voxcity-0.3.2/src/voxcity/utils/weather.py +523 -0
  9. {voxcity-0.3.0 → voxcity-0.3.2}/src/voxcity/voxcity.py +4 -1
  10. {voxcity-0.3.0 → voxcity-0.3.2}/src/voxcity.egg-info/PKG-INFO +2 -1
  11. {voxcity-0.3.0 → voxcity-0.3.2}/src/voxcity.egg-info/SOURCES.txt +2 -0
  12. {voxcity-0.3.0 → voxcity-0.3.2}/src/voxcity.egg-info/requires.txt +1 -0
  13. voxcity-0.3.0/src/voxcity/sim/solar.py +0 -266
  14. voxcity-0.3.0/src/voxcity/utils/__init_.py +0 -2
  15. {voxcity-0.3.0 → voxcity-0.3.2}/AUTHORS.rst +0 -0
  16. {voxcity-0.3.0 → voxcity-0.3.2}/CONTRIBUTING.rst +0 -0
  17. {voxcity-0.3.0 → voxcity-0.3.2}/HISTORY.rst +0 -0
  18. {voxcity-0.3.0 → voxcity-0.3.2}/LICENSE +0 -0
  19. {voxcity-0.3.0 → voxcity-0.3.2}/MANIFEST.in +0 -0
  20. {voxcity-0.3.0 → voxcity-0.3.2}/README.md +0 -0
  21. {voxcity-0.3.0 → voxcity-0.3.2}/docs/Makefile +0 -0
  22. {voxcity-0.3.0 → voxcity-0.3.2}/docs/archive/README.rst +0 -0
  23. {voxcity-0.3.0 → voxcity-0.3.2}/docs/authors.rst +0 -0
  24. {voxcity-0.3.0 → voxcity-0.3.2}/docs/conf.py +0 -0
  25. {voxcity-0.3.0 → voxcity-0.3.2}/docs/index.rst +0 -0
  26. {voxcity-0.3.0 → voxcity-0.3.2}/docs/make.bat +0 -0
  27. {voxcity-0.3.0 → voxcity-0.3.2}/setup.cfg +0 -0
  28. {voxcity-0.3.0 → voxcity-0.3.2}/src/voxcity/__init__.py +0 -0
  29. {voxcity-0.3.0 → voxcity-0.3.2}/src/voxcity/download/__init__.py +0 -0
  30. {voxcity-0.3.0 → voxcity-0.3.2}/src/voxcity/download/eubucco.py +0 -0
  31. {voxcity-0.3.0 → voxcity-0.3.2}/src/voxcity/download/gee.py +0 -0
  32. {voxcity-0.3.0 → voxcity-0.3.2}/src/voxcity/download/mbfp.py +0 -0
  33. {voxcity-0.3.0 → voxcity-0.3.2}/src/voxcity/download/oemj.py +0 -0
  34. {voxcity-0.3.0 → voxcity-0.3.2}/src/voxcity/download/omt.py +0 -0
  35. {voxcity-0.3.0 → voxcity-0.3.2}/src/voxcity/download/osm.py +0 -0
  36. {voxcity-0.3.0 → voxcity-0.3.2}/src/voxcity/download/overture.py +0 -0
  37. {voxcity-0.3.0 → voxcity-0.3.2}/src/voxcity/download/utils.py +0 -0
  38. {voxcity-0.3.0 → voxcity-0.3.2}/src/voxcity/file/__init_.py +0 -0
  39. {voxcity-0.3.0 → voxcity-0.3.2}/src/voxcity/file/envimet.py +0 -0
  40. {voxcity-0.3.0 → voxcity-0.3.2}/src/voxcity/file/geojson.py +0 -0
  41. {voxcity-0.3.0 → voxcity-0.3.2}/src/voxcity/file/magicavoxel.py +0 -0
  42. {voxcity-0.3.0 → voxcity-0.3.2}/src/voxcity/file/obj.py +0 -0
  43. {voxcity-0.3.0 → voxcity-0.3.2}/src/voxcity/geo/__init_.py +0 -0
  44. {voxcity-0.3.0 → voxcity-0.3.2}/src/voxcity/geo/draw.py +0 -0
  45. {voxcity-0.3.0 → voxcity-0.3.2}/src/voxcity/geo/grid.py +0 -0
  46. {voxcity-0.3.0 → voxcity-0.3.2}/src/voxcity/geo/utils.py +0 -0
  47. {voxcity-0.3.0 → voxcity-0.3.2}/src/voxcity/utils/lc.py +0 -0
  48. {voxcity-0.3.0 → voxcity-0.3.2}/src/voxcity/utils/visualization.py +0 -0
  49. {voxcity-0.3.0 → voxcity-0.3.2}/src/voxcity.egg-info/dependency_links.txt +0 -0
  50. {voxcity-0.3.0 → voxcity-0.3.2}/src/voxcity.egg-info/top_level.txt +0 -0
  51. {voxcity-0.3.0 → voxcity-0.3.2}/tests/__init__.py +0 -0
  52. {voxcity-0.3.0 → voxcity-0.3.2}/tests/voxelcity.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: voxcity
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: voxcity is an easy and one-stop tool to output 3d city models for microclimate simulation by integrating multiple geospatial open-data
5
5
  Author-email: Kunihiko Fujiwara <kunihiko@nus.edu.sg>
6
6
  Maintainer-email: Kunihiko Fujiwara <kunihiko@nus.edu.sg>
@@ -47,6 +47,7 @@ Requires-Dist: seaborn
47
47
  Requires-Dist: overturemaps
48
48
  Requires-Dist: protobuf==3.20.3
49
49
  Requires-Dist: timezonefinder
50
+ Requires-Dist: astral
50
51
  Provides-Extra: dev
51
52
  Requires-Dist: coverage; extra == "dev"
52
53
  Requires-Dist: mypy; extra == "dev"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "voxcity"
3
- version = "0.3.0"
3
+ version = "0.3.2"
4
4
  requires-python = ">=3.10,<3.13"
5
5
  classifiers = [
6
6
  "Programming Language :: Python :: 3.10",
@@ -48,7 +48,8 @@ dependencies = [
48
48
  "seaborn",
49
49
  "overturemaps",
50
50
  "protobuf==3.20.3",
51
- "timezonefinder"
51
+ "timezonefinder",
52
+ "astral",
52
53
  ]
53
54
 
54
55
  [project.optional-dependencies]
@@ -1,2 +1,3 @@
1
1
  from .view import *
2
2
  from .solar import *
3
+ from .utils import *
@@ -0,0 +1,619 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ import matplotlib.pyplot as plt
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
9
+
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
13
+
14
+ @njit(parallel=True)
15
+ def compute_direct_solar_irradiance_map_binary(voxel_data, sun_direction, view_point_height, hit_values, meshsize, tree_k, tree_lad, inclusion_mode):
16
+ """
17
+ Compute a map of direct solar irradiation accounting for tree transmittance.
18
+
19
+ Args:
20
+ voxel_data (ndarray): 3D array of voxel values.
21
+ sun_direction (tuple): Direction vector of the sun.
22
+ view_height_voxel (int): Observer height in voxel units.
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.
27
+ inclusion_mode (bool): False here, meaning any voxel not in hit_values is an obstacle.
28
+
29
+ Returns:
30
+ ndarray: 2D array of transmittance values (0.0-1.0), NaN = invalid observer.
31
+ """
32
+
33
+ view_height_voxel = int(view_point_height / meshsize)
34
+
35
+ nx, ny, nz = voxel_data.shape
36
+ irradiance_map = np.full((nx, ny), np.nan, dtype=np.float64)
37
+
38
+ # Normalize sun direction
39
+ sd = np.array(sun_direction, dtype=np.float64)
40
+ sd_len = np.sqrt(sd[0]**2 + sd[1]**2 + sd[2]**2)
41
+ if sd_len == 0.0:
42
+ return np.flipud(irradiance_map)
43
+ sd /= sd_len
44
+
45
+ for x in prange(nx):
46
+ for y in range(ny):
47
+ found_observer = False
48
+ for z in range(1, nz):
49
+ if voxel_data[x, y, z] in (0, -2) and voxel_data[x, y, z - 1] not in (0, -2):
50
+ if voxel_data[x, y, z - 1] in (-30, -3, -2):
51
+ irradiance_map[x, y] = np.nan
52
+ found_observer = True
53
+ break
54
+ else:
55
+ # Place observer and cast a ray in sun direction
56
+ observer_location = np.array([x, y, z + view_height_voxel], dtype=np.float64)
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
60
+ found_observer = True
61
+ break
62
+ if not found_observer:
63
+ irradiance_map[x, y] = np.nan
64
+
65
+ return np.flipud(irradiance_map)
66
+
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
+ """
72
+ view_point_height = kwargs.get("view_point_height", 1.5)
73
+ colormap = kwargs.get("colormap", 'magma')
74
+ vmin = kwargs.get("vmin", 0.0)
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)
80
+
81
+ # Convert angles to direction
82
+ azimuth_degrees = 180 - azimuth_degrees_ori
83
+ azimuth_radians = np.deg2rad(azimuth_degrees)
84
+ elevation_radians = np.deg2rad(elevation_degrees)
85
+ dx = np.cos(elevation_radians) * np.cos(azimuth_radians)
86
+ dy = np.cos(elevation_radians) * np.sin(azimuth_radians)
87
+ dz = np.sin(elevation_radians)
88
+ sun_direction = (dx, dy, dz)
89
+
90
+ # All non-zero voxels are obstacles except for trees which have transmittance
91
+ hit_values = (0,)
92
+ inclusion_mode = False
93
+
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
97
+ )
98
+
99
+ sin_elev = dz
100
+ direct_map = transmittance_map * direct_normal_irradiance * sin_elev
101
+
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()
110
+
111
+ # Optional OBJ export
112
+ obj_export = kwargs.get("obj_export", False)
113
+ if obj_export:
114
+ dem_grid = kwargs.get("dem_grid", np.zeros_like(direct_map))
115
+ output_dir = kwargs.get("output_directory", "output")
116
+ output_file_name = kwargs.get("output_file_name", "direct_solar_irradiance")
117
+ num_colors = kwargs.get("num_colors", 10)
118
+ alpha = kwargs.get("alpha", 1.0)
119
+ grid_to_obj(
120
+ direct_map,
121
+ dem_grid,
122
+ output_dir,
123
+ output_file_name,
124
+ meshsize,
125
+ view_point_height,
126
+ colormap_name=colormap,
127
+ num_colors=num_colors,
128
+ alpha=alpha,
129
+ vmin=vmin,
130
+ vmax=vmax
131
+ )
132
+
133
+ return direct_map
134
+
135
+ def get_diffuse_solar_irradiance_map(voxel_data, meshsize, diffuse_irradiance=1.0, show_plot=False, **kwargs):
136
+ """
137
+ Compute diffuse solar irradiance map using the Sky View Factor (SVF) with tree transmittance.
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
146
+ svf_kwargs = kwargs.copy()
147
+ svf_kwargs["colormap"] = "BuPu_r"
148
+ svf_kwargs["vmin"] = 0
149
+ svf_kwargs["vmax"] = 1
150
+
151
+ # SVF calculation now handles tree transmittance internally
152
+ SVF_map = get_sky_view_factor_map(voxel_data, meshsize, **svf_kwargs)
153
+ diffuse_map = SVF_map * diffuse_irradiance
154
+
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()
165
+
166
+ # Optional OBJ export
167
+ obj_export = kwargs.get("obj_export", False)
168
+ if obj_export:
169
+ dem_grid = kwargs.get("dem_grid", np.zeros_like(diffuse_map))
170
+ output_dir = kwargs.get("output_directory", "output")
171
+ output_file_name = kwargs.get("output_file_name", "diffuse_solar_irradiance")
172
+ num_colors = kwargs.get("num_colors", 10)
173
+ alpha = kwargs.get("alpha", 1.0)
174
+ grid_to_obj(
175
+ diffuse_map,
176
+ dem_grid,
177
+ output_dir,
178
+ output_file_name,
179
+ meshsize,
180
+ view_point_height,
181
+ colormap_name=colormap,
182
+ num_colors=num_colors,
183
+ alpha=alpha,
184
+ vmin=vmin,
185
+ vmax=vmax
186
+ )
187
+
188
+ return diffuse_map
189
+
190
+
191
+ def get_global_solar_irradiance_map(
192
+ voxel_data,
193
+ meshsize,
194
+ azimuth_degrees,
195
+ elevation_degrees,
196
+ direct_normal_irradiance,
197
+ diffuse_irradiance,
198
+ show_plot=False,
199
+ **kwargs
200
+ ):
201
+ """
202
+ Compute global solar irradiance (direct + diffuse) on a horizontal plane at each valid observer location.
203
+
204
+ No mode/hit_values/inclusion_mode needed. Uses the updated direct and diffuse functions.
205
+
206
+ Args:
207
+ voxel_data (ndarray): 3D voxel array.
208
+ 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².
213
+
214
+ Returns:
215
+ ndarray: 2D array of global solar irradiance (W/m²).
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
+
227
+ # Compute direct irradiance map (no mode/hit_values/inclusion_mode needed)
228
+ direct_map = get_direct_solar_irradiance_map(
229
+ voxel_data,
230
+ meshsize,
231
+ azimuth_degrees,
232
+ elevation_degrees,
233
+ direct_normal_irradiance,
234
+ **direct_diffuse_kwargs
235
+ )
236
+
237
+ # Compute diffuse irradiance map
238
+ diffuse_map = get_diffuse_solar_irradiance_map(
239
+ voxel_data,
240
+ meshsize,
241
+ diffuse_irradiance=diffuse_irradiance,
242
+ **direct_diffuse_kwargs
243
+ )
244
+
245
+ # Sum the two
246
+ global_map = direct_map + diffuse_map
247
+
248
+ vmin = kwargs.get("vmin", np.nanmin(global_map))
249
+ vmax = kwargs.get("vmax", np.nanmax(global_map))
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()
259
+
260
+ # Optional OBJ export
261
+ obj_export = kwargs.get("obj_export", False)
262
+ if obj_export:
263
+ dem_grid = kwargs.get("dem_grid", np.zeros_like(global_map))
264
+ output_dir = kwargs.get("output_directory", "output")
265
+ output_file_name = kwargs.get("output_file_name", "global_solar_irradiance")
266
+ num_colors = kwargs.get("num_colors", 10)
267
+ alpha = kwargs.get("alpha", 1.0)
268
+ meshsize_param = kwargs.get("meshsize", meshsize)
269
+ view_point_height = kwargs.get("view_point_height", 1.5)
270
+ grid_to_obj(
271
+ global_map,
272
+ dem_grid,
273
+ output_dir,
274
+ output_file_name,
275
+ meshsize_param,
276
+ view_point_height,
277
+ colormap_name=colormap,
278
+ num_colors=num_colors,
279
+ alpha=alpha,
280
+ vmin=vmin,
281
+ vmax=vmax
282
+ )
283
+
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
@@ -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