voxcity 0.2.1__tar.gz → 0.3.1__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.2.1 → voxcity-0.3.1}/PKG-INFO +2 -1
  2. {voxcity-0.2.1 → voxcity-0.3.1}/pyproject.toml +3 -2
  3. voxcity-0.3.1/src/voxcity/sim/__init_.py +3 -0
  4. voxcity-0.3.1/src/voxcity/sim/solar.py +529 -0
  5. voxcity-0.3.1/src/voxcity/sim/utils.py +6 -0
  6. {voxcity-0.2.1 → voxcity-0.3.1}/src/voxcity/sim/view.py +178 -71
  7. voxcity-0.3.1/src/voxcity/utils/__init_.py +3 -0
  8. voxcity-0.3.1/src/voxcity/utils/weather.py +523 -0
  9. {voxcity-0.2.1 → voxcity-0.3.1}/src/voxcity/voxcity.py +4 -1
  10. {voxcity-0.2.1 → voxcity-0.3.1}/src/voxcity.egg-info/PKG-INFO +2 -1
  11. {voxcity-0.2.1 → voxcity-0.3.1}/src/voxcity.egg-info/SOURCES.txt +3 -0
  12. {voxcity-0.2.1 → voxcity-0.3.1}/src/voxcity.egg-info/requires.txt +1 -0
  13. voxcity-0.2.1/src/voxcity/sim/__init_.py +0 -1
  14. voxcity-0.2.1/src/voxcity/utils/__init_.py +0 -2
  15. {voxcity-0.2.1 → voxcity-0.3.1}/AUTHORS.rst +0 -0
  16. {voxcity-0.2.1 → voxcity-0.3.1}/CONTRIBUTING.rst +0 -0
  17. {voxcity-0.2.1 → voxcity-0.3.1}/HISTORY.rst +0 -0
  18. {voxcity-0.2.1 → voxcity-0.3.1}/LICENSE +0 -0
  19. {voxcity-0.2.1 → voxcity-0.3.1}/MANIFEST.in +0 -0
  20. {voxcity-0.2.1 → voxcity-0.3.1}/README.md +0 -0
  21. {voxcity-0.2.1 → voxcity-0.3.1}/docs/Makefile +0 -0
  22. {voxcity-0.2.1 → voxcity-0.3.1}/docs/archive/README.rst +0 -0
  23. {voxcity-0.2.1 → voxcity-0.3.1}/docs/authors.rst +0 -0
  24. {voxcity-0.2.1 → voxcity-0.3.1}/docs/conf.py +0 -0
  25. {voxcity-0.2.1 → voxcity-0.3.1}/docs/index.rst +0 -0
  26. {voxcity-0.2.1 → voxcity-0.3.1}/docs/make.bat +0 -0
  27. {voxcity-0.2.1 → voxcity-0.3.1}/setup.cfg +0 -0
  28. {voxcity-0.2.1 → voxcity-0.3.1}/src/voxcity/__init__.py +0 -0
  29. {voxcity-0.2.1 → voxcity-0.3.1}/src/voxcity/download/__init__.py +0 -0
  30. {voxcity-0.2.1 → voxcity-0.3.1}/src/voxcity/download/eubucco.py +0 -0
  31. {voxcity-0.2.1 → voxcity-0.3.1}/src/voxcity/download/gee.py +0 -0
  32. {voxcity-0.2.1 → voxcity-0.3.1}/src/voxcity/download/mbfp.py +0 -0
  33. {voxcity-0.2.1 → voxcity-0.3.1}/src/voxcity/download/oemj.py +0 -0
  34. {voxcity-0.2.1 → voxcity-0.3.1}/src/voxcity/download/omt.py +0 -0
  35. {voxcity-0.2.1 → voxcity-0.3.1}/src/voxcity/download/osm.py +0 -0
  36. {voxcity-0.2.1 → voxcity-0.3.1}/src/voxcity/download/overture.py +0 -0
  37. {voxcity-0.2.1 → voxcity-0.3.1}/src/voxcity/download/utils.py +0 -0
  38. {voxcity-0.2.1 → voxcity-0.3.1}/src/voxcity/file/__init_.py +0 -0
  39. {voxcity-0.2.1 → voxcity-0.3.1}/src/voxcity/file/envimet.py +0 -0
  40. {voxcity-0.2.1 → voxcity-0.3.1}/src/voxcity/file/geojson.py +0 -0
  41. {voxcity-0.2.1 → voxcity-0.3.1}/src/voxcity/file/magicavoxel.py +0 -0
  42. {voxcity-0.2.1 → voxcity-0.3.1}/src/voxcity/file/obj.py +0 -0
  43. {voxcity-0.2.1 → voxcity-0.3.1}/src/voxcity/geo/__init_.py +0 -0
  44. {voxcity-0.2.1 → voxcity-0.3.1}/src/voxcity/geo/draw.py +0 -0
  45. {voxcity-0.2.1 → voxcity-0.3.1}/src/voxcity/geo/grid.py +0 -0
  46. {voxcity-0.2.1 → voxcity-0.3.1}/src/voxcity/geo/utils.py +0 -0
  47. {voxcity-0.2.1 → voxcity-0.3.1}/src/voxcity/utils/lc.py +0 -0
  48. {voxcity-0.2.1 → voxcity-0.3.1}/src/voxcity/utils/visualization.py +0 -0
  49. {voxcity-0.2.1 → voxcity-0.3.1}/src/voxcity.egg-info/dependency_links.txt +0 -0
  50. {voxcity-0.2.1 → voxcity-0.3.1}/src/voxcity.egg-info/top_level.txt +0 -0
  51. {voxcity-0.2.1 → voxcity-0.3.1}/tests/__init__.py +0 -0
  52. {voxcity-0.2.1 → voxcity-0.3.1}/tests/voxelcity.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: voxcity
3
- Version: 0.2.1
3
+ Version: 0.3.1
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.2.1"
3
+ version = "0.3.1"
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]
@@ -0,0 +1,3 @@
1
+ from .view import *
2
+ from .solar import *
3
+ from .utils import *
@@ -0,0 +1,529 @@
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
+ start_time,
307
+ end_time,
308
+ direct_normal_irradiance_scaling=1.0,
309
+ diffuse_irradiance_scaling=1.0,
310
+ **kwargs
311
+ ):
312
+ """
313
+ Compute cumulative global solar irradiance over a specified period using data from an EPW file,
314
+ accounting for tree transmittance.
315
+
316
+ Args:
317
+ voxel_data (ndarray): 3D array of voxel values.
318
+ meshsize (float): Size of each voxel in meters.
319
+ start_time (str): Start time in format 'MM-DD HH:MM:SS' (no year).
320
+ end_time (str): End time in format 'MM-DD HH:MM:SS' (no year).
321
+ direct_normal_irradiance_scaling (float): Scaling factor for DNI.
322
+ diffuse_irradiance_scaling (float): Scaling factor for DHI.
323
+ **kwargs: Additional arguments including:
324
+ - view_point_height (float): Observer height in meters
325
+ - tree_k (float): Tree extinction coefficient (default: 0.5)
326
+ - tree_lad (float): Leaf area density in m^-1 (default: 1.0)
327
+ - download_nearest_epw (bool): Whether to download nearest EPW file
328
+ - epw_file_path (str): Path to EPW file
329
+ - show_plot (bool): Whether to show final plot
330
+ - show_each_timestep (bool): Whether to show plots for each timestep
331
+
332
+ Returns:
333
+ ndarray: 2D array of cumulative global solar irradiance (W/m²·hour).
334
+ """
335
+ view_point_height = kwargs.get("view_point_height", 1.5)
336
+ colormap = kwargs.get("colormap", 'magma')
337
+
338
+ # Get EPW file
339
+ download_nearest_epw = kwargs.get("download_nearest_epw", False)
340
+ rectangle_vertices = kwargs.get("rectangle_vertices", None)
341
+ epw_file_path = kwargs.get("epw_file_path", None)
342
+ if download_nearest_epw:
343
+ if rectangle_vertices is None:
344
+ print("rectangle_vertices is required to download nearest EPW file")
345
+ return None
346
+ else:
347
+ # Calculate center point of rectangle
348
+ lats = [coord[0] for coord in rectangle_vertices]
349
+ lons = [coord[1] for coord in rectangle_vertices]
350
+ center_lat = (min(lats) + max(lats)) / 2
351
+ center_lon = (min(lons) + max(lons)) / 2
352
+ target_point = (center_lat, center_lon)
353
+
354
+ # Optional: specify maximum distance in kilometers
355
+ max_distance = 100 # None for no limit
356
+
357
+ output_dir = kwargs.get("output_dir", "output")
358
+
359
+ epw_file_path, weather_data, metadata = get_nearest_epw_from_climate_onebuilding(
360
+ latitude=center_lat,
361
+ longitude=center_lon,
362
+ output_dir=output_dir,
363
+ max_distance=max_distance,
364
+ extract_zip=True,
365
+ load_data=True
366
+ )
367
+
368
+ # Read EPW data
369
+ df, lat, lon, tz, elevation_m = read_epw_for_solar_simulation(epw_file_path)
370
+ if df.empty:
371
+ raise ValueError("No data in EPW file.")
372
+
373
+ # Parse start and end times without year
374
+ try:
375
+ start_dt = datetime.strptime(start_time, "%m-%d %H:%M:%S")
376
+ end_dt = datetime.strptime(end_time, "%m-%d %H:%M:%S")
377
+ except ValueError as ve:
378
+ raise ValueError("start_time and end_time must be in format 'MM-DD HH:MM:SS'") from ve
379
+
380
+ # Add hour of year column and filter data as before...
381
+ df['hour_of_year'] = (df.index.dayofyear - 1) * 24 + df.index.hour + 1
382
+
383
+ start_doy = datetime(2000, start_dt.month, start_dt.day).timetuple().tm_yday
384
+ end_doy = datetime(2000, end_dt.month, end_dt.day).timetuple().tm_yday
385
+
386
+ start_hour = (start_doy - 1) * 24 + start_dt.hour + 1
387
+ end_hour = (end_doy - 1) * 24 + end_dt.hour + 1
388
+
389
+ if start_hour <= end_hour:
390
+ df_period = df[(df['hour_of_year'] >= start_hour) & (df['hour_of_year'] <= end_hour)]
391
+ else:
392
+ df_period = df[(df['hour_of_year'] >= start_hour) | (df['hour_of_year'] <= end_hour)]
393
+
394
+ df_period = df_period[
395
+ ((df_period.index.hour != start_dt.hour) | (df_period.index.minute >= start_dt.minute)) &
396
+ ((df_period.index.hour != end_dt.hour) | (df_period.index.minute <= end_dt.minute))
397
+ ]
398
+
399
+ if df_period.empty:
400
+ raise ValueError("No EPW data in the specified period.")
401
+
402
+ # Prepare timezone conversion
403
+ offset_minutes = int(tz * 60)
404
+ local_tz = pytz.FixedOffset(offset_minutes)
405
+ df_period_local = df_period.copy()
406
+ df_period_local.index = df_period_local.index.tz_localize(local_tz)
407
+ df_period_utc = df_period_local.tz_convert(pytz.UTC)
408
+
409
+ # Compute solar positions
410
+ solar_positions = get_solar_positions_astral(df_period_utc.index, lat, lon)
411
+
412
+ # Create kwargs for diffuse calculation
413
+ diffuse_kwargs = kwargs.copy()
414
+ diffuse_kwargs.update({
415
+ 'show_plot': False,
416
+ 'obj_export': False
417
+ })
418
+
419
+ # Compute base diffuse map once with diffuse_irradiance=1.0
420
+ base_diffuse_map = get_diffuse_solar_irradiance_map(
421
+ voxel_data,
422
+ meshsize,
423
+ diffuse_irradiance=1.0,
424
+ **diffuse_kwargs
425
+ )
426
+
427
+ # Initialize maps
428
+ cumulative_map = np.zeros((voxel_data.shape[0], voxel_data.shape[1]))
429
+ mask_map = np.ones((voxel_data.shape[0], voxel_data.shape[1]), dtype=bool)
430
+
431
+ # Create kwargs for direct calculation
432
+ direct_kwargs = kwargs.copy()
433
+ direct_kwargs.update({
434
+ 'show_plot': False,
435
+ 'view_point_height': view_point_height,
436
+ 'obj_export': False
437
+ })
438
+
439
+ # Iterate through each time step
440
+ for idx, (time_utc, row) in enumerate(df_period_utc.iterrows()):
441
+ DNI = row['DNI'] * direct_normal_irradiance_scaling
442
+ DHI = row['DHI'] * diffuse_irradiance_scaling
443
+ time_local = df_period_local.index[idx]
444
+
445
+ # Get solar position
446
+ solpos = solar_positions.loc[time_utc]
447
+ azimuth_degrees = solpos['azimuth']
448
+ elevation_degrees = solpos['elevation']
449
+
450
+ # Compute direct irradiance map with transmittance
451
+ direct_map = get_direct_solar_irradiance_map(
452
+ voxel_data,
453
+ meshsize,
454
+ azimuth_degrees,
455
+ elevation_degrees,
456
+ direct_normal_irradiance=DNI,
457
+ **direct_kwargs
458
+ )
459
+
460
+ # Scale base_diffuse_map by actual DHI
461
+ diffuse_map = base_diffuse_map * DHI
462
+
463
+ # Combine direct and diffuse
464
+ global_map = direct_map + diffuse_map
465
+
466
+ # Update mask_map
467
+ mask_map &= ~np.isnan(global_map)
468
+
469
+ # Replace NaN with 0 for accumulation
470
+ global_map_filled = np.nan_to_num(global_map, nan=0.0)
471
+ cumulative_map += global_map_filled
472
+
473
+ # Optional timestep visualization
474
+ show_each_timestep = kwargs.get("show_each_timestep", False)
475
+ if show_each_timestep:
476
+ colormap = kwargs.get("colormap", 'viridis')
477
+ vmin = kwargs.get("vmin", 0.0)
478
+ vmax = kwargs.get("vmax", max(direct_normal_irradiance_scaling, diffuse_irradiance_scaling) * 1000)
479
+ cmap = plt.cm.get_cmap(colormap).copy()
480
+ cmap.set_bad(color='lightgray')
481
+ plt.figure(figsize=(8, 6))
482
+ plt.title(f"Global Solar Irradiance at {time_local.strftime('%Y-%m-%d %H:%M:%S')}")
483
+ plt.imshow(global_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
484
+ plt.colorbar(label='Global Solar Irradiance (W/m²)')
485
+ plt.show()
486
+
487
+ # Apply mask
488
+ cumulative_map[~mask_map] = np.nan
489
+
490
+ # Final visualization
491
+ show_plot = kwargs.get("show_plot", True)
492
+ if show_plot:
493
+ colormap = kwargs.get("colormap", 'magma')
494
+ vmin = kwargs.get("vmin", np.nanmin(cumulative_map))
495
+ vmax = kwargs.get("vmax", np.nanmax(cumulative_map))
496
+ cmap = plt.cm.get_cmap(colormap).copy()
497
+ cmap.set_bad(color='lightgray')
498
+ plt.figure(figsize=(8, 6))
499
+ plt.title("Cumulative Global Solar Irradiance Map")
500
+ plt.imshow(cumulative_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
501
+ plt.colorbar(label='Cumulative Global Solar Irradiance (W/m²·hour)')
502
+ plt.show()
503
+
504
+ # Optional OBJ export
505
+ obj_export = kwargs.get("obj_export", False)
506
+ if obj_export:
507
+ colormap = kwargs.get("colormap", "magma")
508
+ vmin = kwargs.get("vmin", np.nanmin(cumulative_map))
509
+ vmax = kwargs.get("vmax", np.nanmax(cumulative_map))
510
+ dem_grid = kwargs.get("dem_grid", np.zeros_like(cumulative_map))
511
+ output_dir = kwargs.get("output_directory", "output")
512
+ output_file_name = kwargs.get("output_file_name", "cummurative_global_solar_irradiance")
513
+ num_colors = kwargs.get("num_colors", 10)
514
+ alpha = kwargs.get("alpha", 1.0)
515
+ grid_to_obj(
516
+ cumulative_map,
517
+ dem_grid,
518
+ output_dir,
519
+ output_file_name,
520
+ meshsize,
521
+ view_point_height,
522
+ colormap_name=colormap,
523
+ num_colors=num_colors,
524
+ alpha=alpha,
525
+ vmin=vmin,
526
+ vmax=vmax
527
+ )
528
+
529
+ return cumulative_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