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/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 trace_ray_generic(voxel_data, origin, direction, hit_values, inclusion_mode=True):
26
- """Trace a ray through a voxel grid and check for hits with specified values.
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
- voxel_data (ndarray): 3D array of voxel values
32
- origin (tuple): Starting point (x,y,z) of ray
33
- direction (tuple): Direction vector of ray
34
- hit_values (tuple): Values to check for hits
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
- bool: True if ray hits target value(s), False otherwise
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 at center of starting voxel
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
- # Determine step direction for each axis
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 distances to next voxel boundaries and step sizes
62
- if dx != 0:
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 != 0:
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 != 0:
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
- # Move to next voxel using DDA algorithm
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
- # No hit found within grid bounds
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 a single observer location by casting multiple rays.
124
-
125
- Args:
126
- observer_location (ndarray): Position of observer (x,y,z)
127
- voxel_data (ndarray): 3D array of voxel values
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
- result = trace_ray_generic(voxel_data, observer_location, direction, hit_values, inclusion_mode)
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 result: # hit found
144
- hit_count += 1
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 result: # no hit means success in exclusion mode
147
- hit_count += 1
174
+ if not hit:
175
+ # For exclusion mode, use transmittance value directly
176
+ visibility_sum += value
148
177
 
149
- return hit_count / total_rays
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, inclusion_mode=True):
153
- """Compute view index map for entire grid by placing observers at valid locations.
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
- # Skip if standing on building or vegetation
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, hit_values, inclusion_mode)
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 other modes, user must specify hit_values
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, hit_values, inclusion_mode)
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
- **kwargs: Additional parameters:
638
- - view_point_height (float): Observer height above ground in meters. Default: 1.5
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 same compute function but with the sky parameters
686
- vi_map = compute_vi_map_generic(voxel_data, ray_directions, view_height_voxel, hit_values, inclusion_mode)
687
-
688
- # vi_map now holds the fraction of rays that 'hit' the condition for sky (no obstruction)
689
- # Actually, since inclusion_mode=False and hit_values=(0,), vi_map gives the fraction of unobstructed rays.
690
- # This is essentially the sky view factor.
691
-
692
- # Plot results
693
- cmap = plt.cm.get_cmap(colormap).copy()
694
- cmap.set_bad(color='lightgray')
695
- plt.figure(figsize=(10, 8))
696
- plt.title("Sky View Factor Map")
697
- plt.imshow(vi_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
698
- plt.colorbar(label='Sky View Factor')
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
@@ -1,2 +1,3 @@
1
1
  from .visualization import *
2
- from .lc import *
2
+ from .lc import *
3
+ from .weather import *