voxcity 1.0.2__py3-none-any.whl → 1.0.15__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.
- voxcity/downloader/ocean.py +559 -0
- voxcity/generator/api.py +6 -0
- voxcity/generator/grids.py +45 -32
- voxcity/generator/pipeline.py +327 -27
- voxcity/geoprocessor/draw.py +14 -8
- voxcity/geoprocessor/raster/__init__.py +2 -0
- voxcity/geoprocessor/raster/core.py +31 -0
- voxcity/geoprocessor/raster/landcover.py +173 -49
- voxcity/geoprocessor/raster/raster.py +1 -1
- voxcity/models.py +2 -0
- voxcity/simulator/solar/__init__.py +13 -0
- voxcity/simulator_gpu/__init__.py +90 -0
- voxcity/simulator_gpu/core.py +322 -0
- voxcity/simulator_gpu/domain.py +36 -0
- voxcity/simulator_gpu/init_taichi.py +154 -0
- voxcity/simulator_gpu/raytracing.py +776 -0
- voxcity/simulator_gpu/solar/__init__.py +222 -0
- voxcity/simulator_gpu/solar/core.py +66 -0
- voxcity/simulator_gpu/solar/csf.py +1249 -0
- voxcity/simulator_gpu/solar/domain.py +618 -0
- voxcity/simulator_gpu/solar/epw.py +421 -0
- voxcity/simulator_gpu/solar/integration.py +4322 -0
- voxcity/simulator_gpu/solar/mask.py +459 -0
- voxcity/simulator_gpu/solar/radiation.py +3019 -0
- voxcity/simulator_gpu/solar/raytracing.py +182 -0
- voxcity/simulator_gpu/solar/reflection.py +533 -0
- voxcity/simulator_gpu/solar/sky.py +907 -0
- voxcity/simulator_gpu/solar/solar.py +337 -0
- voxcity/simulator_gpu/solar/svf.py +446 -0
- voxcity/simulator_gpu/solar/volumetric.py +2099 -0
- voxcity/simulator_gpu/visibility/__init__.py +109 -0
- voxcity/simulator_gpu/visibility/geometry.py +278 -0
- voxcity/simulator_gpu/visibility/integration.py +808 -0
- voxcity/simulator_gpu/visibility/landmark.py +753 -0
- voxcity/simulator_gpu/visibility/view.py +944 -0
- voxcity/visualizer/renderer.py +2 -1
- {voxcity-1.0.2.dist-info → voxcity-1.0.15.dist-info}/METADATA +16 -53
- {voxcity-1.0.2.dist-info → voxcity-1.0.15.dist-info}/RECORD +41 -16
- {voxcity-1.0.2.dist-info → voxcity-1.0.15.dist-info}/WHEEL +0 -0
- {voxcity-1.0.2.dist-info → voxcity-1.0.15.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-1.0.2.dist-info → voxcity-1.0.15.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Computation Mask Utilities for Solar Irradiance Simulation
|
|
3
|
+
|
|
4
|
+
This module provides utilities for creating computation masks to limit
|
|
5
|
+
solar irradiance calculations to specific sub-areas within the domain.
|
|
6
|
+
|
|
7
|
+
Using a computation mask can significantly speed up calculations when
|
|
8
|
+
you only need results for a portion of the area.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
from typing import List, Tuple, Optional, Union
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create_computation_mask(
|
|
16
|
+
voxcity,
|
|
17
|
+
method: str = 'center',
|
|
18
|
+
fraction: float = 0.5,
|
|
19
|
+
i_range: Optional[Tuple[int, int]] = None,
|
|
20
|
+
j_range: Optional[Tuple[int, int]] = None,
|
|
21
|
+
polygon_vertices: Optional[List[Tuple[float, float]]] = None,
|
|
22
|
+
center: Optional[Tuple[float, float]] = None,
|
|
23
|
+
radius_m: float = 500.0,
|
|
24
|
+
) -> np.ndarray:
|
|
25
|
+
"""
|
|
26
|
+
Create a 2D boolean computation mask for sub-area solar calculations.
|
|
27
|
+
|
|
28
|
+
This function creates a mask that specifies which grid cells should be
|
|
29
|
+
computed. Cells where mask is True will be calculated; cells where mask
|
|
30
|
+
is False will be set to NaN in the output.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
voxcity: VoxCity object containing voxel data and metadata
|
|
34
|
+
method: Method for creating the mask. Options:
|
|
35
|
+
- 'center': Fraction-based center crop (uses `fraction` parameter)
|
|
36
|
+
- 'indices': Direct grid index specification (uses `i_range`, `j_range`)
|
|
37
|
+
- 'polygon': From geographic polygon (uses `polygon_vertices`)
|
|
38
|
+
- 'buffer': Circular buffer around a point (uses `center`, `radius_m`)
|
|
39
|
+
- 'full': No masking, compute entire area
|
|
40
|
+
fraction: For 'center' method, the fraction of the area to include.
|
|
41
|
+
0.5 means center 50% of the area. Default: 0.5
|
|
42
|
+
i_range: For 'indices' method, tuple of (start_i, end_i) grid indices.
|
|
43
|
+
Indices are inclusive. If None, uses full i range.
|
|
44
|
+
j_range: For 'indices' method, tuple of (start_j, end_j) grid indices.
|
|
45
|
+
Indices are inclusive. If None, uses full j range.
|
|
46
|
+
polygon_vertices: For 'polygon' method, list of (lon, lat) coordinates
|
|
47
|
+
defining the polygon boundary.
|
|
48
|
+
center: For 'buffer' method, tuple of (lon, lat) for the center point.
|
|
49
|
+
radius_m: For 'buffer' method, radius in meters. Default: 500.0
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
2D numpy boolean array of shape (nx, ny) where True indicates cells
|
|
53
|
+
to compute and False indicates cells to skip.
|
|
54
|
+
|
|
55
|
+
Examples:
|
|
56
|
+
# Center 50% of the area
|
|
57
|
+
>>> mask = create_computation_mask(voxcity, method='center', fraction=0.5)
|
|
58
|
+
|
|
59
|
+
# Specific grid region
|
|
60
|
+
>>> mask = create_computation_mask(voxcity, method='indices',
|
|
61
|
+
... i_range=(100, 200), j_range=(100, 200))
|
|
62
|
+
|
|
63
|
+
# From polygon coordinates
|
|
64
|
+
>>> mask = create_computation_mask(voxcity, method='polygon',
|
|
65
|
+
... polygon_vertices=[(lon1, lat1), ...])
|
|
66
|
+
|
|
67
|
+
# 500m buffer around a point
|
|
68
|
+
>>> mask = create_computation_mask(voxcity, method='buffer',
|
|
69
|
+
... center=(lon, lat), radius_m=500)
|
|
70
|
+
"""
|
|
71
|
+
# Get grid dimensions from voxcity
|
|
72
|
+
voxel_data = voxcity.voxels.classes
|
|
73
|
+
nx, ny = voxel_data.shape[0], voxel_data.shape[1]
|
|
74
|
+
|
|
75
|
+
if method == 'full':
|
|
76
|
+
return np.ones((nx, ny), dtype=bool)
|
|
77
|
+
|
|
78
|
+
elif method == 'center':
|
|
79
|
+
return _create_center_mask(nx, ny, fraction)
|
|
80
|
+
|
|
81
|
+
elif method == 'indices':
|
|
82
|
+
return _create_indices_mask(nx, ny, i_range, j_range)
|
|
83
|
+
|
|
84
|
+
elif method == 'polygon':
|
|
85
|
+
if polygon_vertices is None:
|
|
86
|
+
raise ValueError("polygon_vertices required for method='polygon'")
|
|
87
|
+
return _create_polygon_mask(voxcity, polygon_vertices)
|
|
88
|
+
|
|
89
|
+
elif method == 'buffer':
|
|
90
|
+
if center is None:
|
|
91
|
+
raise ValueError("center (lon, lat) required for method='buffer'")
|
|
92
|
+
return _create_buffer_mask(voxcity, center, radius_m)
|
|
93
|
+
|
|
94
|
+
else:
|
|
95
|
+
raise ValueError(f"Unknown method: {method}. "
|
|
96
|
+
f"Choose from 'center', 'indices', 'polygon', 'buffer', 'full'")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _create_center_mask(nx: int, ny: int, fraction: float) -> np.ndarray:
|
|
100
|
+
"""Create a mask for the center fraction of the grid."""
|
|
101
|
+
if not 0 < fraction <= 1:
|
|
102
|
+
raise ValueError(f"fraction must be between 0 and 1, got {fraction}")
|
|
103
|
+
|
|
104
|
+
mask = np.zeros((nx, ny), dtype=bool)
|
|
105
|
+
|
|
106
|
+
# Calculate center region bounds
|
|
107
|
+
margin_i = int(nx * (1 - fraction) / 2)
|
|
108
|
+
margin_j = int(ny * (1 - fraction) / 2)
|
|
109
|
+
|
|
110
|
+
# Ensure at least 1 cell is included
|
|
111
|
+
start_i = max(0, margin_i)
|
|
112
|
+
end_i = min(nx, nx - margin_i)
|
|
113
|
+
start_j = max(0, margin_j)
|
|
114
|
+
end_j = min(ny, ny - margin_j)
|
|
115
|
+
|
|
116
|
+
# Ensure we have at least 1 cell even for very small fractions
|
|
117
|
+
if end_i <= start_i:
|
|
118
|
+
start_i = nx // 2
|
|
119
|
+
end_i = start_i + 1
|
|
120
|
+
if end_j <= start_j:
|
|
121
|
+
start_j = ny // 2
|
|
122
|
+
end_j = start_j + 1
|
|
123
|
+
|
|
124
|
+
mask[start_i:end_i, start_j:end_j] = True
|
|
125
|
+
return mask
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _create_indices_mask(
|
|
129
|
+
nx: int,
|
|
130
|
+
ny: int,
|
|
131
|
+
i_range: Optional[Tuple[int, int]],
|
|
132
|
+
j_range: Optional[Tuple[int, int]]
|
|
133
|
+
) -> np.ndarray:
|
|
134
|
+
"""Create a mask from grid index ranges."""
|
|
135
|
+
mask = np.zeros((nx, ny), dtype=bool)
|
|
136
|
+
|
|
137
|
+
# Default to full range if not specified
|
|
138
|
+
start_i = 0 if i_range is None else max(0, i_range[0])
|
|
139
|
+
end_i = nx if i_range is None else min(nx, i_range[1] + 1) # +1 for inclusive
|
|
140
|
+
start_j = 0 if j_range is None else max(0, j_range[0])
|
|
141
|
+
end_j = ny if j_range is None else min(ny, j_range[1] + 1) # +1 for inclusive
|
|
142
|
+
|
|
143
|
+
mask[start_i:end_i, start_j:end_j] = True
|
|
144
|
+
return mask
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _create_polygon_mask(
|
|
148
|
+
voxcity,
|
|
149
|
+
polygon_vertices: List[Tuple[float, float]]
|
|
150
|
+
) -> np.ndarray:
|
|
151
|
+
"""Create a mask from a geographic polygon."""
|
|
152
|
+
from shapely.geometry import Polygon, Point
|
|
153
|
+
|
|
154
|
+
voxel_data = voxcity.voxels.classes
|
|
155
|
+
nx, ny = voxel_data.shape[0], voxel_data.shape[1]
|
|
156
|
+
meshsize = voxcity.voxels.meta.meshsize
|
|
157
|
+
|
|
158
|
+
# Get the rectangle vertices for coordinate transformation
|
|
159
|
+
rectangle_vertices = None
|
|
160
|
+
if hasattr(voxcity, 'extras') and isinstance(voxcity.extras, dict):
|
|
161
|
+
rectangle_vertices = voxcity.extras.get('rectangle_vertices', None)
|
|
162
|
+
|
|
163
|
+
if rectangle_vertices is None:
|
|
164
|
+
raise ValueError("voxcity must have rectangle_vertices in extras for polygon masking")
|
|
165
|
+
|
|
166
|
+
# Calculate origin and transformation
|
|
167
|
+
lons = [v[0] for v in rectangle_vertices]
|
|
168
|
+
lats = [v[1] for v in rectangle_vertices]
|
|
169
|
+
origin_lon = min(lons)
|
|
170
|
+
origin_lat = min(lats)
|
|
171
|
+
|
|
172
|
+
# Convert polygon to grid coordinates
|
|
173
|
+
# Use approximate meters per degree at this latitude
|
|
174
|
+
lat_center = (min(lats) + max(lats)) / 2
|
|
175
|
+
meters_per_deg_lon = 111320 * np.cos(np.radians(lat_center))
|
|
176
|
+
meters_per_deg_lat = 110540
|
|
177
|
+
|
|
178
|
+
# Create shapely polygon in grid coordinates
|
|
179
|
+
polygon_grid_coords = []
|
|
180
|
+
for lon, lat in polygon_vertices:
|
|
181
|
+
grid_x = (lon - origin_lon) * meters_per_deg_lon / meshsize
|
|
182
|
+
grid_y = (lat - origin_lat) * meters_per_deg_lat / meshsize
|
|
183
|
+
polygon_grid_coords.append((grid_x, grid_y))
|
|
184
|
+
|
|
185
|
+
polygon = Polygon(polygon_grid_coords)
|
|
186
|
+
|
|
187
|
+
# Create mask by checking which grid cells are inside the polygon
|
|
188
|
+
mask = np.zeros((nx, ny), dtype=bool)
|
|
189
|
+
|
|
190
|
+
# For efficiency, first check bounding box
|
|
191
|
+
minx, miny, maxx, maxy = polygon.bounds
|
|
192
|
+
i_min = max(0, int(minx))
|
|
193
|
+
i_max = min(nx, int(maxx) + 1)
|
|
194
|
+
j_min = max(0, int(miny))
|
|
195
|
+
j_max = min(ny, int(maxy) + 1)
|
|
196
|
+
|
|
197
|
+
# Check each cell center within bounding box
|
|
198
|
+
for i in range(i_min, i_max):
|
|
199
|
+
for j in range(j_min, j_max):
|
|
200
|
+
# Cell center
|
|
201
|
+
cell_center = Point(i + 0.5, j + 0.5)
|
|
202
|
+
if polygon.contains(cell_center):
|
|
203
|
+
mask[i, j] = True
|
|
204
|
+
|
|
205
|
+
return mask
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _create_buffer_mask(
|
|
209
|
+
voxcity,
|
|
210
|
+
center: Tuple[float, float],
|
|
211
|
+
radius_m: float
|
|
212
|
+
) -> np.ndarray:
|
|
213
|
+
"""Create a circular buffer mask around a geographic point."""
|
|
214
|
+
voxel_data = voxcity.voxels.classes
|
|
215
|
+
nx, ny = voxel_data.shape[0], voxel_data.shape[1]
|
|
216
|
+
meshsize = voxcity.voxels.meta.meshsize
|
|
217
|
+
|
|
218
|
+
# Get the rectangle vertices for coordinate transformation
|
|
219
|
+
rectangle_vertices = None
|
|
220
|
+
if hasattr(voxcity, 'extras') and isinstance(voxcity.extras, dict):
|
|
221
|
+
rectangle_vertices = voxcity.extras.get('rectangle_vertices', None)
|
|
222
|
+
|
|
223
|
+
if rectangle_vertices is None:
|
|
224
|
+
raise ValueError("voxcity must have rectangle_vertices in extras for buffer masking")
|
|
225
|
+
|
|
226
|
+
# Calculate origin and transformation
|
|
227
|
+
lons = [v[0] for v in rectangle_vertices]
|
|
228
|
+
lats = [v[1] for v in rectangle_vertices]
|
|
229
|
+
origin_lon = min(lons)
|
|
230
|
+
origin_lat = min(lats)
|
|
231
|
+
|
|
232
|
+
# Convert center to grid coordinates
|
|
233
|
+
lat_center = (min(lats) + max(lats)) / 2
|
|
234
|
+
meters_per_deg_lon = 111320 * np.cos(np.radians(lat_center))
|
|
235
|
+
meters_per_deg_lat = 110540
|
|
236
|
+
|
|
237
|
+
center_lon, center_lat = center
|
|
238
|
+
center_i = (center_lon - origin_lon) * meters_per_deg_lon / meshsize
|
|
239
|
+
center_j = (center_lat - origin_lat) * meters_per_deg_lat / meshsize
|
|
240
|
+
|
|
241
|
+
# Convert radius to grid cells
|
|
242
|
+
radius_cells = radius_m / meshsize
|
|
243
|
+
|
|
244
|
+
# Create mask using distance from center
|
|
245
|
+
ii, jj = np.meshgrid(np.arange(nx), np.arange(ny), indexing='ij')
|
|
246
|
+
distances = np.sqrt((ii - center_i)**2 + (jj - center_j)**2)
|
|
247
|
+
mask = distances <= radius_cells
|
|
248
|
+
|
|
249
|
+
return mask
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def draw_computation_mask(
|
|
253
|
+
voxcity,
|
|
254
|
+
zoom: int = 17,
|
|
255
|
+
):
|
|
256
|
+
"""
|
|
257
|
+
Interactive map for drawing a computation mask polygon.
|
|
258
|
+
|
|
259
|
+
This function displays an interactive map where users can draw a polygon
|
|
260
|
+
to define the computation area. After drawing, call `get_mask_from_drawing()`
|
|
261
|
+
with the returned polygon to create the mask.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
voxcity: VoxCity object containing voxel data and metadata
|
|
265
|
+
zoom: Initial zoom level for the map. Default: 17
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
tuple: (map_object, drawn_polygons)
|
|
269
|
+
- map_object: ipyleaflet Map instance with drawing controls
|
|
270
|
+
- drawn_polygons: List that will contain drawn polygon vertices
|
|
271
|
+
after the user draws on the map
|
|
272
|
+
|
|
273
|
+
Example:
|
|
274
|
+
# Step 1: Display map and draw polygon
|
|
275
|
+
>>> m, polygons = draw_computation_mask(voxcity)
|
|
276
|
+
>>> display(m) # In Jupyter, draw a polygon on the map
|
|
277
|
+
|
|
278
|
+
# Step 2: After drawing, get the mask
|
|
279
|
+
>>> mask = get_mask_from_drawing(voxcity, polygons)
|
|
280
|
+
|
|
281
|
+
# Step 3: Use the mask in solar calculation
|
|
282
|
+
>>> solar_grid = get_global_solar_irradiance_using_epw(
|
|
283
|
+
... voxcity, computation_mask=mask, ...
|
|
284
|
+
... )
|
|
285
|
+
"""
|
|
286
|
+
try:
|
|
287
|
+
from ipyleaflet import Map, DrawControl, TileLayer
|
|
288
|
+
except ImportError:
|
|
289
|
+
raise ImportError("ipyleaflet is required for interactive mask drawing. "
|
|
290
|
+
"Install with: pip install ipyleaflet")
|
|
291
|
+
|
|
292
|
+
# Get rectangle vertices for map center
|
|
293
|
+
rectangle_vertices = None
|
|
294
|
+
if hasattr(voxcity, 'extras') and isinstance(voxcity.extras, dict):
|
|
295
|
+
rectangle_vertices = voxcity.extras.get('rectangle_vertices', None)
|
|
296
|
+
|
|
297
|
+
if rectangle_vertices is not None:
|
|
298
|
+
lons = [v[0] for v in rectangle_vertices]
|
|
299
|
+
lats = [v[1] for v in rectangle_vertices]
|
|
300
|
+
center_lon = (min(lons) + max(lons)) / 2
|
|
301
|
+
center_lat = (min(lats) + max(lats)) / 2
|
|
302
|
+
else:
|
|
303
|
+
center_lon, center_lat = -100.0, 40.0
|
|
304
|
+
|
|
305
|
+
# Create map
|
|
306
|
+
m = Map(center=(center_lat, center_lon), zoom=zoom, scroll_wheel_zoom=True)
|
|
307
|
+
|
|
308
|
+
# Store drawn polygons
|
|
309
|
+
drawn_polygons = []
|
|
310
|
+
|
|
311
|
+
# Add draw control for polygons only
|
|
312
|
+
draw_control = DrawControl(
|
|
313
|
+
polygon={
|
|
314
|
+
"shapeOptions": {
|
|
315
|
+
"color": "red",
|
|
316
|
+
"fillColor": "red",
|
|
317
|
+
"fillOpacity": 0.3
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
rectangle={
|
|
321
|
+
"shapeOptions": {
|
|
322
|
+
"color": "blue",
|
|
323
|
+
"fillColor": "blue",
|
|
324
|
+
"fillOpacity": 0.3
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
circle={},
|
|
328
|
+
circlemarker={},
|
|
329
|
+
polyline={},
|
|
330
|
+
marker={}
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
def handle_draw(self, action, geo_json):
|
|
334
|
+
if action == 'created':
|
|
335
|
+
geom_type = geo_json['geometry']['type']
|
|
336
|
+
if geom_type == 'Polygon':
|
|
337
|
+
coordinates = geo_json['geometry']['coordinates'][0]
|
|
338
|
+
vertices = [(coord[0], coord[1]) for coord in coordinates[:-1]]
|
|
339
|
+
drawn_polygons.clear() # Only keep the last polygon
|
|
340
|
+
drawn_polygons.append(vertices)
|
|
341
|
+
print(f"Computation area polygon drawn with {len(vertices)} vertices")
|
|
342
|
+
elif geom_type == 'Rectangle':
|
|
343
|
+
# Handle rectangle (treated as polygon)
|
|
344
|
+
coordinates = geo_json['geometry']['coordinates'][0]
|
|
345
|
+
vertices = [(coord[0], coord[1]) for coord in coordinates[:-1]]
|
|
346
|
+
drawn_polygons.clear()
|
|
347
|
+
drawn_polygons.append(vertices)
|
|
348
|
+
print(f"Computation area rectangle drawn with {len(vertices)} vertices")
|
|
349
|
+
|
|
350
|
+
draw_control.on_draw(handle_draw)
|
|
351
|
+
m.add_control(draw_control)
|
|
352
|
+
|
|
353
|
+
return m, drawn_polygons
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def get_mask_from_drawing(
|
|
357
|
+
voxcity,
|
|
358
|
+
drawn_polygons: List,
|
|
359
|
+
) -> np.ndarray:
|
|
360
|
+
"""
|
|
361
|
+
Create a computation mask from interactively drawn polygon(s).
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
voxcity: VoxCity object containing voxel data and metadata
|
|
365
|
+
drawn_polygons: List of polygon vertices from draw_computation_mask()
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
2D numpy boolean array mask
|
|
369
|
+
|
|
370
|
+
Example:
|
|
371
|
+
>>> m, polygons = draw_computation_mask(voxcity)
|
|
372
|
+
>>> display(m) # Draw polygon
|
|
373
|
+
>>> mask = get_mask_from_drawing(voxcity, polygons)
|
|
374
|
+
"""
|
|
375
|
+
if not drawn_polygons:
|
|
376
|
+
raise ValueError("No polygons drawn. Draw a polygon on the map first.")
|
|
377
|
+
|
|
378
|
+
# Use the first (or only) polygon
|
|
379
|
+
polygon_vertices = drawn_polygons[0] if isinstance(drawn_polygons[0], list) else drawn_polygons
|
|
380
|
+
|
|
381
|
+
return create_computation_mask(voxcity, method='polygon', polygon_vertices=polygon_vertices)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def visualize_computation_mask(
|
|
385
|
+
voxcity,
|
|
386
|
+
mask: np.ndarray,
|
|
387
|
+
show_plot: bool = True,
|
|
388
|
+
) -> Optional[np.ndarray]:
|
|
389
|
+
"""
|
|
390
|
+
Visualize a computation mask overlaid on the voxcity grid.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
voxcity: VoxCity object
|
|
394
|
+
mask: 2D boolean mask array
|
|
395
|
+
show_plot: Whether to display the plot. Default: True
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
The mask array (for chaining)
|
|
399
|
+
"""
|
|
400
|
+
try:
|
|
401
|
+
import matplotlib.pyplot as plt
|
|
402
|
+
except ImportError:
|
|
403
|
+
print("matplotlib required for visualization")
|
|
404
|
+
return mask
|
|
405
|
+
|
|
406
|
+
if show_plot:
|
|
407
|
+
voxel_data = voxcity.voxels.classes
|
|
408
|
+
|
|
409
|
+
# Create a simple ground-level view
|
|
410
|
+
ground_level = np.zeros((voxel_data.shape[0], voxel_data.shape[1]))
|
|
411
|
+
for i in range(voxel_data.shape[0]):
|
|
412
|
+
for j in range(voxel_data.shape[1]):
|
|
413
|
+
# Find highest non-zero value
|
|
414
|
+
col = voxel_data[i, j, :]
|
|
415
|
+
non_zero = np.where(col != 0)[0]
|
|
416
|
+
if len(non_zero) > 0:
|
|
417
|
+
ground_level[i, j] = col[non_zero[-1]]
|
|
418
|
+
|
|
419
|
+
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
|
|
420
|
+
|
|
421
|
+
# Left: Ground level view
|
|
422
|
+
axes[0].imshow(np.flipud(ground_level.T), cmap='gray', alpha=0.5)
|
|
423
|
+
axes[0].set_title('Voxcity Grid')
|
|
424
|
+
axes[0].axis('off')
|
|
425
|
+
|
|
426
|
+
# Right: Mask overlay
|
|
427
|
+
axes[1].imshow(np.flipud(ground_level.T), cmap='gray', alpha=0.3)
|
|
428
|
+
mask_display = np.ma.masked_where(~mask.T, np.ones_like(mask.T))
|
|
429
|
+
axes[1].imshow(np.flipud(mask_display), cmap='Reds', alpha=0.5)
|
|
430
|
+
axes[1].set_title(f'Computation Mask ({np.sum(mask)} of {mask.size} cells)')
|
|
431
|
+
axes[1].axis('off')
|
|
432
|
+
|
|
433
|
+
plt.tight_layout()
|
|
434
|
+
plt.show()
|
|
435
|
+
|
|
436
|
+
return mask
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def get_mask_info(mask: np.ndarray) -> dict:
|
|
440
|
+
"""
|
|
441
|
+
Get information about a computation mask.
|
|
442
|
+
|
|
443
|
+
Args:
|
|
444
|
+
mask: 2D boolean mask array
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
Dictionary with mask statistics
|
|
448
|
+
"""
|
|
449
|
+
total_cells = mask.size
|
|
450
|
+
active_cells = int(np.sum(mask))
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
'shape': mask.shape,
|
|
454
|
+
'total_cells': total_cells,
|
|
455
|
+
'active_cells': active_cells,
|
|
456
|
+
'inactive_cells': total_cells - active_cells,
|
|
457
|
+
'coverage_fraction': active_cells / total_cells if total_cells > 0 else 0,
|
|
458
|
+
'coverage_percent': 100 * active_cells / total_cells if total_cells > 0 else 0,
|
|
459
|
+
}
|