voxcity 0.6.15__py3-none-any.whl → 0.7.0__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 (78) hide show
  1. voxcity/__init__.py +14 -8
  2. voxcity/downloader/__init__.py +2 -1
  3. voxcity/downloader/citygml.py +32 -18
  4. voxcity/downloader/gba.py +210 -0
  5. voxcity/downloader/gee.py +5 -1
  6. voxcity/downloader/mbfp.py +1 -1
  7. voxcity/downloader/oemj.py +80 -8
  8. voxcity/downloader/osm.py +23 -7
  9. voxcity/downloader/overture.py +26 -1
  10. voxcity/downloader/utils.py +73 -73
  11. voxcity/errors.py +30 -0
  12. voxcity/exporter/__init__.py +13 -4
  13. voxcity/exporter/cityles.py +633 -535
  14. voxcity/exporter/envimet.py +728 -708
  15. voxcity/exporter/magicavoxel.py +334 -297
  16. voxcity/exporter/netcdf.py +238 -0
  17. voxcity/exporter/obj.py +1481 -655
  18. voxcity/generator/__init__.py +44 -0
  19. voxcity/generator/api.py +675 -0
  20. voxcity/generator/grids.py +379 -0
  21. voxcity/generator/io.py +94 -0
  22. voxcity/generator/pipeline.py +282 -0
  23. voxcity/generator/voxelizer.py +380 -0
  24. voxcity/geoprocessor/__init__.py +75 -6
  25. voxcity/geoprocessor/conversion.py +153 -0
  26. voxcity/geoprocessor/draw.py +62 -12
  27. voxcity/geoprocessor/heights.py +199 -0
  28. voxcity/geoprocessor/io.py +101 -0
  29. voxcity/geoprocessor/merge_utils.py +91 -0
  30. voxcity/geoprocessor/mesh.py +806 -790
  31. voxcity/geoprocessor/network.py +708 -679
  32. voxcity/geoprocessor/overlap.py +84 -0
  33. voxcity/geoprocessor/raster/__init__.py +82 -0
  34. voxcity/geoprocessor/raster/buildings.py +428 -0
  35. voxcity/geoprocessor/raster/canopy.py +258 -0
  36. voxcity/geoprocessor/raster/core.py +150 -0
  37. voxcity/geoprocessor/raster/export.py +93 -0
  38. voxcity/geoprocessor/raster/landcover.py +156 -0
  39. voxcity/geoprocessor/raster/raster.py +110 -0
  40. voxcity/geoprocessor/selection.py +85 -0
  41. voxcity/geoprocessor/utils.py +18 -14
  42. voxcity/models.py +113 -0
  43. voxcity/simulator/common/__init__.py +22 -0
  44. voxcity/simulator/common/geometry.py +98 -0
  45. voxcity/simulator/common/raytracing.py +450 -0
  46. voxcity/simulator/solar/__init__.py +43 -0
  47. voxcity/simulator/solar/integration.py +336 -0
  48. voxcity/simulator/solar/kernels.py +62 -0
  49. voxcity/simulator/solar/radiation.py +648 -0
  50. voxcity/simulator/solar/temporal.py +434 -0
  51. voxcity/simulator/view.py +36 -2286
  52. voxcity/simulator/visibility/__init__.py +29 -0
  53. voxcity/simulator/visibility/landmark.py +392 -0
  54. voxcity/simulator/visibility/view.py +508 -0
  55. voxcity/utils/logging.py +61 -0
  56. voxcity/utils/orientation.py +51 -0
  57. voxcity/utils/weather/__init__.py +26 -0
  58. voxcity/utils/weather/epw.py +146 -0
  59. voxcity/utils/weather/files.py +36 -0
  60. voxcity/utils/weather/onebuilding.py +486 -0
  61. voxcity/visualizer/__init__.py +24 -0
  62. voxcity/visualizer/builder.py +43 -0
  63. voxcity/visualizer/grids.py +141 -0
  64. voxcity/visualizer/maps.py +187 -0
  65. voxcity/visualizer/palette.py +228 -0
  66. voxcity/visualizer/renderer.py +928 -0
  67. {voxcity-0.6.15.dist-info → voxcity-0.7.0.dist-info}/METADATA +113 -36
  68. voxcity-0.7.0.dist-info/RECORD +77 -0
  69. {voxcity-0.6.15.dist-info → voxcity-0.7.0.dist-info}/WHEEL +1 -1
  70. voxcity/generator.py +0 -1137
  71. voxcity/geoprocessor/grid.py +0 -1568
  72. voxcity/geoprocessor/polygon.py +0 -1344
  73. voxcity/simulator/solar.py +0 -2329
  74. voxcity/utils/visualization.py +0 -2660
  75. voxcity/utils/weather.py +0 -817
  76. voxcity-0.6.15.dist-info/RECORD +0 -37
  77. {voxcity-0.6.15.dist-info → voxcity-0.7.0.dist-info/licenses}/AUTHORS.rst +0 -0
  78. {voxcity-0.6.15.dist-info → voxcity-0.7.0.dist-info/licenses}/LICENSE +0 -0
@@ -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
+