voxcity 0.7.0__py3-none-any.whl → 1.0.13__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.
Files changed (81) hide show
  1. voxcity/__init__.py +14 -14
  2. voxcity/downloader/ocean.py +559 -0
  3. voxcity/exporter/__init__.py +12 -12
  4. voxcity/exporter/cityles.py +633 -633
  5. voxcity/exporter/envimet.py +733 -728
  6. voxcity/exporter/magicavoxel.py +333 -333
  7. voxcity/exporter/netcdf.py +238 -238
  8. voxcity/exporter/obj.py +1480 -1480
  9. voxcity/generator/__init__.py +47 -44
  10. voxcity/generator/api.py +727 -675
  11. voxcity/generator/grids.py +394 -379
  12. voxcity/generator/io.py +94 -94
  13. voxcity/generator/pipeline.py +582 -282
  14. voxcity/generator/update.py +429 -0
  15. voxcity/generator/voxelizer.py +18 -6
  16. voxcity/geoprocessor/__init__.py +75 -75
  17. voxcity/geoprocessor/draw.py +1494 -1219
  18. voxcity/geoprocessor/merge_utils.py +91 -91
  19. voxcity/geoprocessor/mesh.py +806 -806
  20. voxcity/geoprocessor/network.py +708 -708
  21. voxcity/geoprocessor/raster/__init__.py +2 -0
  22. voxcity/geoprocessor/raster/buildings.py +435 -428
  23. voxcity/geoprocessor/raster/core.py +31 -0
  24. voxcity/geoprocessor/raster/export.py +93 -93
  25. voxcity/geoprocessor/raster/landcover.py +178 -51
  26. voxcity/geoprocessor/raster/raster.py +1 -1
  27. voxcity/geoprocessor/utils.py +824 -824
  28. voxcity/models.py +115 -113
  29. voxcity/simulator/solar/__init__.py +66 -43
  30. voxcity/simulator/solar/integration.py +336 -336
  31. voxcity/simulator/solar/sky.py +668 -0
  32. voxcity/simulator/solar/temporal.py +792 -434
  33. voxcity/simulator_gpu/__init__.py +115 -0
  34. voxcity/simulator_gpu/common/__init__.py +9 -0
  35. voxcity/simulator_gpu/common/geometry.py +11 -0
  36. voxcity/simulator_gpu/core.py +322 -0
  37. voxcity/simulator_gpu/domain.py +262 -0
  38. voxcity/simulator_gpu/environment.yml +11 -0
  39. voxcity/simulator_gpu/init_taichi.py +154 -0
  40. voxcity/simulator_gpu/integration.py +15 -0
  41. voxcity/simulator_gpu/kernels.py +56 -0
  42. voxcity/simulator_gpu/radiation.py +28 -0
  43. voxcity/simulator_gpu/raytracing.py +623 -0
  44. voxcity/simulator_gpu/sky.py +9 -0
  45. voxcity/simulator_gpu/solar/__init__.py +178 -0
  46. voxcity/simulator_gpu/solar/core.py +66 -0
  47. voxcity/simulator_gpu/solar/csf.py +1249 -0
  48. voxcity/simulator_gpu/solar/domain.py +561 -0
  49. voxcity/simulator_gpu/solar/epw.py +421 -0
  50. voxcity/simulator_gpu/solar/integration.py +2953 -0
  51. voxcity/simulator_gpu/solar/radiation.py +3019 -0
  52. voxcity/simulator_gpu/solar/raytracing.py +686 -0
  53. voxcity/simulator_gpu/solar/reflection.py +533 -0
  54. voxcity/simulator_gpu/solar/sky.py +907 -0
  55. voxcity/simulator_gpu/solar/solar.py +337 -0
  56. voxcity/simulator_gpu/solar/svf.py +446 -0
  57. voxcity/simulator_gpu/solar/volumetric.py +1151 -0
  58. voxcity/simulator_gpu/solar/voxcity.py +2953 -0
  59. voxcity/simulator_gpu/temporal.py +13 -0
  60. voxcity/simulator_gpu/utils.py +25 -0
  61. voxcity/simulator_gpu/view.py +32 -0
  62. voxcity/simulator_gpu/visibility/__init__.py +109 -0
  63. voxcity/simulator_gpu/visibility/geometry.py +278 -0
  64. voxcity/simulator_gpu/visibility/integration.py +808 -0
  65. voxcity/simulator_gpu/visibility/landmark.py +753 -0
  66. voxcity/simulator_gpu/visibility/view.py +944 -0
  67. voxcity/utils/__init__.py +11 -0
  68. voxcity/utils/classes.py +194 -0
  69. voxcity/utils/lc.py +80 -39
  70. voxcity/utils/shape.py +230 -0
  71. voxcity/visualizer/__init__.py +24 -24
  72. voxcity/visualizer/builder.py +43 -43
  73. voxcity/visualizer/grids.py +141 -141
  74. voxcity/visualizer/maps.py +187 -187
  75. voxcity/visualizer/renderer.py +1146 -928
  76. {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/METADATA +56 -52
  77. voxcity-1.0.13.dist-info/RECORD +116 -0
  78. voxcity-0.7.0.dist-info/RECORD +0 -77
  79. {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/WHEEL +0 -0
  80. {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/licenses/AUTHORS.rst +0 -0
  81. {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/licenses/LICENSE +0 -0
@@ -1,336 +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
-
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
+