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.
Files changed (81) hide show
  1. voxcity/__init__.py +10 -4
  2. voxcity/downloader/__init__.py +2 -1
  3. voxcity/downloader/gba.py +210 -0
  4. voxcity/downloader/gee.py +5 -1
  5. voxcity/downloader/mbfp.py +1 -1
  6. voxcity/downloader/oemj.py +80 -8
  7. voxcity/downloader/utils.py +73 -73
  8. voxcity/errors.py +30 -0
  9. voxcity/exporter/__init__.py +9 -1
  10. voxcity/exporter/cityles.py +129 -34
  11. voxcity/exporter/envimet.py +51 -26
  12. voxcity/exporter/magicavoxel.py +42 -5
  13. voxcity/exporter/netcdf.py +27 -0
  14. voxcity/exporter/obj.py +103 -28
  15. voxcity/generator/__init__.py +47 -0
  16. voxcity/generator/api.py +721 -0
  17. voxcity/generator/grids.py +381 -0
  18. voxcity/generator/io.py +94 -0
  19. voxcity/generator/pipeline.py +282 -0
  20. voxcity/generator/update.py +429 -0
  21. voxcity/generator/voxelizer.py +392 -0
  22. voxcity/geoprocessor/__init__.py +75 -6
  23. voxcity/geoprocessor/conversion.py +153 -0
  24. voxcity/geoprocessor/draw.py +1488 -1169
  25. voxcity/geoprocessor/heights.py +199 -0
  26. voxcity/geoprocessor/io.py +101 -0
  27. voxcity/geoprocessor/merge_utils.py +91 -0
  28. voxcity/geoprocessor/mesh.py +26 -10
  29. voxcity/geoprocessor/network.py +35 -6
  30. voxcity/geoprocessor/overlap.py +84 -0
  31. voxcity/geoprocessor/raster/__init__.py +82 -0
  32. voxcity/geoprocessor/raster/buildings.py +435 -0
  33. voxcity/geoprocessor/raster/canopy.py +258 -0
  34. voxcity/geoprocessor/raster/core.py +150 -0
  35. voxcity/geoprocessor/raster/export.py +93 -0
  36. voxcity/geoprocessor/raster/landcover.py +159 -0
  37. voxcity/geoprocessor/raster/raster.py +110 -0
  38. voxcity/geoprocessor/selection.py +85 -0
  39. voxcity/geoprocessor/utils.py +824 -820
  40. voxcity/models.py +113 -0
  41. voxcity/simulator/common/__init__.py +22 -0
  42. voxcity/simulator/common/geometry.py +98 -0
  43. voxcity/simulator/common/raytracing.py +450 -0
  44. voxcity/simulator/solar/__init__.py +66 -0
  45. voxcity/simulator/solar/integration.py +336 -0
  46. voxcity/simulator/solar/kernels.py +62 -0
  47. voxcity/simulator/solar/radiation.py +648 -0
  48. voxcity/simulator/solar/sky.py +668 -0
  49. voxcity/simulator/solar/temporal.py +792 -0
  50. voxcity/simulator/view.py +36 -2286
  51. voxcity/simulator/visibility/__init__.py +29 -0
  52. voxcity/simulator/visibility/landmark.py +392 -0
  53. voxcity/simulator/visibility/view.py +508 -0
  54. voxcity/utils/__init__.py +11 -0
  55. voxcity/utils/classes.py +194 -0
  56. voxcity/utils/lc.py +80 -39
  57. voxcity/utils/logging.py +61 -0
  58. voxcity/utils/orientation.py +51 -0
  59. voxcity/utils/shape.py +230 -0
  60. voxcity/utils/weather/__init__.py +26 -0
  61. voxcity/utils/weather/epw.py +146 -0
  62. voxcity/utils/weather/files.py +36 -0
  63. voxcity/utils/weather/onebuilding.py +486 -0
  64. voxcity/visualizer/__init__.py +24 -0
  65. voxcity/visualizer/builder.py +43 -0
  66. voxcity/visualizer/grids.py +141 -0
  67. voxcity/visualizer/maps.py +187 -0
  68. voxcity/visualizer/palette.py +228 -0
  69. voxcity/visualizer/renderer.py +1145 -0
  70. {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/METADATA +162 -48
  71. voxcity-1.0.2.dist-info/RECORD +81 -0
  72. voxcity/generator.py +0 -1302
  73. voxcity/geoprocessor/grid.py +0 -1739
  74. voxcity/geoprocessor/polygon.py +0 -1344
  75. voxcity/simulator/solar.py +0 -2339
  76. voxcity/utils/visualization.py +0 -2849
  77. voxcity/utils/weather.py +0 -1038
  78. voxcity-0.6.26.dist-info/RECORD +0 -38
  79. {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/WHEEL +0 -0
  80. {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/licenses/AUTHORS.rst +0 -0
  81. {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
+