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
voxcity/models.py ADDED
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Tuple, Optional, Dict, Any
5
+
6
+ import numpy as np
7
+
8
+
9
+ @dataclass
10
+ class GridMetadata:
11
+ crs: str
12
+ bounds: Tuple[float, float, float, float]
13
+ meshsize: float
14
+
15
+
16
+ @dataclass
17
+ class BuildingGrid:
18
+ heights: np.ndarray
19
+ min_heights: np.ndarray # object-dtype array of lists per cell
20
+ ids: np.ndarray
21
+ meta: GridMetadata
22
+
23
+
24
+ @dataclass
25
+ class LandCoverGrid:
26
+ classes: np.ndarray
27
+ meta: GridMetadata
28
+
29
+
30
+ @dataclass
31
+ class DemGrid:
32
+ elevation: np.ndarray
33
+ meta: GridMetadata
34
+
35
+
36
+ @dataclass
37
+ class VoxelGrid:
38
+ classes: np.ndarray
39
+ meta: GridMetadata
40
+
41
+
42
+ @dataclass
43
+ class CanopyGrid:
44
+ top: np.ndarray
45
+ meta: GridMetadata
46
+ bottom: Optional[np.ndarray] = None
47
+
48
+
49
+ @dataclass
50
+ class VoxCity:
51
+ voxels: VoxelGrid
52
+ buildings: BuildingGrid
53
+ land_cover: LandCoverGrid
54
+ dem: DemGrid
55
+ tree_canopy: CanopyGrid
56
+ extras: Dict[str, Any] = field(default_factory=dict)
57
+
58
+
59
+ @dataclass
60
+ class PipelineConfig:
61
+ rectangle_vertices: Any
62
+ meshsize: float
63
+ building_source: Optional[str] = None
64
+ land_cover_source: Optional[str] = None
65
+ canopy_height_source: Optional[str] = None
66
+ dem_source: Optional[str] = None
67
+ output_dir: str = "output"
68
+ trunk_height_ratio: Optional[float] = None
69
+ static_tree_height: Optional[float] = None
70
+ remove_perimeter_object: Optional[float] = None
71
+ mapvis: bool = False
72
+ gridvis: bool = True
73
+ # Structured options for strategies and I/O/visualization
74
+ land_cover_options: Dict[str, Any] = field(default_factory=dict)
75
+ building_options: Dict[str, Any] = field(default_factory=dict)
76
+ canopy_options: Dict[str, Any] = field(default_factory=dict)
77
+ dem_options: Dict[str, Any] = field(default_factory=dict)
78
+ io_options: Dict[str, Any] = field(default_factory=dict)
79
+ visualize_options: Dict[str, Any] = field(default_factory=dict)
80
+
81
+
82
+ # -----------------------------
83
+ # Mesh data structures
84
+ # -----------------------------
85
+
86
+ @dataclass
87
+ class MeshModel:
88
+ vertices: np.ndarray # (N, 3) float
89
+ faces: np.ndarray # (M, 3|4) int
90
+ colors: Optional[np.ndarray] = None # (M, 4) uint8 or None
91
+ name: Optional[str] = None
92
+
93
+
94
+ @dataclass
95
+ class MeshCollection:
96
+ """Container for named meshes with simple add/access helpers."""
97
+ meshes: Dict[str, MeshModel] = field(default_factory=dict)
98
+
99
+ def add(self, name: str, mesh: MeshModel) -> None:
100
+ self.meshes[name] = mesh
101
+
102
+ def get(self, name: str) -> Optional[MeshModel]:
103
+ return self.meshes.get(name)
104
+
105
+ def __iter__(self):
106
+ return iter(self.meshes.items())
107
+
108
+ # Compatibility: some renderers expect `collection.items.items()`
109
+ @property
110
+ def items(self) -> Dict[str, MeshModel]:
111
+ return self.meshes
112
+
113
+
@@ -0,0 +1,22 @@
1
+ """
2
+ Shared utilities for simulator subpackages.
3
+
4
+ Currently exposes lightweight 3D geometry helpers used by both
5
+ `visibility` and `solar`.
6
+ """
7
+
8
+ from .geometry import ( # noqa: F401
9
+ _generate_ray_directions_grid,
10
+ _generate_ray_directions_fibonacci,
11
+ rotate_vector_axis_angle,
12
+ _build_face_basis,
13
+ )
14
+
15
+ __all__ = [
16
+ "_generate_ray_directions_grid",
17
+ "_generate_ray_directions_fibonacci",
18
+ "rotate_vector_axis_angle",
19
+ "_build_face_basis",
20
+ ]
21
+
22
+
@@ -0,0 +1,98 @@
1
+ import numpy as np
2
+ from numba import njit
3
+
4
+
5
+ def _generate_ray_directions_grid(N_azimuth: int, N_elevation: int, elevation_min_degrees: float, elevation_max_degrees: float) -> np.ndarray:
6
+ azimuth_angles = np.linspace(0.0, 2.0 * np.pi, int(N_azimuth), endpoint=False)
7
+ elevation_angles = np.deg2rad(
8
+ np.linspace(float(elevation_min_degrees), float(elevation_max_degrees), int(N_elevation))
9
+ )
10
+ ray_directions = np.empty((len(azimuth_angles) * len(elevation_angles), 3), dtype=np.float64)
11
+ out_idx = 0
12
+ for elevation in elevation_angles:
13
+ cos_elev = np.cos(elevation)
14
+ sin_elev = np.sin(elevation)
15
+ for azimuth in azimuth_angles:
16
+ dx = cos_elev * np.cos(azimuth)
17
+ dy = cos_elev * np.sin(azimuth)
18
+ dz = sin_elev
19
+ ray_directions[out_idx, 0] = dx
20
+ ray_directions[out_idx, 1] = dy
21
+ ray_directions[out_idx, 2] = dz
22
+ out_idx += 1
23
+ return ray_directions
24
+
25
+
26
+ def _generate_ray_directions_fibonacci(N_rays: int, elevation_min_degrees: float, elevation_max_degrees: float) -> np.ndarray:
27
+ N = int(max(1, N_rays))
28
+ emin = np.deg2rad(float(elevation_min_degrees))
29
+ emax = np.deg2rad(float(elevation_max_degrees))
30
+ z_min = np.sin(min(emin, emax))
31
+ z_max = np.sin(max(emin, emax))
32
+ golden_angle = np.pi * (3.0 - np.sqrt(5.0))
33
+ i = np.arange(N, dtype=np.float64)
34
+ z = z_min + (i + 0.5) * (z_max - z_min) / N
35
+ phi = i * golden_angle
36
+ r = np.sqrt(np.clip(1.0 - z * z, 0.0, 1.0))
37
+ x = r * np.cos(phi)
38
+ y = r * np.sin(phi)
39
+ return np.stack((x, y, z), axis=1).astype(np.float64)
40
+
41
+
42
+ @njit
43
+ def rotate_vector_axis_angle(vec, axis, angle):
44
+ axis_len = np.sqrt(axis[0]**2 + axis[1]**2 + axis[2]**2)
45
+ if axis_len < 1e-12:
46
+ return vec
47
+ ux, uy, uz = axis / axis_len
48
+ c = np.cos(angle)
49
+ s = np.sin(angle)
50
+ dot = vec[0]*ux + vec[1]*uy + vec[2]*uz
51
+ cross_x = uy*vec[2] - uz*vec[1]
52
+ cross_y = uz*vec[0] - ux*vec[2]
53
+ cross_z = ux*vec[1] - uy*vec[0]
54
+ v_rot = np.zeros(3, dtype=np.float64)
55
+ v_rot[0] = vec[0] * c
56
+ v_rot[1] = vec[1] * c
57
+ v_rot[2] = vec[2] * c
58
+ v_rot[0] += cross_x * s
59
+ v_rot[1] += cross_y * s
60
+ v_rot[2] += cross_z * s
61
+ tmp = dot * (1.0 - c)
62
+ v_rot[0] += ux * tmp
63
+ v_rot[1] += uy * tmp
64
+ v_rot[2] += uz * tmp
65
+ return v_rot
66
+
67
+
68
+ @njit(cache=True, fastmath=True, nogil=True)
69
+ def _build_face_basis(normal):
70
+ nx = normal[0]; ny = normal[1]; nz = normal[2]
71
+ nrm = (nx*nx + ny*ny + nz*nz) ** 0.5
72
+ if nrm < 1e-12:
73
+ return (np.array((1.0, 0.0, 0.0)),
74
+ np.array((0.0, 1.0, 0.0)),
75
+ np.array((0.0, 0.0, 1.0)))
76
+ invn = 1.0 / nrm
77
+ nx *= invn; ny *= invn; nz *= invn
78
+ n = np.array((nx, ny, nz))
79
+ if abs(nz) < 0.999:
80
+ helper = np.array((0.0, 0.0, 1.0))
81
+ else:
82
+ helper = np.array((1.0, 0.0, 0.0))
83
+ ux = helper[1]*n[2] - helper[2]*n[1]
84
+ uy = helper[2]*n[0] - helper[0]*n[2]
85
+ uz = helper[0]*n[1] - helper[1]*n[0]
86
+ ul = (ux*ux + uy*uy + uz*uz) ** 0.5
87
+ if ul < 1e-12:
88
+ u = np.array((1.0, 0.0, 0.0))
89
+ else:
90
+ invul = 1.0 / ul
91
+ u = np.array((ux*invul, uy*invul, uz*invul))
92
+ vx = n[1]*u[2] - n[2]*u[1]
93
+ vy = n[2]*u[0] - n[0]*u[2]
94
+ vz = n[0]*u[1] - n[1]*u[0]
95
+ v = np.array((vx, vy, vz))
96
+ return u, v, n
97
+
98
+
@@ -0,0 +1,450 @@
1
+ import numpy as np
2
+ from numba import njit, prange
3
+
4
+
5
+ @njit
6
+ def calculate_transmittance(length, tree_k=0.6, tree_lad=1.0):
7
+ return np.exp(-tree_k * tree_lad * length)
8
+
9
+
10
+ @njit
11
+ def trace_ray_generic(voxel_data, origin, direction, hit_values, meshsize, tree_k, tree_lad, inclusion_mode=True):
12
+ nx, ny, nz = voxel_data.shape
13
+ x0, y0, z0 = origin
14
+ dx, dy, dz = direction
15
+
16
+ length = np.sqrt(dx*dx + dy*dy + dz*dz)
17
+ if length == 0.0:
18
+ return False, 1.0
19
+ dx /= length
20
+ dy /= length
21
+ dz /= length
22
+
23
+ x, y, z = x0 + 0.5, y0 + 0.5, z0 + 0.5
24
+ i, j, k = int(x0), int(y0), int(z0)
25
+
26
+ step_x = 1 if dx >= 0 else -1
27
+ step_y = 1 if dy >= 0 else -1
28
+ step_z = 1 if dz >= 0 else -1
29
+
30
+ EPSILON = 1e-10
31
+
32
+ if abs(dx) > EPSILON:
33
+ t_max_x = ((i + (step_x > 0)) - x) / dx
34
+ t_delta_x = abs(1 / dx)
35
+ else:
36
+ t_max_x = np.inf
37
+ t_delta_x = np.inf
38
+
39
+ if abs(dy) > EPSILON:
40
+ t_max_y = ((j + (step_y > 0)) - y) / dy
41
+ t_delta_y = abs(1 / dy)
42
+ else:
43
+ t_max_y = np.inf
44
+ t_delta_y = np.inf
45
+
46
+ if abs(dz) > EPSILON:
47
+ t_max_z = ((k + (step_z > 0)) - z) / dz
48
+ t_delta_z = abs(1 / dz)
49
+ else:
50
+ t_max_z = np.inf
51
+ t_delta_z = np.inf
52
+
53
+ cumulative_transmittance = 1.0
54
+ last_t = 0.0
55
+
56
+ while (0 <= i < nx) and (0 <= j < ny) and (0 <= k < nz):
57
+ voxel_value = voxel_data[i, j, k]
58
+
59
+ t_next = min(t_max_x, t_max_y, t_max_z)
60
+ segment_length = (t_next - last_t) * meshsize
61
+ if segment_length < 0.0:
62
+ segment_length = 0.0
63
+
64
+ if voxel_value == -2:
65
+ transmittance = calculate_transmittance(segment_length, tree_k, tree_lad)
66
+ cumulative_transmittance *= transmittance
67
+ if cumulative_transmittance < 0.01:
68
+ if inclusion_mode:
69
+ return False, cumulative_transmittance
70
+ else:
71
+ return True, cumulative_transmittance
72
+
73
+ if inclusion_mode:
74
+ for hv in hit_values:
75
+ if voxel_value == hv:
76
+ return True, cumulative_transmittance
77
+ if voxel_value != 0 and voxel_value != -2:
78
+ return False, cumulative_transmittance
79
+ else:
80
+ in_set = False
81
+ for hv in hit_values:
82
+ if voxel_value == hv:
83
+ in_set = True
84
+ break
85
+ if not in_set and voxel_value != -2:
86
+ return True, cumulative_transmittance
87
+
88
+ last_t = t_next
89
+
90
+ TIE_EPS = 1e-12
91
+ eq_x = abs(t_max_x - t_next) <= TIE_EPS
92
+ eq_y = abs(t_max_y - t_next) <= TIE_EPS
93
+ eq_z = abs(t_max_z - t_next) <= TIE_EPS
94
+
95
+ if inclusion_mode and ((eq_x and eq_y) or (eq_x and eq_z) or (eq_y and eq_z)):
96
+ if eq_x:
97
+ ii = i + step_x
98
+ if 0 <= ii < nx:
99
+ val = voxel_data[ii, j, k]
100
+ is_target = False
101
+ for hv in hit_values:
102
+ if val == hv:
103
+ is_target = True
104
+ break
105
+ if (val != 0) and (val != -2) and (not is_target):
106
+ return False, cumulative_transmittance
107
+ if eq_y:
108
+ jj = j + step_y
109
+ if 0 <= jj < ny:
110
+ val = voxel_data[i, jj, k]
111
+ is_target = False
112
+ for hv in hit_values:
113
+ if val == hv:
114
+ is_target = True
115
+ break
116
+ if (val != 0) and (val != -2) and (not is_target):
117
+ return False, cumulative_transmittance
118
+ if eq_z:
119
+ kk = k + step_z
120
+ if 0 <= kk < nz:
121
+ val = voxel_data[i, j, kk]
122
+ is_target = False
123
+ for hv in hit_values:
124
+ if val == hv:
125
+ is_target = True
126
+ break
127
+ if (val != 0) and (val != -2) and (not is_target):
128
+ return False, cumulative_transmittance
129
+
130
+ stepped = False
131
+ if eq_x:
132
+ t_max_x += t_delta_x
133
+ i += step_x
134
+ stepped = True
135
+ if eq_y:
136
+ t_max_y += t_delta_y
137
+ j += step_y
138
+ stepped = True
139
+ if eq_z:
140
+ t_max_z += t_delta_z
141
+ k += step_z
142
+ stepped = True
143
+
144
+ if not stepped:
145
+ if t_max_x < t_max_y:
146
+ if t_max_x < t_max_z:
147
+ t_max_x += t_delta_x; i += step_x
148
+ else:
149
+ t_max_z += t_delta_z; k += step_z
150
+ else:
151
+ if t_max_y < t_max_z:
152
+ t_max_y += t_delta_y; j += step_y
153
+ else:
154
+ t_max_z += t_delta_z; k += step_z
155
+
156
+ return False, cumulative_transmittance
157
+
158
+
159
+ @njit
160
+ def compute_vi_generic(observer_location, voxel_data, ray_directions, hit_values, meshsize, tree_k, tree_lad, inclusion_mode=True):
161
+ total_rays = ray_directions.shape[0]
162
+ visibility_sum = 0.0
163
+ for idx in range(total_rays):
164
+ direction = ray_directions[idx]
165
+ hit, value = trace_ray_generic(voxel_data, observer_location, direction, hit_values, meshsize, tree_k, tree_lad, inclusion_mode)
166
+ if inclusion_mode:
167
+ if hit:
168
+ if -2 in hit_values:
169
+ contrib = 1.0 - max(0.0, min(1.0, value))
170
+ visibility_sum += contrib
171
+ else:
172
+ visibility_sum += 1.0
173
+ else:
174
+ if not hit:
175
+ visibility_sum += value
176
+ return visibility_sum / total_rays
177
+
178
+
179
+ @njit(parallel=True)
180
+ def compute_vi_map_generic(voxel_data, ray_directions, view_height_voxel, hit_values, meshsize, tree_k, tree_lad, inclusion_mode=True):
181
+ nx, ny, nz = voxel_data.shape
182
+ vi_map = np.full((nx, ny), np.nan)
183
+ for x in prange(nx):
184
+ for y in range(ny):
185
+ found_observer = False
186
+ for z in range(1, nz):
187
+ if voxel_data[x, y, z] in (0, -2) and voxel_data[x, y, z - 1] not in (0, -2):
188
+ if (voxel_data[x, y, z - 1] in (7, 8, 9)) or (voxel_data[x, y, z - 1] < 0):
189
+ vi_map[x, y] = np.nan
190
+ found_observer = True
191
+ break
192
+ else:
193
+ observer_location = np.array([x, y, z + view_height_voxel], dtype=np.float64)
194
+ vi_value = compute_vi_generic(observer_location, voxel_data, ray_directions, hit_values, meshsize, tree_k, tree_lad, inclusion_mode)
195
+ vi_map[x, y] = vi_value
196
+ found_observer = True
197
+ break
198
+ if not found_observer:
199
+ vi_map[x, y] = np.nan
200
+ return np.flipud(vi_map)
201
+
202
+
203
+ def _prepare_masks_for_vi(voxel_data: np.ndarray, hit_values, inclusion_mode: bool):
204
+ is_tree = (voxel_data == -2)
205
+ if inclusion_mode:
206
+ is_target = np.isin(voxel_data, hit_values)
207
+ is_blocker_inc = (voxel_data != 0) & (~is_tree) & (~is_target)
208
+ return is_tree, is_target, None, is_blocker_inc
209
+ else:
210
+ is_allowed = np.isin(voxel_data, hit_values)
211
+ return is_tree, None, is_allowed, None
212
+
213
+
214
+ @njit(cache=True, fastmath=True)
215
+ def _trace_ray_inclusion_masks(is_tree, is_target, is_blocker_inc, origin, direction, meshsize, tree_k, tree_lad):
216
+ nx, ny, nz = is_tree.shape
217
+ x0, y0, z0 = origin
218
+ dx, dy, dz = direction
219
+ length = (dx*dx + dy*dy + dz*dz) ** 0.5
220
+ if length == 0.0:
221
+ return False, 1.0
222
+ dx /= length; dy /= length; dz /= length
223
+ x, y, z = x0 + 0.5, y0 + 0.5, z0 + 0.5
224
+ i, j, k = int(x0), int(y0), int(z0)
225
+ step_x = 1 if dx >= 0 else -1
226
+ step_y = 1 if dy >= 0 else -1
227
+ step_z = 1 if dz >= 0 else -1
228
+ EPS = 1e-10
229
+ if abs(dx) > EPS:
230
+ t_max_x = ((i + (step_x > 0)) - x) / dx
231
+ t_delta_x = abs(1.0 / dx)
232
+ else:
233
+ t_max_x = np.inf; t_delta_x = np.inf
234
+ if abs(dy) > EPS:
235
+ t_max_y = ((j + (step_y > 0)) - y) / dy
236
+ t_delta_y = abs(1.0 / dy)
237
+ else:
238
+ t_max_y = np.inf; t_delta_y = np.inf
239
+ if abs(dz) > EPS:
240
+ t_max_z = ((k + (step_z > 0)) - z) / dz
241
+ t_delta_z = abs(1.0 / dz)
242
+ else:
243
+ t_max_z = np.inf; t_delta_z = np.inf
244
+ cumulative_transmittance = 1.0
245
+ last_t = 0.0
246
+ while (0 <= i < nx) and (0 <= j < ny) and (0 <= k < nz):
247
+ t_next = t_max_x
248
+ axis = 0
249
+ if t_max_y < t_next:
250
+ t_next = t_max_y; axis = 1
251
+ if t_max_z < t_next:
252
+ t_next = t_max_z; axis = 2
253
+ segment_length = (t_next - last_t) * meshsize
254
+ if segment_length < 0.0:
255
+ segment_length = 0.0
256
+ if is_tree[i, j, k]:
257
+ trans = np.exp(-tree_k * tree_lad * segment_length)
258
+ cumulative_transmittance *= trans
259
+ if cumulative_transmittance < 1e-2:
260
+ return False, cumulative_transmittance
261
+ if is_target[i, j, k]:
262
+ return True, cumulative_transmittance
263
+ if is_blocker_inc[i, j, k]:
264
+ return False, cumulative_transmittance
265
+ last_t = t_next
266
+ if axis == 0:
267
+ t_max_x += t_delta_x; i += step_x
268
+ elif axis == 1:
269
+ t_max_y += t_delta_y; j += step_y
270
+ else:
271
+ t_max_z += t_delta_z; k += step_z
272
+ return False, cumulative_transmittance
273
+
274
+
275
+ @njit(cache=True, fastmath=True)
276
+ def _trace_ray_exclusion_masks(is_tree, is_allowed, origin, direction, meshsize, tree_k, tree_lad):
277
+ nx, ny, nz = is_tree.shape
278
+ x0, y0, z0 = origin
279
+ dx, dy, dz = direction
280
+ length = (dx*dx + dy*dy + dz*dz) ** 0.5
281
+ if length == 0.0:
282
+ return False, 1.0
283
+ dx /= length; dy /= length; dz /= length
284
+ x, y, z = x0 + 0.5, y0 + 0.5, z0 + 0.5
285
+ i, j, k = int(x0), int(y0), int(z0)
286
+ step_x = 1 if dx >= 0 else -1
287
+ step_y = 1 if dy >= 0 else -1
288
+ step_z = 1 if dz >= 0 else -1
289
+ EPS = 1e-10
290
+ if abs(dx) > EPS:
291
+ t_max_x = ((i + (step_x > 0)) - x) / dx
292
+ t_delta_x = abs(1.0 / dx)
293
+ else:
294
+ t_max_x = np.inf; t_delta_x = np.inf
295
+ if abs(dy) > EPS:
296
+ t_max_y = ((j + (step_y > 0)) - y) / dy
297
+ t_delta_y = abs(1.0 / dy)
298
+ else:
299
+ t_max_y = np.inf; t_delta_y = np.inf
300
+ if abs(dz) > EPS:
301
+ t_max_z = ((k + (step_z > 0)) - z) / dz
302
+ t_delta_z = abs(1.0 / dz)
303
+ else:
304
+ t_max_z = np.inf; t_delta_z = np.inf
305
+ cumulative_transmittance = 1.0
306
+ last_t = 0.0
307
+ while (0 <= i < nx) and (0 <= j < ny) and (0 <= k < nz):
308
+ t_next = t_max_x
309
+ axis = 0
310
+ if t_max_y < t_next:
311
+ t_next = t_max_y; axis = 1
312
+ if t_max_z < t_next:
313
+ t_next = t_max_z; axis = 2
314
+ segment_length = (t_next - last_t) * meshsize
315
+ if segment_length < 0.0:
316
+ segment_length = 0.0
317
+ if is_tree[i, j, k]:
318
+ trans = np.exp(-tree_k * tree_lad * segment_length)
319
+ cumulative_transmittance *= trans
320
+ if cumulative_transmittance < 1e-2:
321
+ return True, cumulative_transmittance
322
+ if (not is_allowed[i, j, k]) and (not is_tree[i, j, k]):
323
+ return True, cumulative_transmittance
324
+ last_t = t_next
325
+ if axis == 0:
326
+ t_max_x += t_delta_x; i += step_x
327
+ elif axis == 1:
328
+ t_max_y += t_delta_y; j += step_y
329
+ else:
330
+ t_max_z += t_delta_z; k += step_z
331
+ return False, cumulative_transmittance
332
+
333
+
334
+ @njit(parallel=True, cache=True, fastmath=True)
335
+ def _compute_vi_map_generic_fast(voxel_data, ray_directions, view_height_voxel, meshsize, tree_k, tree_lad, is_tree, is_target, is_allowed, is_blocker_inc, inclusion_mode, trees_in_targets):
336
+ nx, ny, nz = voxel_data.shape
337
+ vi_map = np.full((nx, ny), np.nan)
338
+ obs_base_z = _precompute_observer_base_z(voxel_data)
339
+ for x in prange(nx):
340
+ for y in range(ny):
341
+ base_z = obs_base_z[x, y]
342
+ if base_z < 0:
343
+ vi_map[x, y] = np.nan
344
+ continue
345
+ below = voxel_data[x, y, base_z]
346
+ if (below == 7) or (below == 8) or (below == 9) or (below < 0):
347
+ vi_map[x, y] = np.nan
348
+ continue
349
+ oz = base_z + 1 + view_height_voxel
350
+ obs = np.array([x, y, oz], dtype=np.float64)
351
+ visibility_sum = 0.0
352
+ n_rays = ray_directions.shape[0]
353
+ for r in range(n_rays):
354
+ direction = ray_directions[r]
355
+ if inclusion_mode:
356
+ hit, value = _trace_ray_inclusion_masks(is_tree, is_target, is_blocker_inc, obs, direction, meshsize, tree_k, tree_lad)
357
+ if hit:
358
+ if trees_in_targets:
359
+ contrib = 1.0 - max(0.0, min(1.0, value))
360
+ visibility_sum += contrib
361
+ else:
362
+ visibility_sum += 1.0
363
+ else:
364
+ hit, value = _trace_ray_exclusion_masks(is_tree, is_allowed, obs, direction, meshsize, tree_k, tree_lad)
365
+ if not hit:
366
+ visibility_sum += value
367
+ vi_map[x, y] = visibility_sum / n_rays
368
+ return np.flipud(vi_map)
369
+
370
+
371
+ @njit(cache=True, fastmath=True)
372
+ def _precompute_observer_base_z(voxel_data):
373
+ nx, ny, nz = voxel_data.shape
374
+ out = np.empty((nx, ny), dtype=np.int32)
375
+ for x in range(nx):
376
+ for y in range(ny):
377
+ found = False
378
+ for z in range(1, nz):
379
+ v_above = voxel_data[x, y, z]
380
+ v_base = voxel_data[x, y, z - 1]
381
+ if (v_above == 0 or v_above == -2) and not (v_base == 0 or v_base == -2):
382
+ out[x, y] = z - 1
383
+ found = True
384
+ break
385
+ if not found:
386
+ out[x, y] = -1
387
+ return out
388
+
389
+
390
+ @njit(cache=True, fastmath=True, nogil=True)
391
+ def _trace_ray(vox_is_tree, vox_is_opaque, origin, target, att, att_cutoff):
392
+ nx, ny, nz = vox_is_opaque.shape
393
+ x0, y0, z0 = origin[0], origin[1], origin[2]
394
+ x1, y1, z1 = target[0], target[1], target[2]
395
+ dx = x1 - x0
396
+ dy = y1 - y0
397
+ dz = z1 - z0
398
+ length = (dx*dx + dy*dy + dz*dz) ** 0.5
399
+ if length == 0.0:
400
+ return True
401
+ inv_len = 1.0 / length
402
+ dx *= inv_len; dy *= inv_len; dz *= inv_len
403
+ x = x0 + 0.5
404
+ y = y0 + 0.5
405
+ z = z0 + 0.5
406
+ i = int(x0); j = int(y0); k = int(z0)
407
+ step_x = 1 if dx >= 0.0 else -1
408
+ step_y = 1 if dy >= 0.0 else -1
409
+ step_z = 1 if dz >= 0.0 else -1
410
+ BIG = 1e30
411
+ if dx != 0.0:
412
+ t_max_x = (((i + (1 if step_x > 0 else 0)) - x) / dx)
413
+ t_delta_x = abs(1.0 / dx)
414
+ else:
415
+ t_max_x = BIG; t_delta_x = BIG
416
+ if dy != 0.0:
417
+ t_max_y = (((j + (1 if step_y > 0 else 0)) - y) / dy)
418
+ t_delta_y = abs(1.0 / dy)
419
+ else:
420
+ t_max_y = BIG; t_delta_y = BIG
421
+ if dz != 0.0:
422
+ t_max_z = (((k + (1 if step_z > 0 else 0)) - z) / dz)
423
+ t_delta_z = abs(1.0 / dz)
424
+ else:
425
+ t_max_z = BIG; t_delta_z = BIG
426
+ T = 1.0
427
+ ti = int(x1); tj = int(y1); tk = int(z1)
428
+ while True:
429
+ if (i < 0) or (i >= nx) or (j < 0) or (j >= ny) or (k < 0) or (k >= nz):
430
+ return False
431
+ if vox_is_opaque[i, j, k]:
432
+ return False
433
+ if vox_is_tree[i, j, k]:
434
+ T *= att
435
+ if T < att_cutoff:
436
+ return False
437
+ if (i == ti) and (j == tj) and (k == tk):
438
+ return True
439
+ if t_max_x < t_max_y:
440
+ if t_max_x < t_max_z:
441
+ t_max_x += t_delta_x; i += step_x
442
+ else:
443
+ t_max_z += t_delta_z; k += step_z
444
+ else:
445
+ if t_max_y < t_max_z:
446
+ t_max_y += t_delta_y; j += step_y
447
+ else:
448
+ t_max_z += t_delta_z; k += step_z
449
+
450
+