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.
Files changed (41) hide show
  1. voxcity/downloader/ocean.py +559 -0
  2. voxcity/generator/api.py +6 -0
  3. voxcity/generator/grids.py +45 -32
  4. voxcity/generator/pipeline.py +327 -27
  5. voxcity/geoprocessor/draw.py +14 -8
  6. voxcity/geoprocessor/raster/__init__.py +2 -0
  7. voxcity/geoprocessor/raster/core.py +31 -0
  8. voxcity/geoprocessor/raster/landcover.py +173 -49
  9. voxcity/geoprocessor/raster/raster.py +1 -1
  10. voxcity/models.py +2 -0
  11. voxcity/simulator/solar/__init__.py +13 -0
  12. voxcity/simulator_gpu/__init__.py +90 -0
  13. voxcity/simulator_gpu/core.py +322 -0
  14. voxcity/simulator_gpu/domain.py +36 -0
  15. voxcity/simulator_gpu/init_taichi.py +154 -0
  16. voxcity/simulator_gpu/raytracing.py +776 -0
  17. voxcity/simulator_gpu/solar/__init__.py +222 -0
  18. voxcity/simulator_gpu/solar/core.py +66 -0
  19. voxcity/simulator_gpu/solar/csf.py +1249 -0
  20. voxcity/simulator_gpu/solar/domain.py +618 -0
  21. voxcity/simulator_gpu/solar/epw.py +421 -0
  22. voxcity/simulator_gpu/solar/integration.py +4322 -0
  23. voxcity/simulator_gpu/solar/mask.py +459 -0
  24. voxcity/simulator_gpu/solar/radiation.py +3019 -0
  25. voxcity/simulator_gpu/solar/raytracing.py +182 -0
  26. voxcity/simulator_gpu/solar/reflection.py +533 -0
  27. voxcity/simulator_gpu/solar/sky.py +907 -0
  28. voxcity/simulator_gpu/solar/solar.py +337 -0
  29. voxcity/simulator_gpu/solar/svf.py +446 -0
  30. voxcity/simulator_gpu/solar/volumetric.py +2099 -0
  31. voxcity/simulator_gpu/visibility/__init__.py +109 -0
  32. voxcity/simulator_gpu/visibility/geometry.py +278 -0
  33. voxcity/simulator_gpu/visibility/integration.py +808 -0
  34. voxcity/simulator_gpu/visibility/landmark.py +753 -0
  35. voxcity/simulator_gpu/visibility/view.py +944 -0
  36. voxcity/visualizer/renderer.py +2 -1
  37. {voxcity-1.0.2.dist-info → voxcity-1.0.15.dist-info}/METADATA +16 -53
  38. {voxcity-1.0.2.dist-info → voxcity-1.0.15.dist-info}/RECORD +41 -16
  39. {voxcity-1.0.2.dist-info → voxcity-1.0.15.dist-info}/WHEEL +0 -0
  40. {voxcity-1.0.2.dist-info → voxcity-1.0.15.dist-info}/licenses/AUTHORS.rst +0 -0
  41. {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
+ }