voxcity 0.6.26__py3-none-any.whl → 1.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- voxcity/__init__.py +10 -4
- voxcity/downloader/__init__.py +2 -1
- voxcity/downloader/gba.py +210 -0
- voxcity/downloader/gee.py +5 -1
- voxcity/downloader/mbfp.py +1 -1
- voxcity/downloader/oemj.py +80 -8
- voxcity/downloader/utils.py +73 -73
- voxcity/errors.py +30 -0
- voxcity/exporter/__init__.py +9 -1
- voxcity/exporter/cityles.py +129 -34
- voxcity/exporter/envimet.py +51 -26
- voxcity/exporter/magicavoxel.py +42 -5
- voxcity/exporter/netcdf.py +27 -0
- voxcity/exporter/obj.py +103 -28
- voxcity/generator/__init__.py +47 -0
- voxcity/generator/api.py +721 -0
- voxcity/generator/grids.py +381 -0
- voxcity/generator/io.py +94 -0
- voxcity/generator/pipeline.py +282 -0
- voxcity/generator/update.py +429 -0
- voxcity/generator/voxelizer.py +392 -0
- voxcity/geoprocessor/__init__.py +75 -6
- voxcity/geoprocessor/conversion.py +153 -0
- voxcity/geoprocessor/draw.py +1488 -1169
- voxcity/geoprocessor/heights.py +199 -0
- voxcity/geoprocessor/io.py +101 -0
- voxcity/geoprocessor/merge_utils.py +91 -0
- voxcity/geoprocessor/mesh.py +26 -10
- voxcity/geoprocessor/network.py +35 -6
- voxcity/geoprocessor/overlap.py +84 -0
- voxcity/geoprocessor/raster/__init__.py +82 -0
- voxcity/geoprocessor/raster/buildings.py +435 -0
- voxcity/geoprocessor/raster/canopy.py +258 -0
- voxcity/geoprocessor/raster/core.py +150 -0
- voxcity/geoprocessor/raster/export.py +93 -0
- voxcity/geoprocessor/raster/landcover.py +159 -0
- voxcity/geoprocessor/raster/raster.py +110 -0
- voxcity/geoprocessor/selection.py +85 -0
- voxcity/geoprocessor/utils.py +824 -820
- voxcity/models.py +113 -0
- voxcity/simulator/common/__init__.py +22 -0
- voxcity/simulator/common/geometry.py +98 -0
- voxcity/simulator/common/raytracing.py +450 -0
- voxcity/simulator/solar/__init__.py +66 -0
- voxcity/simulator/solar/integration.py +336 -0
- voxcity/simulator/solar/kernels.py +62 -0
- voxcity/simulator/solar/radiation.py +648 -0
- voxcity/simulator/solar/sky.py +668 -0
- voxcity/simulator/solar/temporal.py +792 -0
- voxcity/simulator/view.py +36 -2286
- voxcity/simulator/visibility/__init__.py +29 -0
- voxcity/simulator/visibility/landmark.py +392 -0
- voxcity/simulator/visibility/view.py +508 -0
- voxcity/utils/__init__.py +11 -0
- voxcity/utils/classes.py +194 -0
- voxcity/utils/lc.py +80 -39
- voxcity/utils/logging.py +61 -0
- voxcity/utils/orientation.py +51 -0
- voxcity/utils/shape.py +230 -0
- voxcity/utils/weather/__init__.py +26 -0
- voxcity/utils/weather/epw.py +146 -0
- voxcity/utils/weather/files.py +36 -0
- voxcity/utils/weather/onebuilding.py +486 -0
- voxcity/visualizer/__init__.py +24 -0
- voxcity/visualizer/builder.py +43 -0
- voxcity/visualizer/grids.py +141 -0
- voxcity/visualizer/maps.py +187 -0
- voxcity/visualizer/palette.py +228 -0
- voxcity/visualizer/renderer.py +1145 -0
- {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/METADATA +162 -48
- voxcity-1.0.2.dist-info/RECORD +81 -0
- voxcity/generator.py +0 -1302
- voxcity/geoprocessor/grid.py +0 -1739
- voxcity/geoprocessor/polygon.py +0 -1344
- voxcity/simulator/solar.py +0 -2339
- voxcity/utils/visualization.py +0 -2849
- voxcity/utils/weather.py +0 -1038
- voxcity-0.6.26.dist-info/RECORD +0 -38
- {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/WHEEL +0 -0
- {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Solar Irradiance Simulation Package
|
|
3
|
+
|
|
4
|
+
Public API exports for the refactored solar simulator. The implementation
|
|
5
|
+
is decomposed into focused stages:
|
|
6
|
+
1) kernels.py - Low-level kernels for visibility/irradiance
|
|
7
|
+
2) radiation.py - Physics: convert geometry to irradiance
|
|
8
|
+
3) temporal.py - Time-series integration and solar position
|
|
9
|
+
4) integration.py- High-level workflows and I/O
|
|
10
|
+
5) sky.py - Sky hemisphere discretization methods
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
# Stage 1: Kernels / Solar position
|
|
14
|
+
from .kernels import ( # noqa: F401
|
|
15
|
+
compute_direct_solar_irradiance_map_binary,
|
|
16
|
+
)
|
|
17
|
+
from .temporal import ( # noqa: F401
|
|
18
|
+
get_solar_positions_astral,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Sky discretization methods
|
|
22
|
+
from .sky import ( # noqa: F401
|
|
23
|
+
# Tregenza (145 patches)
|
|
24
|
+
generate_tregenza_patches,
|
|
25
|
+
get_tregenza_patch_index,
|
|
26
|
+
get_tregenza_patch_index_fast,
|
|
27
|
+
TREGENZA_BANDS,
|
|
28
|
+
TREGENZA_BAND_BOUNDARIES,
|
|
29
|
+
# Reinhart (subdivided Tregenza)
|
|
30
|
+
generate_reinhart_patches,
|
|
31
|
+
# Uniform grid
|
|
32
|
+
generate_uniform_grid_patches,
|
|
33
|
+
# Fibonacci spiral
|
|
34
|
+
generate_fibonacci_patches,
|
|
35
|
+
# Sun position binning
|
|
36
|
+
bin_sun_positions_to_patches,
|
|
37
|
+
bin_sun_positions_to_tregenza_fast,
|
|
38
|
+
# Utilities
|
|
39
|
+
get_patch_info,
|
|
40
|
+
visualize_sky_patches,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Stage 2: Radiation
|
|
44
|
+
from .radiation import ( # noqa: F401
|
|
45
|
+
get_direct_solar_irradiance_map,
|
|
46
|
+
get_diffuse_solar_irradiance_map,
|
|
47
|
+
get_global_solar_irradiance_map,
|
|
48
|
+
compute_solar_irradiance_for_all_faces,
|
|
49
|
+
get_building_solar_irradiance,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Stage 3: Temporal
|
|
53
|
+
from .temporal import ( # noqa: F401
|
|
54
|
+
get_cumulative_global_solar_irradiance,
|
|
55
|
+
get_cumulative_building_solar_irradiance,
|
|
56
|
+
_configure_num_threads,
|
|
57
|
+
_auto_time_batch_size,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Stage 4: Integration
|
|
61
|
+
from .integration import ( # noqa: F401
|
|
62
|
+
get_global_solar_irradiance_using_epw,
|
|
63
|
+
get_building_global_solar_irradiance_using_epw,
|
|
64
|
+
save_irradiance_mesh,
|
|
65
|
+
load_irradiance_mesh,
|
|
66
|
+
)
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Stage 4: High-level workflows & I/O.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
import pytz
|
|
7
|
+
|
|
8
|
+
from ...models import VoxCity
|
|
9
|
+
from ...utils.weather import (
|
|
10
|
+
get_nearest_epw_from_climate_onebuilding,
|
|
11
|
+
read_epw_for_solar_simulation,
|
|
12
|
+
)
|
|
13
|
+
from .radiation import get_global_solar_irradiance_map, get_building_solar_irradiance
|
|
14
|
+
from .temporal import get_cumulative_global_solar_irradiance, get_cumulative_building_solar_irradiance
|
|
15
|
+
from ..visibility import get_surface_view_factor
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_global_solar_irradiance_using_epw(
|
|
19
|
+
voxcity: VoxCity,
|
|
20
|
+
calc_type: str = "instantaneous",
|
|
21
|
+
direct_normal_irradiance_scaling: float = 1.0,
|
|
22
|
+
diffuse_irradiance_scaling: float = 1.0,
|
|
23
|
+
**kwargs,
|
|
24
|
+
):
|
|
25
|
+
"""
|
|
26
|
+
Compute global irradiance from EPW, either instantaneous or cumulative.
|
|
27
|
+
"""
|
|
28
|
+
# EPW acquisition
|
|
29
|
+
download_nearest_epw = kwargs.get("download_nearest_epw", False)
|
|
30
|
+
epw_file_path = kwargs.get("epw_file_path", None)
|
|
31
|
+
# Extract rectangle_vertices with fallback to voxcity.extras
|
|
32
|
+
rectangle_vertices = kwargs.get("rectangle_vertices", None)
|
|
33
|
+
if rectangle_vertices is None:
|
|
34
|
+
extras = getattr(voxcity, "extras", None)
|
|
35
|
+
if isinstance(extras, dict):
|
|
36
|
+
rectangle_vertices = extras.get("rectangle_vertices", None)
|
|
37
|
+
if download_nearest_epw:
|
|
38
|
+
if rectangle_vertices is None:
|
|
39
|
+
print("rectangle_vertices is required to download nearest EPW file")
|
|
40
|
+
return None
|
|
41
|
+
lons = [coord[0] for coord in rectangle_vertices]
|
|
42
|
+
lats = [coord[1] for coord in rectangle_vertices]
|
|
43
|
+
center_lon = (min(lons) + max(lons)) / 2
|
|
44
|
+
center_lat = (min(lats) + max(lats)) / 2
|
|
45
|
+
output_dir = kwargs.get("output_dir", "output")
|
|
46
|
+
max_distance = kwargs.get("max_distance", kwargs.get("max_distance_km", 100))
|
|
47
|
+
epw_file_path, weather_data, metadata = get_nearest_epw_from_climate_onebuilding(
|
|
48
|
+
longitude=center_lon,
|
|
49
|
+
latitude=center_lat,
|
|
50
|
+
output_dir=output_dir,
|
|
51
|
+
max_distance=max_distance,
|
|
52
|
+
extract_zip=True,
|
|
53
|
+
load_data=True,
|
|
54
|
+
allow_insecure_ssl=kwargs.get("allow_insecure_ssl", False),
|
|
55
|
+
allow_http_fallback=kwargs.get("allow_http_fallback", False),
|
|
56
|
+
ssl_verify=kwargs.get("ssl_verify", True),
|
|
57
|
+
)
|
|
58
|
+
if not download_nearest_epw and not epw_file_path:
|
|
59
|
+
raise ValueError("epw_file_path must be provided when download_nearest_epw is False")
|
|
60
|
+
|
|
61
|
+
# Read EPW
|
|
62
|
+
df, lon, lat, tz, elevation_m = read_epw_for_solar_simulation(epw_file_path)
|
|
63
|
+
if df.empty:
|
|
64
|
+
raise ValueError("No data in EPW file.")
|
|
65
|
+
|
|
66
|
+
if calc_type == "instantaneous":
|
|
67
|
+
calc_time = kwargs.get("calc_time", "01-01 12:00:00")
|
|
68
|
+
try:
|
|
69
|
+
calc_dt = datetime.strptime(calc_time, "%m-%d %H:%M:%S")
|
|
70
|
+
except ValueError as ve:
|
|
71
|
+
raise ValueError("calc_time must be in format 'MM-DD HH:MM:SS'") from ve
|
|
72
|
+
|
|
73
|
+
df_period = df[
|
|
74
|
+
(df.index.month == calc_dt.month)
|
|
75
|
+
& (df.index.day == calc_dt.day)
|
|
76
|
+
& (df.index.hour == calc_dt.hour)
|
|
77
|
+
]
|
|
78
|
+
if df_period.empty:
|
|
79
|
+
raise ValueError("No EPW data at the specified time.")
|
|
80
|
+
|
|
81
|
+
# Localize and convert to UTC
|
|
82
|
+
offset_minutes = int(tz * 60)
|
|
83
|
+
local_tz = pytz.FixedOffset(offset_minutes)
|
|
84
|
+
df_local = df_period.copy()
|
|
85
|
+
df_local.index = df_local.index.tz_localize(local_tz)
|
|
86
|
+
df_utc = df_local.tz_convert(pytz.UTC)
|
|
87
|
+
|
|
88
|
+
from .temporal import get_solar_positions_astral
|
|
89
|
+
|
|
90
|
+
solar_positions = get_solar_positions_astral(df_utc.index, lon, lat)
|
|
91
|
+
DNI = float(df_utc.iloc[0]["DNI"]) * direct_normal_irradiance_scaling
|
|
92
|
+
DHI = float(df_utc.iloc[0]["DHI"]) * diffuse_irradiance_scaling
|
|
93
|
+
azimuth_degrees = float(solar_positions.iloc[0]["azimuth"])
|
|
94
|
+
elevation_degrees = float(solar_positions.iloc[0]["elevation"])
|
|
95
|
+
|
|
96
|
+
solar_map = get_global_solar_irradiance_map(
|
|
97
|
+
voxcity,
|
|
98
|
+
azimuth_degrees,
|
|
99
|
+
elevation_degrees,
|
|
100
|
+
DNI,
|
|
101
|
+
DHI,
|
|
102
|
+
show_plot=True,
|
|
103
|
+
**kwargs,
|
|
104
|
+
)
|
|
105
|
+
return solar_map
|
|
106
|
+
|
|
107
|
+
if calc_type == "cumulative":
|
|
108
|
+
start_hour = kwargs.get("start_hour", 0)
|
|
109
|
+
end_hour = kwargs.get("end_hour", 23)
|
|
110
|
+
df_filtered = df[(df.index.hour >= start_hour) & (df.index.hour <= end_hour)]
|
|
111
|
+
solar_map = get_cumulative_global_solar_irradiance(
|
|
112
|
+
voxcity,
|
|
113
|
+
df_filtered,
|
|
114
|
+
lon,
|
|
115
|
+
lat,
|
|
116
|
+
tz,
|
|
117
|
+
direct_normal_irradiance_scaling=direct_normal_irradiance_scaling,
|
|
118
|
+
diffuse_irradiance_scaling=diffuse_irradiance_scaling,
|
|
119
|
+
**kwargs,
|
|
120
|
+
)
|
|
121
|
+
return solar_map
|
|
122
|
+
|
|
123
|
+
raise ValueError("calc_type must be 'instantaneous' or 'cumulative'")
|
|
124
|
+
def get_building_global_solar_irradiance_using_epw(*args, **kwargs):
|
|
125
|
+
"""
|
|
126
|
+
Compute building-surface irradiance using EPW (instantaneous or cumulative).
|
|
127
|
+
"""
|
|
128
|
+
voxcity: VoxCity = kwargs.get("voxcity")
|
|
129
|
+
if voxcity is None and len(args) > 0 and isinstance(args[0], VoxCity):
|
|
130
|
+
voxcity = args[0]
|
|
131
|
+
if voxcity is None:
|
|
132
|
+
raise ValueError("voxcity (VoxCity) must be provided as first arg or kwarg")
|
|
133
|
+
|
|
134
|
+
calc_type = kwargs.get("calc_type", "instantaneous")
|
|
135
|
+
direct_normal_irradiance_scaling = float(kwargs.get("direct_normal_irradiance_scaling", 1.0))
|
|
136
|
+
diffuse_irradiance_scaling = float(kwargs.get("diffuse_irradiance_scaling", 1.0))
|
|
137
|
+
building_svf_mesh = kwargs.get("building_svf_mesh", None)
|
|
138
|
+
building_id_grid = kwargs.get("building_id_grid", None)
|
|
139
|
+
progress_report = kwargs.get("progress_report", False)
|
|
140
|
+
fast_path = kwargs.get("fast_path", True)
|
|
141
|
+
|
|
142
|
+
# Thread configuration
|
|
143
|
+
from .temporal import _configure_num_threads
|
|
144
|
+
desired_threads = kwargs.get("numba_num_threads", None)
|
|
145
|
+
_configure_num_threads(desired_threads, progress=progress_report)
|
|
146
|
+
|
|
147
|
+
# EPW acquisition
|
|
148
|
+
download_nearest_epw = kwargs.get("download_nearest_epw", False)
|
|
149
|
+
epw_file_path = kwargs.get("epw_file_path", None)
|
|
150
|
+
# Extract rectangle_vertices with fallback to voxcity.extras
|
|
151
|
+
rectangle_vertices = kwargs.get("rectangle_vertices", None)
|
|
152
|
+
if rectangle_vertices is None:
|
|
153
|
+
extras = getattr(voxcity, "extras", None)
|
|
154
|
+
if isinstance(extras, dict):
|
|
155
|
+
rectangle_vertices = extras.get("rectangle_vertices", None)
|
|
156
|
+
if download_nearest_epw:
|
|
157
|
+
if rectangle_vertices is None:
|
|
158
|
+
print("rectangle_vertices is required to download nearest EPW file")
|
|
159
|
+
return None
|
|
160
|
+
lons = [coord[0] for coord in rectangle_vertices]
|
|
161
|
+
lats = [coord[1] for coord in rectangle_vertices]
|
|
162
|
+
center_lon = (min(lons) + max(lons)) / 2
|
|
163
|
+
center_lat = (min(lats) + max(lats)) / 2
|
|
164
|
+
output_dir = kwargs.get("output_dir", "output")
|
|
165
|
+
max_distance = kwargs.get("max_distance", kwargs.get("max_distance_km", 100))
|
|
166
|
+
epw_file_path, _weather_data, _metadata = get_nearest_epw_from_climate_onebuilding(
|
|
167
|
+
longitude=center_lon,
|
|
168
|
+
latitude=center_lat,
|
|
169
|
+
output_dir=output_dir,
|
|
170
|
+
max_distance=max_distance,
|
|
171
|
+
extract_zip=True,
|
|
172
|
+
load_data=True,
|
|
173
|
+
allow_insecure_ssl=kwargs.get("allow_insecure_ssl", False),
|
|
174
|
+
allow_http_fallback=kwargs.get("allow_http_fallback", False),
|
|
175
|
+
ssl_verify=kwargs.get("ssl_verify", True),
|
|
176
|
+
)
|
|
177
|
+
if not download_nearest_epw and not epw_file_path:
|
|
178
|
+
raise ValueError("epw_file_path must be provided when download_nearest_epw is False")
|
|
179
|
+
|
|
180
|
+
# Read EPW
|
|
181
|
+
df, lon, lat, tz, _elevation_m = read_epw_for_solar_simulation(epw_file_path)
|
|
182
|
+
if df.empty:
|
|
183
|
+
raise ValueError("No data in EPW file.")
|
|
184
|
+
|
|
185
|
+
# SVF for building faces (compute if not provided)
|
|
186
|
+
if building_svf_mesh is None:
|
|
187
|
+
if progress_report:
|
|
188
|
+
print("Processing Sky View Factor for building surfaces...")
|
|
189
|
+
svf_kwargs = {
|
|
190
|
+
'value_name': 'svf',
|
|
191
|
+
'target_values': (0,),
|
|
192
|
+
'inclusion_mode': False,
|
|
193
|
+
'building_id_grid': building_id_grid,
|
|
194
|
+
'progress_report': progress_report,
|
|
195
|
+
'fast_path': fast_path,
|
|
196
|
+
}
|
|
197
|
+
for k in ("N_azimuth", "N_elevation", "tree_k", "tree_lad", "debug"):
|
|
198
|
+
if k in kwargs:
|
|
199
|
+
svf_kwargs[k] = kwargs[k]
|
|
200
|
+
building_svf_mesh = get_surface_view_factor(voxcity, **svf_kwargs)
|
|
201
|
+
|
|
202
|
+
# Precompute geometry/masks
|
|
203
|
+
import numpy as _np
|
|
204
|
+
voxel_data = voxcity.voxels.classes
|
|
205
|
+
meshsize = voxcity.voxels.meta.meshsize
|
|
206
|
+
precomputed_geometry = {}
|
|
207
|
+
try:
|
|
208
|
+
grid_shape = voxel_data.shape
|
|
209
|
+
grid_bounds_voxel = _np.array([[0, 0, 0], [grid_shape[0], grid_shape[1], grid_shape[2]]], dtype=_np.float64)
|
|
210
|
+
grid_bounds_real = grid_bounds_voxel * meshsize
|
|
211
|
+
boundary_epsilon = meshsize * 0.05
|
|
212
|
+
precomputed_geometry = {
|
|
213
|
+
'face_centers': building_svf_mesh.triangles_center,
|
|
214
|
+
'face_normals': building_svf_mesh.face_normals,
|
|
215
|
+
'face_svf': building_svf_mesh.metadata['svf'] if ('svf' in building_svf_mesh.metadata) else None,
|
|
216
|
+
'grid_bounds_real': grid_bounds_real,
|
|
217
|
+
'boundary_epsilon': boundary_epsilon,
|
|
218
|
+
}
|
|
219
|
+
except Exception:
|
|
220
|
+
precomputed_geometry = {}
|
|
221
|
+
|
|
222
|
+
tree_k = kwargs.get("tree_k", 0.6)
|
|
223
|
+
tree_lad = kwargs.get("tree_lad", 1.0)
|
|
224
|
+
precomputed_masks = {
|
|
225
|
+
'vox_is_tree': (voxel_data == -2),
|
|
226
|
+
'vox_is_opaque': (voxel_data != 0) & (voxel_data != -2),
|
|
227
|
+
'att': float(_np.exp(-tree_k * tree_lad * meshsize)),
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if progress_report:
|
|
231
|
+
t_cnt = int(_np.count_nonzero(precomputed_masks['vox_is_tree']))
|
|
232
|
+
print(f"Precomputed caches: trees={t_cnt:,}, tree_att_per_voxel={precomputed_masks['att']:.4f}")
|
|
233
|
+
|
|
234
|
+
result_mesh = None
|
|
235
|
+
if calc_type == "instantaneous":
|
|
236
|
+
calc_time = kwargs.get("calc_time", "01-01 12:00:00")
|
|
237
|
+
try:
|
|
238
|
+
calc_dt = datetime.strptime(calc_time, "%m-%d %H:%M:%S")
|
|
239
|
+
except ValueError as ve:
|
|
240
|
+
raise ValueError("calc_time must be in format 'MM-DD HH:MM:SS'") from ve
|
|
241
|
+
|
|
242
|
+
df_period = df[
|
|
243
|
+
(df.index.month == calc_dt.month) & (df.index.day == calc_dt.day) & (df.index.hour == calc_dt.hour)
|
|
244
|
+
]
|
|
245
|
+
if df_period.empty:
|
|
246
|
+
raise ValueError("No EPW data at the specified time.")
|
|
247
|
+
|
|
248
|
+
offset_minutes = int(tz * 60)
|
|
249
|
+
local_tz = pytz.FixedOffset(offset_minutes)
|
|
250
|
+
df_local = df_period.copy()
|
|
251
|
+
df_local.index = df_local.index.tz_localize(local_tz)
|
|
252
|
+
df_utc = df_local.tz_convert(pytz.UTC)
|
|
253
|
+
|
|
254
|
+
from .temporal import get_solar_positions_astral
|
|
255
|
+
solar_positions = get_solar_positions_astral(df_utc.index, lon, lat)
|
|
256
|
+
DNI = float(df_utc.iloc[0]['DNI']) * direct_normal_irradiance_scaling
|
|
257
|
+
DHI = float(df_utc.iloc[0]['DHI']) * diffuse_irradiance_scaling
|
|
258
|
+
azimuth_degrees = float(solar_positions.iloc[0]['azimuth'])
|
|
259
|
+
elevation_degrees = float(solar_positions.iloc[0]['elevation'])
|
|
260
|
+
|
|
261
|
+
_call_kwargs = kwargs.copy()
|
|
262
|
+
_call_kwargs.update({
|
|
263
|
+
'progress_report': progress_report,
|
|
264
|
+
'fast_path': fast_path,
|
|
265
|
+
'precomputed_geometry': precomputed_geometry,
|
|
266
|
+
'precomputed_masks': precomputed_masks,
|
|
267
|
+
})
|
|
268
|
+
result_mesh = get_building_solar_irradiance(
|
|
269
|
+
voxcity,
|
|
270
|
+
building_svf_mesh,
|
|
271
|
+
azimuth_degrees,
|
|
272
|
+
elevation_degrees,
|
|
273
|
+
DNI,
|
|
274
|
+
DHI,
|
|
275
|
+
**_call_kwargs
|
|
276
|
+
)
|
|
277
|
+
elif calc_type == "cumulative":
|
|
278
|
+
period_start = kwargs.get("period_start", "01-01 00:00:00")
|
|
279
|
+
period_end = kwargs.get("period_end", "12-31 23:59:59")
|
|
280
|
+
time_step_hours = float(kwargs.get("time_step_hours", 1.0))
|
|
281
|
+
|
|
282
|
+
result_mesh = get_cumulative_building_solar_irradiance(
|
|
283
|
+
voxcity,
|
|
284
|
+
building_svf_mesh,
|
|
285
|
+
df,
|
|
286
|
+
lon,
|
|
287
|
+
lat,
|
|
288
|
+
tz,
|
|
289
|
+
period_start=period_start,
|
|
290
|
+
period_end=period_end,
|
|
291
|
+
time_step_hours=time_step_hours,
|
|
292
|
+
direct_normal_irradiance_scaling=direct_normal_irradiance_scaling,
|
|
293
|
+
diffuse_irradiance_scaling=diffuse_irradiance_scaling,
|
|
294
|
+
progress_report=progress_report,
|
|
295
|
+
fast_path=fast_path,
|
|
296
|
+
precomputed_geometry=precomputed_geometry,
|
|
297
|
+
precomputed_masks=precomputed_masks,
|
|
298
|
+
)
|
|
299
|
+
else:
|
|
300
|
+
raise ValueError("calc_type must be 'instantaneous' or 'cumulative'")
|
|
301
|
+
|
|
302
|
+
# Optional persist
|
|
303
|
+
if kwargs.get("save_mesh", False):
|
|
304
|
+
mesh_output_path = kwargs.get("mesh_output_path")
|
|
305
|
+
if not mesh_output_path:
|
|
306
|
+
output_directory = kwargs.get("output_directory", "output")
|
|
307
|
+
output_file_name = kwargs.get("output_file_name", f"{calc_type}_solar_irradiance")
|
|
308
|
+
mesh_output_path = f"{output_directory}/{output_file_name}.pkl"
|
|
309
|
+
save_irradiance_mesh(result_mesh, mesh_output_path)
|
|
310
|
+
if progress_report:
|
|
311
|
+
print(f"Saved irradiance mesh data to: {mesh_output_path}")
|
|
312
|
+
|
|
313
|
+
return result_mesh
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def save_irradiance_mesh(irradiance_mesh, output_file_path):
|
|
317
|
+
"""
|
|
318
|
+
Persist irradiance mesh to pickle file.
|
|
319
|
+
"""
|
|
320
|
+
import pickle
|
|
321
|
+
import os
|
|
322
|
+
os.makedirs(os.path.dirname(output_file_path), exist_ok=True)
|
|
323
|
+
with open(output_file_path, 'wb') as f:
|
|
324
|
+
pickle.dump(irradiance_mesh, f)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def load_irradiance_mesh(input_file_path):
|
|
328
|
+
"""
|
|
329
|
+
Load irradiance mesh from pickle file.
|
|
330
|
+
"""
|
|
331
|
+
import pickle
|
|
332
|
+
with open(input_file_path, 'rb') as f:
|
|
333
|
+
irradiance_mesh = pickle.load(f)
|
|
334
|
+
return irradiance_mesh
|
|
335
|
+
|
|
336
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Numba kernels and low-level computation utilities for solar simulation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from numba import njit, prange
|
|
6
|
+
import numpy as np
|
|
7
|
+
from ..common.raytracing import trace_ray_generic
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@njit(parallel=True)
|
|
11
|
+
def compute_direct_solar_irradiance_map_binary(
|
|
12
|
+
voxel_data,
|
|
13
|
+
sun_direction,
|
|
14
|
+
view_point_height,
|
|
15
|
+
hit_values,
|
|
16
|
+
meshsize,
|
|
17
|
+
tree_k,
|
|
18
|
+
tree_lad,
|
|
19
|
+
inclusion_mode,
|
|
20
|
+
):
|
|
21
|
+
"""
|
|
22
|
+
Return 2D transmittance map (0..1, NaN invalid) for direct beam along sun_direction.
|
|
23
|
+
"""
|
|
24
|
+
view_height_voxel = int(view_point_height / meshsize)
|
|
25
|
+
nx, ny, nz = voxel_data.shape
|
|
26
|
+
irradiance_map = np.full((nx, ny), np.nan, dtype=np.float64)
|
|
27
|
+
|
|
28
|
+
sd = np.array(sun_direction, dtype=np.float64)
|
|
29
|
+
sd_len = np.sqrt(sd[0] ** 2 + sd[1] ** 2 + sd[2] ** 2)
|
|
30
|
+
if sd_len == 0.0:
|
|
31
|
+
return np.flipud(irradiance_map)
|
|
32
|
+
sd /= sd_len
|
|
33
|
+
|
|
34
|
+
for x in prange(nx):
|
|
35
|
+
for y in range(ny):
|
|
36
|
+
found_observer = False
|
|
37
|
+
for z in range(1, nz):
|
|
38
|
+
if voxel_data[x, y, z] in (0, -2) and voxel_data[x, y, z - 1] not in (0, -2):
|
|
39
|
+
if (voxel_data[x, y, z - 1] in (7, 8, 9)) or (voxel_data[x, y, z - 1] < 0):
|
|
40
|
+
irradiance_map[x, y] = np.nan
|
|
41
|
+
found_observer = True
|
|
42
|
+
break
|
|
43
|
+
else:
|
|
44
|
+
observer_location = np.array([x, y, z + view_height_voxel], dtype=np.float64)
|
|
45
|
+
hit, transmittance = trace_ray_generic(
|
|
46
|
+
voxel_data,
|
|
47
|
+
observer_location,
|
|
48
|
+
sd,
|
|
49
|
+
hit_values,
|
|
50
|
+
meshsize,
|
|
51
|
+
tree_k,
|
|
52
|
+
tree_lad,
|
|
53
|
+
inclusion_mode,
|
|
54
|
+
)
|
|
55
|
+
irradiance_map[x, y] = transmittance if not hit else 0.0
|
|
56
|
+
found_observer = True
|
|
57
|
+
break
|
|
58
|
+
if not found_observer:
|
|
59
|
+
irradiance_map[x, y] = np.nan
|
|
60
|
+
return np.flipud(irradiance_map)
|
|
61
|
+
|
|
62
|
+
|