voxcity 0.3.0__py3-none-any.whl → 0.3.1__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.
Potentially problematic release.
This version of voxcity might be problematic. Click here for more details.
- voxcity/sim/__init_.py +1 -0
- voxcity/sim/solar.py +335 -72
- voxcity/sim/utils.py +6 -0
- voxcity/sim/view.py +114 -108
- voxcity/utils/__init_.py +2 -1
- voxcity/utils/weather.py +523 -0
- voxcity/voxcity.py +4 -1
- {voxcity-0.3.0.dist-info → voxcity-0.3.1.dist-info}/METADATA +2 -1
- {voxcity-0.3.0.dist-info → voxcity-0.3.1.dist-info}/RECORD +13 -11
- {voxcity-0.3.0.dist-info → voxcity-0.3.1.dist-info}/AUTHORS.rst +0 -0
- {voxcity-0.3.0.dist-info → voxcity-0.3.1.dist-info}/LICENSE +0 -0
- {voxcity-0.3.0.dist-info → voxcity-0.3.1.dist-info}/WHEEL +0 -0
- {voxcity-0.3.0.dist-info → voxcity-0.3.1.dist-info}/top_level.txt +0 -0
voxcity/sim/view.py
CHANGED
|
@@ -22,20 +22,26 @@ from ..file.geojson import find_building_containing_point
|
|
|
22
22
|
from ..file.obj import grid_to_obj, export_obj
|
|
23
23
|
|
|
24
24
|
@njit
|
|
25
|
-
def
|
|
26
|
-
"""
|
|
25
|
+
def calculate_transmittance(length, tree_k=0.6, tree_lad=1.0):
|
|
26
|
+
"""Calculate tree transmittance using the Beer-Lambert law.
|
|
27
27
|
|
|
28
|
-
Uses an optimized DDA (Digital Differential Analyzer) algorithm for ray traversal.
|
|
29
|
-
|
|
30
28
|
Args:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
inclusion_mode (bool): If True, hit when value in hit_values. If False, hit when not in hit_values.
|
|
36
|
-
|
|
29
|
+
length (float): Path length through tree voxel in meters
|
|
30
|
+
tree_k (float): Static extinction coefficient (default: 0.5)
|
|
31
|
+
tree_lad (float): Leaf area density in m^-1 (default: 1.0)
|
|
32
|
+
|
|
37
33
|
Returns:
|
|
38
|
-
|
|
34
|
+
float: Transmittance value between 0 and 1
|
|
35
|
+
"""
|
|
36
|
+
return np.exp(-tree_k * tree_lad * length)
|
|
37
|
+
|
|
38
|
+
@njit
|
|
39
|
+
def trace_ray_generic(voxel_data, origin, direction, hit_values, meshsize, tree_k, tree_lad, inclusion_mode=True):
|
|
40
|
+
"""Trace a ray through a voxel grid and check for hits with specified values.
|
|
41
|
+
|
|
42
|
+
For tree voxels (-2):
|
|
43
|
+
- If -2 in hit_values: counts obstruction (1 - transmittance) as hit contribution
|
|
44
|
+
- If -2 not in hit_values: applies transmittance normally
|
|
39
45
|
"""
|
|
40
46
|
nx, ny, nz = voxel_data.shape
|
|
41
47
|
x0, y0, z0 = origin
|
|
@@ -44,62 +50,86 @@ def trace_ray_generic(voxel_data, origin, direction, hit_values, inclusion_mode=
|
|
|
44
50
|
# Normalize direction vector
|
|
45
51
|
length = np.sqrt(dx*dx + dy*dy + dz*dz)
|
|
46
52
|
if length == 0.0:
|
|
47
|
-
return False
|
|
53
|
+
return False, 1.0
|
|
48
54
|
dx /= length
|
|
49
55
|
dy /= length
|
|
50
56
|
dz /= length
|
|
51
57
|
|
|
52
|
-
# Initialize ray position
|
|
58
|
+
# Initialize ray position
|
|
53
59
|
x, y, z = x0 + 0.5, y0 + 0.5, z0 + 0.5
|
|
54
60
|
i, j, k = int(x0), int(y0), int(z0)
|
|
55
61
|
|
|
56
|
-
#
|
|
62
|
+
# Calculate step directions and initial distances
|
|
57
63
|
step_x = 1 if dx >= 0 else -1
|
|
58
64
|
step_y = 1 if dy >= 0 else -1
|
|
59
65
|
step_z = 1 if dz >= 0 else -1
|
|
60
66
|
|
|
61
|
-
# Calculate
|
|
62
|
-
|
|
67
|
+
# Calculate DDA parameters with safety checks
|
|
68
|
+
EPSILON = 1e-10 # Small value to prevent division by zero
|
|
69
|
+
|
|
70
|
+
if abs(dx) > EPSILON:
|
|
63
71
|
t_max_x = ((i + (step_x > 0)) - x) / dx
|
|
64
72
|
t_delta_x = abs(1 / dx)
|
|
65
73
|
else:
|
|
66
74
|
t_max_x = np.inf
|
|
67
75
|
t_delta_x = np.inf
|
|
68
76
|
|
|
69
|
-
if dy
|
|
77
|
+
if abs(dy) > EPSILON:
|
|
70
78
|
t_max_y = ((j + (step_y > 0)) - y) / dy
|
|
71
79
|
t_delta_y = abs(1 / dy)
|
|
72
80
|
else:
|
|
73
81
|
t_max_y = np.inf
|
|
74
82
|
t_delta_y = np.inf
|
|
75
83
|
|
|
76
|
-
if dz
|
|
84
|
+
if abs(dz) > EPSILON:
|
|
77
85
|
t_max_z = ((k + (step_z > 0)) - z) / dz
|
|
78
86
|
t_delta_z = abs(1 / dz)
|
|
79
87
|
else:
|
|
80
88
|
t_max_z = np.inf
|
|
81
89
|
t_delta_z = np.inf
|
|
82
90
|
|
|
91
|
+
# Track cumulative values
|
|
92
|
+
cumulative_transmittance = 1.0
|
|
93
|
+
cumulative_hit_contribution = 0.0
|
|
94
|
+
last_t = 0.0
|
|
95
|
+
|
|
83
96
|
# Main ray traversal loop
|
|
84
97
|
while (0 <= i < nx) and (0 <= j < ny) and (0 <= k < nz):
|
|
85
98
|
voxel_value = voxel_data[i, j, k]
|
|
99
|
+
|
|
100
|
+
# Find next intersection
|
|
101
|
+
t_next = min(t_max_x, t_max_y, t_max_z)
|
|
102
|
+
|
|
103
|
+
# Calculate segment length in current voxel
|
|
104
|
+
segment_length = (t_next - last_t) * meshsize
|
|
105
|
+
|
|
106
|
+
# Handle tree voxels (value -2)
|
|
107
|
+
if voxel_value == -2:
|
|
108
|
+
transmittance = calculate_transmittance(segment_length, tree_k, tree_lad)
|
|
109
|
+
cumulative_transmittance *= transmittance
|
|
110
|
+
|
|
111
|
+
# If transmittance becomes too low, consider it a hit
|
|
112
|
+
if cumulative_transmittance < 0.01:
|
|
113
|
+
return True, cumulative_transmittance
|
|
86
114
|
|
|
115
|
+
# Check for hits with other objects
|
|
87
116
|
if inclusion_mode:
|
|
88
|
-
# Inclusion mode: hit if voxel_value in hit_values
|
|
89
117
|
for hv in hit_values:
|
|
90
118
|
if voxel_value == hv:
|
|
91
|
-
return True
|
|
119
|
+
return True, cumulative_transmittance
|
|
92
120
|
else:
|
|
93
|
-
# Exclusion mode: hit if voxel_value not in hit_values
|
|
94
121
|
in_set = False
|
|
95
122
|
for hv in hit_values:
|
|
96
123
|
if voxel_value == hv:
|
|
97
124
|
in_set = True
|
|
98
125
|
break
|
|
99
|
-
if not in_set:
|
|
100
|
-
return True
|
|
126
|
+
if not in_set and voxel_value != -2: # Exclude trees from regular hits
|
|
127
|
+
return True, cumulative_transmittance
|
|
101
128
|
|
|
102
|
-
#
|
|
129
|
+
# Update for next iteration
|
|
130
|
+
last_t = t_next
|
|
131
|
+
|
|
132
|
+
# Move to next voxel
|
|
103
133
|
if t_max_x < t_max_y:
|
|
104
134
|
if t_max_x < t_max_z:
|
|
105
135
|
t_max_x += t_delta_x
|
|
@@ -115,75 +145,58 @@ def trace_ray_generic(voxel_data, origin, direction, hit_values, inclusion_mode=
|
|
|
115
145
|
t_max_z += t_delta_z
|
|
116
146
|
k += step_z
|
|
117
147
|
|
|
118
|
-
|
|
119
|
-
return False
|
|
148
|
+
return False, cumulative_transmittance
|
|
120
149
|
|
|
121
150
|
@njit
|
|
122
|
-
def compute_vi_generic(observer_location, voxel_data, ray_directions, hit_values, inclusion_mode=True):
|
|
123
|
-
"""Compute view index for
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
ray_directions (ndarray): Array of direction vectors for rays
|
|
129
|
-
hit_values (tuple): Values to check for hits
|
|
130
|
-
inclusion_mode (bool): If True, hit when value in hit_values. If False, hit when not in hit_values.
|
|
131
|
-
|
|
132
|
-
Returns:
|
|
133
|
-
float: Ratio of successful rays (0.0 to 1.0)
|
|
151
|
+
def compute_vi_generic(observer_location, voxel_data, ray_directions, hit_values, meshsize, tree_k, tree_lad, inclusion_mode=True):
|
|
152
|
+
"""Compute view index accounting for tree transmittance.
|
|
153
|
+
|
|
154
|
+
For tree voxels (-2):
|
|
155
|
+
- If -2 in hit_values: counts obstruction (1 - transmittance) as hit contribution
|
|
156
|
+
- If -2 not in hit_values: applies transmittance normally
|
|
134
157
|
"""
|
|
135
|
-
hit_count = 0
|
|
136
158
|
total_rays = ray_directions.shape[0]
|
|
159
|
+
visibility_sum = 0.0
|
|
137
160
|
|
|
138
|
-
# Cast rays in all directions and count hits
|
|
139
161
|
for idx in range(total_rays):
|
|
140
162
|
direction = ray_directions[idx]
|
|
141
|
-
|
|
163
|
+
hit, value = trace_ray_generic(voxel_data, observer_location, direction,
|
|
164
|
+
hit_values, meshsize, tree_k, tree_lad, inclusion_mode)
|
|
165
|
+
|
|
142
166
|
if inclusion_mode:
|
|
143
|
-
if
|
|
144
|
-
|
|
167
|
+
if hit:
|
|
168
|
+
if -2 in hit_values:
|
|
169
|
+
# For trees in hit_values, use the hit contribution (1 - transmittance)
|
|
170
|
+
visibility_sum += (1.0 - value) if value < 1.0 else 1.0
|
|
171
|
+
else:
|
|
172
|
+
visibility_sum += 1.0
|
|
145
173
|
else:
|
|
146
|
-
if not
|
|
147
|
-
|
|
174
|
+
if not hit:
|
|
175
|
+
# For exclusion mode, use transmittance value directly
|
|
176
|
+
visibility_sum += value
|
|
148
177
|
|
|
149
|
-
return
|
|
178
|
+
return visibility_sum / total_rays
|
|
150
179
|
|
|
151
180
|
@njit(parallel=True)
|
|
152
|
-
def compute_vi_map_generic(voxel_data, ray_directions, view_height_voxel, hit_values,
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
Valid observer locations are empty voxels above ground level, excluding building roofs
|
|
156
|
-
and vegetation surfaces.
|
|
157
|
-
|
|
158
|
-
Args:
|
|
159
|
-
voxel_data (ndarray): 3D array of voxel values
|
|
160
|
-
ray_directions (ndarray): Array of direction vectors for rays
|
|
161
|
-
view_height_voxel (int): Height offset for observer in voxels
|
|
162
|
-
hit_values (tuple): Values to check for hits
|
|
163
|
-
inclusion_mode (bool): If True, hit when value in hit_values. If False, hit when not in hit_values.
|
|
164
|
-
|
|
165
|
-
Returns:
|
|
166
|
-
ndarray: 2D array of view index values with y-axis flipped
|
|
167
|
-
"""
|
|
181
|
+
def compute_vi_map_generic(voxel_data, ray_directions, view_height_voxel, hit_values,
|
|
182
|
+
meshsize, tree_k, tree_lad, inclusion_mode=True):
|
|
183
|
+
"""Compute view index map incorporating tree transmittance."""
|
|
168
184
|
nx, ny, nz = voxel_data.shape
|
|
169
185
|
vi_map = np.full((nx, ny), np.nan)
|
|
170
186
|
|
|
171
|
-
# Process each x,y position in parallel
|
|
172
187
|
for x in prange(nx):
|
|
173
188
|
for y in range(ny):
|
|
174
189
|
found_observer = False
|
|
175
|
-
# Find lowest empty voxel above ground
|
|
176
190
|
for z in range(1, nz):
|
|
177
191
|
if voxel_data[x, y, z] in (0, -2) and voxel_data[x, y, z - 1] not in (0, -2):
|
|
178
|
-
|
|
179
|
-
if voxel_data[x, y, z - 1] in (-3, 7, 8, 9):
|
|
192
|
+
if voxel_data[x, y, z - 1] in (-3, -2):
|
|
180
193
|
vi_map[x, y] = np.nan
|
|
181
194
|
found_observer = True
|
|
182
195
|
break
|
|
183
196
|
else:
|
|
184
|
-
# Place observer and compute view index
|
|
185
197
|
observer_location = np.array([x, y, z + view_height_voxel], dtype=np.float64)
|
|
186
|
-
vi_value = compute_vi_generic(observer_location, voxel_data, ray_directions,
|
|
198
|
+
vi_value = compute_vi_generic(observer_location, voxel_data, ray_directions,
|
|
199
|
+
hit_values, meshsize, tree_k, tree_lad, inclusion_mode)
|
|
187
200
|
vi_map[x, y] = vi_value
|
|
188
201
|
found_observer = True
|
|
189
202
|
break
|
|
@@ -221,6 +234,8 @@ def get_view_index(voxel_data, meshsize, mode=None, hit_values=None, inclusion_m
|
|
|
221
234
|
- N_elevation (int): Number of elevation angles for ray directions
|
|
222
235
|
- elevation_min_degrees (float): Minimum elevation angle in degrees
|
|
223
236
|
- elevation_max_degrees (float): Maximum elevation angle in degrees
|
|
237
|
+
- tree_k (float): Tree extinction coefficient (default: 0.5)
|
|
238
|
+
- tree_lad (float): Leaf area density in m^-1 (default: 1.0)
|
|
224
239
|
|
|
225
240
|
Returns:
|
|
226
241
|
ndarray: 2D array of computed view index values.
|
|
@@ -235,7 +250,7 @@ def get_view_index(voxel_data, meshsize, mode=None, hit_values=None, inclusion_m
|
|
|
235
250
|
hit_values = (0,)
|
|
236
251
|
inclusion_mode = False
|
|
237
252
|
else:
|
|
238
|
-
# For
|
|
253
|
+
# For custom mode, user must specify hit_values
|
|
239
254
|
if hit_values is None:
|
|
240
255
|
raise ValueError("For custom mode, you must provide hit_values.")
|
|
241
256
|
|
|
@@ -249,6 +264,10 @@ def get_view_index(voxel_data, meshsize, mode=None, hit_values=None, inclusion_m
|
|
|
249
264
|
N_elevation = kwargs.get("N_elevation", 10)
|
|
250
265
|
elevation_min_degrees = kwargs.get("elevation_min_degrees", -30)
|
|
251
266
|
elevation_max_degrees = kwargs.get("elevation_max_degrees", 30)
|
|
267
|
+
|
|
268
|
+
# Tree transmittance parameters
|
|
269
|
+
tree_k = kwargs.get("tree_k", 0.5)
|
|
270
|
+
tree_lad = kwargs.get("tree_lad", 1.0)
|
|
252
271
|
|
|
253
272
|
# Generate ray directions using spherical coordinates
|
|
254
273
|
azimuth_angles = np.linspace(0, 2 * np.pi, N_azimuth, endpoint=False)
|
|
@@ -265,10 +284,12 @@ def get_view_index(voxel_data, meshsize, mode=None, hit_values=None, inclusion_m
|
|
|
265
284
|
ray_directions.append([dx, dy, dz])
|
|
266
285
|
ray_directions = np.array(ray_directions, dtype=np.float64)
|
|
267
286
|
|
|
268
|
-
# Compute the view index map
|
|
269
|
-
vi_map = compute_vi_map_generic(voxel_data, ray_directions, view_height_voxel,
|
|
287
|
+
# Compute the view index map with transmittance parameters
|
|
288
|
+
vi_map = compute_vi_map_generic(voxel_data, ray_directions, view_height_voxel,
|
|
289
|
+
hit_values, meshsize, tree_k, tree_lad, inclusion_mode)
|
|
270
290
|
|
|
271
291
|
# Plot results
|
|
292
|
+
import matplotlib.pyplot as plt
|
|
272
293
|
cmap = plt.cm.get_cmap(colormap).copy()
|
|
273
294
|
cmap.set_bad(color='lightgray')
|
|
274
295
|
plt.figure(figsize=(10, 8))
|
|
@@ -622,29 +643,15 @@ def get_landmark_visibility_map(voxcity_grid, building_id_grid, building_geojson
|
|
|
622
643
|
|
|
623
644
|
return landmark_vis_map
|
|
624
645
|
|
|
625
|
-
def get_sky_view_factor_map(voxel_data, meshsize, **kwargs):
|
|
646
|
+
def get_sky_view_factor_map(voxel_data, meshsize, show_plot=False, **kwargs):
|
|
626
647
|
"""
|
|
627
648
|
Compute and visualize the Sky View Factor (SVF) for each valid observer cell in the voxel grid.
|
|
628
649
|
|
|
629
|
-
The SVF is computed similarly to how the 'sky' mode of the get_view_index function works:
|
|
630
|
-
- Rays are cast from each observer position upward (within a specified angular range).
|
|
631
|
-
- Any non-empty voxel encountered is considered an obstruction.
|
|
632
|
-
- The ratio of unobstructed rays to total rays is the SVF.
|
|
633
|
-
|
|
634
650
|
Args:
|
|
635
651
|
voxel_data (ndarray): 3D array of voxel values.
|
|
636
652
|
meshsize (float): Size of each voxel in meters.
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
- colormap (str): Matplotlib colormap name. Default: 'viridis'
|
|
640
|
-
- vmin (float): Minimum value for color bar. Default: 0.0
|
|
641
|
-
- vmax (float): Maximum value for color bar. Default: 1.0
|
|
642
|
-
- N_azimuth (int): Number of azimuth angles. Default: 60
|
|
643
|
-
- N_elevation (int): Number of elevation angles. Default: 10
|
|
644
|
-
- elevation_min_degrees (float): Minimum elevation angle in degrees. Typically 0 (horizon). Default: 0
|
|
645
|
-
- elevation_max_degrees (float): Maximum elevation angle in degrees. Typically 90 for full hemisphere. Default: 90
|
|
646
|
-
- obj_export (bool): Whether to export the result as an OBJ file. Default: False
|
|
647
|
-
- output_directory (str), output_file_name (str), etc. are also supported if OBJ export is needed.
|
|
653
|
+
show_plot (bool): Whether to display the plot.
|
|
654
|
+
**kwargs: Additional parameters.
|
|
648
655
|
|
|
649
656
|
Returns:
|
|
650
657
|
ndarray: 2D array of SVF values at each cell (x, y).
|
|
@@ -660,10 +667,11 @@ def get_sky_view_factor_map(voxel_data, meshsize, **kwargs):
|
|
|
660
667
|
elevation_min_degrees = kwargs.get("elevation_min_degrees", 0)
|
|
661
668
|
elevation_max_degrees = kwargs.get("elevation_max_degrees", 90)
|
|
662
669
|
|
|
670
|
+
# Get tree transmittance parameters
|
|
671
|
+
tree_k = kwargs.get("tree_k", 0.6) # Static extinction coefficient
|
|
672
|
+
tree_lad = kwargs.get("tree_lad", 1.0) # Leaf area density in m^-1
|
|
673
|
+
|
|
663
674
|
# Define hit_values and inclusion_mode for sky detection
|
|
664
|
-
# For sky: hit_values=(0,), inclusion_mode=False means:
|
|
665
|
-
# A hit occurs whenever we encounter a voxel_value != 0 along the ray.
|
|
666
|
-
# Thus, a ray that escapes with only 0's encountered is unobstructed sky.
|
|
667
675
|
hit_values = (0,)
|
|
668
676
|
inclusion_mode = False
|
|
669
677
|
|
|
@@ -682,26 +690,24 @@ def get_sky_view_factor_map(voxel_data, meshsize, **kwargs):
|
|
|
682
690
|
ray_directions.append([dx, dy, dz])
|
|
683
691
|
ray_directions = np.array(ray_directions, dtype=np.float64)
|
|
684
692
|
|
|
685
|
-
# Compute the SVF map using the
|
|
686
|
-
vi_map = compute_vi_map_generic(voxel_data, ray_directions, view_height_voxel,
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
#
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
plt.show()
|
|
693
|
+
# Compute the SVF map using the compute function
|
|
694
|
+
vi_map = compute_vi_map_generic(voxel_data, ray_directions, view_height_voxel,
|
|
695
|
+
hit_values, meshsize, tree_k, tree_lad, inclusion_mode)
|
|
696
|
+
|
|
697
|
+
# Plot results if requested
|
|
698
|
+
if show_plot:
|
|
699
|
+
import matplotlib.pyplot as plt
|
|
700
|
+
cmap = plt.cm.get_cmap(colormap).copy()
|
|
701
|
+
cmap.set_bad(color='lightgray')
|
|
702
|
+
plt.figure(figsize=(10, 8))
|
|
703
|
+
plt.title("Sky View Factor Map")
|
|
704
|
+
plt.imshow(vi_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
|
|
705
|
+
plt.colorbar(label='Sky View Factor')
|
|
706
|
+
plt.show()
|
|
700
707
|
|
|
701
708
|
# Optional OBJ export
|
|
702
709
|
obj_export = kwargs.get("obj_export", False)
|
|
703
|
-
if obj_export:
|
|
704
|
-
from ..file.obj import grid_to_obj
|
|
710
|
+
if obj_export:
|
|
705
711
|
dem_grid = kwargs.get("dem_grid", np.zeros_like(vi_map))
|
|
706
712
|
output_dir = kwargs.get("output_directory", "output")
|
|
707
713
|
output_file_name = kwargs.get("output_file_name", "sky_view_factor")
|
voxcity/utils/__init_.py
CHANGED