voxcity 0.3.0__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.
- {voxcity-0.3.0 → voxcity-0.3.1}/PKG-INFO +2 -1
- {voxcity-0.3.0 → voxcity-0.3.1}/pyproject.toml +3 -2
- {voxcity-0.3.0 → voxcity-0.3.1}/src/voxcity/sim/__init_.py +1 -0
- voxcity-0.3.1/src/voxcity/sim/solar.py +529 -0
- voxcity-0.3.1/src/voxcity/sim/utils.py +6 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/src/voxcity/sim/view.py +114 -108
- voxcity-0.3.1/src/voxcity/utils/__init_.py +3 -0
- voxcity-0.3.1/src/voxcity/utils/weather.py +523 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/src/voxcity/voxcity.py +4 -1
- {voxcity-0.3.0 → voxcity-0.3.1}/src/voxcity.egg-info/PKG-INFO +2 -1
- {voxcity-0.3.0 → voxcity-0.3.1}/src/voxcity.egg-info/SOURCES.txt +2 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/src/voxcity.egg-info/requires.txt +1 -0
- voxcity-0.3.0/src/voxcity/sim/solar.py +0 -266
- voxcity-0.3.0/src/voxcity/utils/__init_.py +0 -2
- {voxcity-0.3.0 → voxcity-0.3.1}/AUTHORS.rst +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/CONTRIBUTING.rst +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/HISTORY.rst +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/LICENSE +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/MANIFEST.in +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/README.md +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/docs/Makefile +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/docs/archive/README.rst +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/docs/authors.rst +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/docs/conf.py +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/docs/index.rst +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/docs/make.bat +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/setup.cfg +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/src/voxcity/__init__.py +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/src/voxcity/download/__init__.py +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/src/voxcity/download/eubucco.py +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/src/voxcity/download/gee.py +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/src/voxcity/download/mbfp.py +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/src/voxcity/download/oemj.py +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/src/voxcity/download/omt.py +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/src/voxcity/download/osm.py +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/src/voxcity/download/overture.py +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/src/voxcity/download/utils.py +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/src/voxcity/file/__init_.py +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/src/voxcity/file/envimet.py +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/src/voxcity/file/geojson.py +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/src/voxcity/file/magicavoxel.py +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/src/voxcity/file/obj.py +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/src/voxcity/geo/__init_.py +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/src/voxcity/geo/draw.py +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/src/voxcity/geo/grid.py +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/src/voxcity/geo/utils.py +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/src/voxcity/utils/lc.py +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/src/voxcity/utils/visualization.py +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/src/voxcity.egg-info/dependency_links.txt +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/src/voxcity.egg-info/top_level.txt +0 -0
- {voxcity-0.3.0 → voxcity-0.3.1}/tests/__init__.py +0 -0
- {voxcity-0.3.0 → 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.3.
|
|
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.3.
|
|
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,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
|