voxcity 1.0.2__py3-none-any.whl → 1.0.13__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 (50) 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_gpu/__init__.py +115 -0
  12. voxcity/simulator_gpu/common/__init__.py +9 -0
  13. voxcity/simulator_gpu/common/geometry.py +11 -0
  14. voxcity/simulator_gpu/core.py +322 -0
  15. voxcity/simulator_gpu/domain.py +262 -0
  16. voxcity/simulator_gpu/environment.yml +11 -0
  17. voxcity/simulator_gpu/init_taichi.py +154 -0
  18. voxcity/simulator_gpu/integration.py +15 -0
  19. voxcity/simulator_gpu/kernels.py +56 -0
  20. voxcity/simulator_gpu/radiation.py +28 -0
  21. voxcity/simulator_gpu/raytracing.py +623 -0
  22. voxcity/simulator_gpu/sky.py +9 -0
  23. voxcity/simulator_gpu/solar/__init__.py +178 -0
  24. voxcity/simulator_gpu/solar/core.py +66 -0
  25. voxcity/simulator_gpu/solar/csf.py +1249 -0
  26. voxcity/simulator_gpu/solar/domain.py +561 -0
  27. voxcity/simulator_gpu/solar/epw.py +421 -0
  28. voxcity/simulator_gpu/solar/integration.py +2953 -0
  29. voxcity/simulator_gpu/solar/radiation.py +3019 -0
  30. voxcity/simulator_gpu/solar/raytracing.py +686 -0
  31. voxcity/simulator_gpu/solar/reflection.py +533 -0
  32. voxcity/simulator_gpu/solar/sky.py +907 -0
  33. voxcity/simulator_gpu/solar/solar.py +337 -0
  34. voxcity/simulator_gpu/solar/svf.py +446 -0
  35. voxcity/simulator_gpu/solar/volumetric.py +1151 -0
  36. voxcity/simulator_gpu/solar/voxcity.py +2953 -0
  37. voxcity/simulator_gpu/temporal.py +13 -0
  38. voxcity/simulator_gpu/utils.py +25 -0
  39. voxcity/simulator_gpu/view.py +32 -0
  40. voxcity/simulator_gpu/visibility/__init__.py +109 -0
  41. voxcity/simulator_gpu/visibility/geometry.py +278 -0
  42. voxcity/simulator_gpu/visibility/integration.py +808 -0
  43. voxcity/simulator_gpu/visibility/landmark.py +753 -0
  44. voxcity/simulator_gpu/visibility/view.py +944 -0
  45. voxcity/visualizer/renderer.py +2 -1
  46. {voxcity-1.0.2.dist-info → voxcity-1.0.13.dist-info}/METADATA +16 -53
  47. {voxcity-1.0.2.dist-info → voxcity-1.0.13.dist-info}/RECORD +50 -15
  48. {voxcity-1.0.2.dist-info → voxcity-1.0.13.dist-info}/WHEEL +0 -0
  49. {voxcity-1.0.2.dist-info → voxcity-1.0.13.dist-info}/licenses/AUTHORS.rst +0 -0
  50. {voxcity-1.0.2.dist-info → voxcity-1.0.13.dist-info}/licenses/LICENSE +0 -0
@@ -58,7 +58,7 @@ def create_land_cover_grid_from_geotiff_polygon(
58
58
  xs, ys = new_affine * (cols, rows)
59
59
  xs_flat, ys_flat = xs.flatten(), ys.flatten()
60
60
 
61
- row, col = src.index(xs_flat, ys_flat)
61
+ row, col = rasterio.transform.rowcol(src.transform, xs_flat, ys_flat)
62
62
  row, col = np.array(row), np.array(col)
63
63
 
64
64
  valid = (row >= 0) & (row < src.height) & (col >= 0) & (col < src.width)
@@ -79,14 +79,35 @@ def create_land_cover_grid_from_gdf_polygon(
79
79
  meshsize: float,
80
80
  source: str,
81
81
  rectangle_vertices: List[Tuple[float, float]],
82
- default_class: str = 'Developed space'
82
+ default_class: str = 'Developed space',
83
+ detect_ocean: bool = True,
84
+ land_polygon = "NOT_PROVIDED"
83
85
  ) -> np.ndarray:
84
- """Create a grid of land cover classes from GeoDataFrame polygon data."""
86
+ """
87
+ Create a grid of land cover classes from GeoDataFrame polygon data.
88
+
89
+ Uses vectorized rasterization for ~100x speedup over cell-by-cell intersection.
90
+
91
+ Args:
92
+ gdf: GeoDataFrame with land cover polygons and 'class' column
93
+ meshsize: Grid cell size in meters
94
+ source: Land cover data source name (e.g., 'OpenStreetMap')
95
+ rectangle_vertices: List of (lon, lat) tuples defining the area
96
+ default_class: Default class for cells not covered by any polygon
97
+ detect_ocean: If True, use OSM land polygons to detect ocean areas.
98
+ Areas outside land polygons will be classified as 'Water'
99
+ instead of the default class.
100
+ land_polygon: Optional pre-computed land polygon from OSM coastlines.
101
+ If provided (including None), this is used directly.
102
+ If "NOT_PROVIDED", coastlines will be queried when detect_ocean=True.
103
+
104
+ Returns:
105
+ 2D numpy array of land cover class names
106
+ """
85
107
  import numpy as np
86
108
  import geopandas as gpd
87
- from shapely.geometry import box
88
- from shapely.errors import GEOSException
89
- from rtree import index
109
+ from rasterio import features
110
+ from shapely.geometry import box, Polygon as ShapelyPolygon
90
111
 
91
112
  class_priority = get_class_priority(source)
92
113
 
@@ -95,8 +116,9 @@ def create_land_cover_grid_from_gdf_polygon(
95
116
  calculate_distance,
96
117
  normalize_to_one_meter,
97
118
  )
98
- from .core import calculate_grid_size, create_cell_polygon
119
+ from .core import calculate_grid_size
99
120
 
121
+ # Calculate grid dimensions
100
122
  geod = initialize_geod()
101
123
  vertex_0, vertex_1, vertex_3 = rectangle_vertices[0], rectangle_vertices[1], rectangle_vertices[3]
102
124
  dist_side_1 = calculate_distance(geod, vertex_0[0], vertex_0[1], vertex_1[0], vertex_1[1])
@@ -107,51 +129,153 @@ def create_land_cover_grid_from_gdf_polygon(
107
129
  u_vec = normalize_to_one_meter(side_1, dist_side_1)
108
130
  v_vec = normalize_to_one_meter(side_2, dist_side_2)
109
131
 
110
- origin = np.array(rectangle_vertices[0])
111
132
  grid_size, adjusted_meshsize = calculate_grid_size(side_1, side_2, u_vec, v_vec, meshsize)
133
+ rows, cols = grid_size
134
+
135
+ # Get bounding box for the raster
136
+ min_lon = min(coord[0] for coord in rectangle_vertices)
137
+ max_lon = max(coord[0] for coord in rectangle_vertices)
138
+ min_lat = min(coord[1] for coord in rectangle_vertices)
139
+ max_lat = max(coord[1] for coord in rectangle_vertices)
140
+
141
+ # Create affine transform (top-left origin, pixel size)
142
+ pixel_width = (max_lon - min_lon) / cols
143
+ pixel_height = (max_lat - min_lat) / rows
144
+ transform = Affine(pixel_width, 0, min_lon, 0, -pixel_height, max_lat)
145
+
146
+ # Build class name to priority mapping, then sort classes by priority (highest priority = lowest number = rasterize last)
147
+ unique_classes = gdf['class'].unique().tolist()
148
+ if default_class not in unique_classes:
149
+ unique_classes.append(default_class)
150
+
151
+ # Map class names to integer codes
152
+ class_to_code = {cls: i for i, cls in enumerate(unique_classes)}
153
+ code_to_class = {i: cls for cls, i in class_to_code.items()}
154
+ default_code = class_to_code[default_class]
155
+
156
+ # Initialize grid with default class code
157
+ grid_int = np.full((rows, cols), default_code, dtype=np.int32)
158
+
159
+ # Sort classes by priority (highest priority last so they overwrite lower priority)
160
+ # Lower priority number = higher priority = should be drawn last
161
+ sorted_classes = sorted(unique_classes, key=lambda c: class_priority.get(c, 999), reverse=True)
162
+
163
+ # Rasterize each class in priority order (lowest priority first, highest priority last overwrites)
164
+ for lc_class in sorted_classes:
165
+ if lc_class == default_class:
166
+ continue # Already filled as default
167
+
168
+ class_gdf = gdf[gdf['class'] == lc_class]
169
+ if class_gdf.empty:
170
+ continue
171
+
172
+ # Get all geometries for this class
173
+ geometries = class_gdf.geometry.tolist()
174
+
175
+ # Filter out invalid geometries and fix them
176
+ valid_geometries = []
177
+ for geom in geometries:
178
+ if geom is None or geom.is_empty:
179
+ continue
180
+ if not geom.is_valid:
181
+ geom = geom.buffer(0)
182
+ if geom.is_valid and not geom.is_empty:
183
+ valid_geometries.append(geom)
184
+
185
+ if not valid_geometries:
186
+ continue
187
+
188
+ # Create shapes for rasterization: (geometry, value) pairs
189
+ class_code = class_to_code[lc_class]
190
+ shapes = [(geom, class_code) for geom in valid_geometries]
191
+
192
+ # Rasterize this class onto the grid (overwrites previous values)
193
+ try:
194
+ features.rasterize(
195
+ shapes=shapes,
196
+ out=grid_int,
197
+ transform=transform,
198
+ all_touched=False, # Only cells whose center is inside
199
+ )
200
+ except Exception:
201
+ # Fallback: try each geometry individually
202
+ for geom, val in shapes:
203
+ try:
204
+ features.rasterize(
205
+ shapes=[(geom, val)],
206
+ out=grid_int,
207
+ transform=transform,
208
+ all_touched=False,
209
+ )
210
+ except Exception:
211
+ continue
212
+
213
+ # Convert integer codes back to class names
214
+ grid = np.empty((rows, cols), dtype=object)
215
+ for code, cls_name in code_to_class.items():
216
+ grid[grid_int == code] = cls_name
112
217
 
113
- grid = np.full(grid_size, default_class, dtype=object)
114
-
115
- extent = [min(coord[1] for coord in rectangle_vertices), max(coord[1] for coord in rectangle_vertices),
116
- min(coord[0] for coord in rectangle_vertices), max(coord[0] for coord in rectangle_vertices)]
117
- plotting_box = box(extent[2], extent[0], extent[3], extent[1])
118
-
119
- land_cover_polygons = []
120
- idx = index.Index()
121
- for i, row in gdf.iterrows():
122
- polygon = row.geometry
123
- land_cover_class = row['class']
124
- land_cover_polygons.append((polygon, land_cover_class))
125
- idx.insert(i, polygon.bounds)
126
-
127
- for i in range(grid_size[0]):
128
- for j in range(grid_size[1]):
129
- land_cover_class = default_class
130
- cell = create_cell_polygon(origin, i, j, adjusted_meshsize, u_vec, v_vec)
131
- for k in idx.intersection(cell.bounds):
132
- polygon, land_cover_class_temp = land_cover_polygons[k]
218
+ # Apply ocean detection BEFORE flipping if requested
219
+ # This uses land polygons from OSM coastlines to classify ocean areas
220
+ if detect_ocean:
221
+ try:
222
+ from ...downloader.ocean import get_land_polygon_for_area, get_ocean_class_for_source
223
+
224
+ ocean_class = get_ocean_class_for_source(source)
225
+
226
+ # Use provided land_polygon or query from coastlines if not provided
227
+ if land_polygon == "NOT_PROVIDED":
228
+ land_polygon = get_land_polygon_for_area(rectangle_vertices, use_cache=False)
229
+
230
+ if land_polygon is not None:
231
+ # Rasterize land polygon - cells inside are land, outside are ocean
232
+ land_mask = np.zeros((rows, cols), dtype=np.uint8)
233
+
133
234
  try:
134
- if cell.intersects(polygon):
135
- intersection = cell.intersection(polygon)
136
- if intersection.area > cell.area / 2:
137
- rank = class_priority[land_cover_class]
138
- rank_temp = class_priority[land_cover_class_temp]
139
- if rank_temp < rank:
140
- land_cover_class = land_cover_class_temp
141
- grid[i, j] = land_cover_class
142
- except GEOSException as e:
143
- try:
144
- fixed_polygon = polygon.buffer(0)
145
- if cell.intersects(fixed_polygon):
146
- intersection = cell.intersection(fixed_polygon)
147
- if intersection.area > cell.area / 2:
148
- rank = class_priority[land_cover_class]
149
- rank_temp = class_priority[land_cover_class_temp]
150
- if rank_temp < rank:
151
- land_cover_class = land_cover_class_temp
152
- grid[i, j] = land_cover_class
153
- except Exception:
154
- continue
235
+ if land_polygon.geom_type == 'Polygon':
236
+ land_geometries = [(land_polygon, 1)]
237
+ else: # MultiPolygon
238
+ land_geometries = [(geom, 1) for geom in land_polygon.geoms]
239
+
240
+ features.rasterize(
241
+ shapes=land_geometries,
242
+ out=land_mask,
243
+ transform=transform,
244
+ all_touched=False
245
+ )
246
+
247
+ # Apply ocean class to cells that are:
248
+ # 1. Outside land polygon (land_mask == 0)
249
+ # 2. Currently classified as the default class
250
+ ocean_cells = (land_mask == 0) & (grid == default_class)
251
+ ocean_count = np.sum(ocean_cells)
252
+
253
+ if ocean_count > 0:
254
+ grid[ocean_cells] = ocean_class
255
+ pct = 100 * ocean_count / grid.size
256
+ print(f" Ocean detection: {ocean_count:,} cells ({pct:.1f}%) classified as '{ocean_class}'")
257
+
258
+ except Exception as e:
259
+ print(f" Warning: Ocean rasterization failed: {e}")
260
+ else:
261
+ # No coastlines - check if area is all ocean or all land
262
+ from ...downloader.ocean import check_if_area_is_ocean_via_land_features
263
+ is_ocean = check_if_area_is_ocean_via_land_features(rectangle_vertices)
264
+ if is_ocean:
265
+ # Convert all default class cells to water
266
+ ocean_cells = (grid == default_class)
267
+ ocean_count = np.sum(ocean_cells)
268
+ if ocean_count > 0:
269
+ grid[ocean_cells] = ocean_class
270
+ pct = 100 * ocean_count / grid.size
271
+ print(f" Ocean detection: {ocean_count:,} cells ({pct:.1f}%) classified as '{ocean_class}' (open ocean)")
272
+
273
+ except Exception as e:
274
+ print(f" Warning: Ocean detection failed: {e}")
275
+
276
+ # Flip to match expected orientation (north-up)
277
+ grid = np.flipud(grid)
278
+
155
279
  return grid
156
280
 
157
281
 
@@ -38,7 +38,7 @@ def create_height_grid_from_geotiff_polygon(
38
38
  xs, ys = new_affine * (cols, rows)
39
39
  xs_flat, ys_flat = xs.flatten(), ys.flatten()
40
40
 
41
- row, col = src.index(xs_flat, ys_flat)
41
+ row, col = rasterio.transform.rowcol(src.transform, xs_flat, ys_flat)
42
42
  row, col = np.array(row), np.array(col)
43
43
 
44
44
  valid = (row >= 0) & (row < src.height) & (col >= 0) & (col < src.width)
voxcity/models.py CHANGED
@@ -70,6 +70,8 @@ class PipelineConfig:
70
70
  remove_perimeter_object: Optional[float] = None
71
71
  mapvis: bool = False
72
72
  gridvis: bool = True
73
+ # Parallel download mode: if True, downloads run concurrently using ThreadPoolExecutor
74
+ parallel_download: bool = False
73
75
  # Structured options for strategies and I/O/visualization
74
76
  land_cover_options: Dict[str, Any] = field(default_factory=dict)
75
77
  building_options: Dict[str, Any] = field(default_factory=dict)
@@ -0,0 +1,115 @@
1
+ """simulator_gpu: GPU-accelerated simulation modules using Taichi.
2
+
3
+ Compatibility goal:
4
+ Allow the common VoxCity pattern to work without code changes beyond the
5
+ import alias:
6
+
7
+ import simulator_gpu as simulator
8
+
9
+ by flattening a VoxCity-like public namespace (view/visibility/solar/utils).
10
+ """
11
+
12
+ import os
13
+
14
+ # Disable Numba caching to prevent stale cache issues when module paths change.
15
+ # This avoids "ModuleNotFoundError: No module named 'simulator_gpu'" errors
16
+ # that can occur when Numba tries to load cached functions with old module paths.
17
+ os.environ.setdefault("NUMBA_CACHE_DIR", "") # Disable disk caching
18
+ os.environ.setdefault("NUMBA_DISABLE_JIT", "0") # Keep JIT enabled for performance
19
+
20
+ # Import Taichi initialization utilities first
21
+ from .init_taichi import ( # noqa: F401
22
+ init_taichi,
23
+ ensure_initialized,
24
+ is_initialized,
25
+ )
26
+
27
+ # Check if Taichi is available
28
+ try:
29
+ import taichi as ti
30
+ _TAICHI_AVAILABLE = True
31
+ except ImportError:
32
+ _TAICHI_AVAILABLE = False
33
+
34
+ # VoxCity-style flattening
35
+ from .view import * # noqa: F401,F403
36
+ from .solar import * # noqa: F401,F403
37
+ from .utils import * # noqa: F401,F403
38
+
39
+ # Export submodules for explicit access
40
+ from . import solar # noqa: F401
41
+ from . import visibility # noqa: F401
42
+ from . import view # noqa: F401
43
+ from . import utils # noqa: F401
44
+ from . import common # noqa: F401
45
+
46
+ # VoxCity-flattened module names that some code expects to exist on the toplevel
47
+ from . import sky # noqa: F401
48
+ from . import kernels # noqa: F401
49
+ from . import radiation # noqa: F401
50
+ from . import temporal # noqa: F401
51
+ from . import integration # noqa: F401
52
+
53
+ # Commonly re-exported VoxCity solar helpers
54
+ from .kernels import compute_direct_solar_irradiance_map_binary # noqa: F401
55
+ from .radiation import compute_solar_irradiance_for_all_faces # noqa: F401
56
+
57
+ # Backward compatibility: some code treats `simulator.view` as `simulator.visibility`
58
+ # (VoxCity provides `view.py` wrapper; we also provide that module).
59
+
60
+ # Export shared modules (kept; extra symbols are fine)
61
+ from .core import ( # noqa: F401
62
+ Vector3, Point3,
63
+ PI, TWO_PI, DEG_TO_RAD, RAD_TO_DEG,
64
+ SOLAR_CONSTANT, EXT_COEF,
65
+ )
66
+ from .domain import Domain, IUP, IDOWN, INORTH, ISOUTH, IEAST, IWEST # noqa: F401
67
+
68
+
69
+ def clear_numba_cache():
70
+ """Clear Numba's compiled function cache to resolve stale cache issues.
71
+
72
+ Call this function if you encounter errors like:
73
+ ModuleNotFoundError: No module named 'simulator_gpu'
74
+
75
+ After calling this function, restart your Python kernel/interpreter.
76
+ """
77
+ import shutil
78
+ import glob
79
+ from pathlib import Path
80
+
81
+ cleared = []
82
+
83
+ # Clear .nbc and .nbi files in the package directory
84
+ package_dir = Path(__file__).parent
85
+ for pattern in ["**/*.nbc", "**/*.nbi"]:
86
+ for cache_file in package_dir.glob(pattern):
87
+ try:
88
+ cache_file.unlink()
89
+ cleared.append(str(cache_file))
90
+ except Exception:
91
+ pass
92
+
93
+ # Clear __pycache__ directories
94
+ for pycache in package_dir.glob("**/__pycache__"):
95
+ try:
96
+ shutil.rmtree(pycache)
97
+ cleared.append(str(pycache))
98
+ except Exception:
99
+ pass
100
+
101
+ # Try to clear user's .numba_cache if it exists
102
+ home = Path.home()
103
+ numba_cache = home / ".numba_cache"
104
+ if numba_cache.exists():
105
+ try:
106
+ shutil.rmtree(numba_cache)
107
+ cleared.append(str(numba_cache))
108
+ except Exception:
109
+ pass
110
+
111
+ print(f"Cleared {len(cleared)} cache items. Please restart your Python kernel.")
112
+ return cleared
113
+
114
+
115
+ __version__ = "0.1.0"
@@ -0,0 +1,9 @@
1
+ """VoxCity-style `common` namespace.
2
+
3
+ VoxCity exposes helpers under `voxcity.simulator.common.*`.
4
+ `simulator_gpu` implements only the subset needed for drop-in compatibility.
5
+ """
6
+
7
+ from .geometry import rotate_vector_axis_angle
8
+
9
+ __all__ = ["rotate_vector_axis_angle"]
@@ -0,0 +1,11 @@
1
+ """Geometry helpers for VoxCity compatibility.
2
+
3
+ This module is intended to satisfy imports like:
4
+ from simulator.common.geometry import rotate_vector_axis_angle
5
+
6
+ It forwards to the implementation used by `simulator_gpu.visibility`.
7
+ """
8
+
9
+ from ..visibility.geometry import rotate_vector_axis_angle
10
+
11
+ __all__ = ["rotate_vector_axis_angle"]