voxcity 0.6.3__py3-none-any.whl → 0.6.5__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.

@@ -1328,9 +1328,17 @@ def get_building_solar_irradiance(
1328
1328
  sun_dz = np.sin(el_rad)
1329
1329
  sun_direction = np.array([sun_dx, sun_dy, sun_dz], dtype=np.float64)
1330
1330
 
1331
- # Extract mesh geometry data for processing
1332
- face_centers = building_svf_mesh.triangles_center # Center point of each face
1333
- face_normals = building_svf_mesh.face_normals # Normal vector for each face
1331
+ # Extract mesh geometry data for processing (optionally from cache)
1332
+ precomputed_geometry = kwargs.get("precomputed_geometry", None)
1333
+ if precomputed_geometry is not None:
1334
+ face_centers = precomputed_geometry.get("face_centers", building_svf_mesh.triangles_center)
1335
+ face_normals = precomputed_geometry.get("face_normals", building_svf_mesh.face_normals)
1336
+ face_svf_opt = precomputed_geometry.get("face_svf", None)
1337
+ grid_bounds_real = precomputed_geometry.get("grid_bounds_real", None)
1338
+ boundary_epsilon = precomputed_geometry.get("boundary_epsilon", None)
1339
+ else:
1340
+ face_centers = building_svf_mesh.triangles_center # Center point of each face
1341
+ face_normals = building_svf_mesh.face_normals # Normal vector for each face
1334
1342
 
1335
1343
  # Extract Sky View Factor data from mesh metadata
1336
1344
  if hasattr(building_svf_mesh, 'metadata') and ('svf' in building_svf_mesh.metadata):
@@ -1339,20 +1347,27 @@ def get_building_solar_irradiance(
1339
1347
  # Initialize with zeros if SVF not available (should be pre-calculated)
1340
1348
  face_svf = np.zeros(len(building_svf_mesh.faces), dtype=np.float64)
1341
1349
 
1342
- # Set up domain boundaries for boundary face detection
1343
- grid_shape = voxel_data.shape
1344
- grid_bounds_voxel = np.array([[0,0,0],[grid_shape[0], grid_shape[1], grid_shape[2]]], dtype=np.float64)
1345
- grid_bounds_real = grid_bounds_voxel * meshsize
1346
- boundary_epsilon = meshsize * 0.05 # Small tolerance for boundary detection
1350
+ # Set up domain boundaries for boundary face detection (use cache if available)
1351
+ if grid_bounds_real is None or boundary_epsilon is None:
1352
+ grid_shape = voxel_data.shape
1353
+ grid_bounds_voxel = np.array([[0,0,0],[grid_shape[0], grid_shape[1], grid_shape[2]]], dtype=np.float64)
1354
+ grid_bounds_real = grid_bounds_voxel * meshsize
1355
+ boundary_epsilon = meshsize * 0.05 # Small tolerance for boundary detection
1347
1356
 
1348
1357
  # Optional fast path using masked DDA kernel
1349
1358
  fast_path = kwargs.get("fast_path", True)
1359
+ precomputed_masks = kwargs.get("precomputed_masks", None)
1350
1360
  t0 = time.time()
1351
1361
  if fast_path:
1352
- # Prepare masks once
1353
- vox_is_tree = (voxel_data == -2)
1354
- vox_is_opaque = (voxel_data != 0) & (~vox_is_tree)
1355
- att = float(np.exp(-tree_k * tree_lad * meshsize))
1362
+ # Prepare masks (reuse cache when possible)
1363
+ if precomputed_masks is not None:
1364
+ vox_is_tree = precomputed_masks.get("vox_is_tree", (voxel_data == -2))
1365
+ vox_is_opaque = precomputed_masks.get("vox_is_opaque", (voxel_data != 0) & (voxel_data != -2))
1366
+ att = float(precomputed_masks.get("att", np.exp(-tree_k * tree_lad * meshsize)))
1367
+ else:
1368
+ vox_is_tree = (voxel_data == -2)
1369
+ vox_is_opaque = (voxel_data != 0) & (~vox_is_tree)
1370
+ att = float(np.exp(-tree_k * tree_lad * meshsize))
1356
1371
 
1357
1372
  face_direct, face_diffuse, face_global = compute_solar_irradiance_for_all_faces_masked(
1358
1373
  face_centers.astype(np.float64),
@@ -1373,7 +1388,7 @@ def get_building_solar_irradiance(
1373
1388
  face_direct, face_diffuse, face_global = compute_solar_irradiance_for_all_faces(
1374
1389
  face_centers,
1375
1390
  face_normals,
1376
- face_svf,
1391
+ face_svf_opt if (precomputed_geometry is not None and face_svf_opt is not None) else face_svf,
1377
1392
  sun_direction,
1378
1393
  direct_normal_irradiance,
1379
1394
  diffuse_irradiance,
@@ -1757,13 +1772,29 @@ def get_cumulative_building_solar_irradiance(
1757
1772
  face_cum_global = np.zeros(n_faces, dtype=np.float64)
1758
1773
 
1759
1774
  # Pre-extract mesh face arrays and domain bounds for fast path
1760
- face_centers = building_svf_mesh.triangles_center
1761
- face_normals = building_svf_mesh.face_normals
1762
- face_svf = building_svf_mesh.metadata['svf'] if ('svf' in building_svf_mesh.metadata) else np.zeros(n_faces, dtype=np.float64)
1763
- grid_shape = voxel_data.shape
1764
- grid_bounds_voxel = np.array([[0, 0, 0], [grid_shape[0], grid_shape[1], grid_shape[2]]], dtype=np.float64)
1765
- grid_bounds_real = grid_bounds_voxel * meshsize
1766
- boundary_epsilon = meshsize * 0.05
1775
+ # Optionally reuse precomputed geometry/bounds
1776
+ precomputed_geometry = kwargs.get("precomputed_geometry", None)
1777
+ if precomputed_geometry is not None:
1778
+ face_centers = precomputed_geometry.get("face_centers", building_svf_mesh.triangles_center)
1779
+ face_normals = precomputed_geometry.get("face_normals", building_svf_mesh.face_normals)
1780
+ face_svf = precomputed_geometry.get(
1781
+ "face_svf",
1782
+ building_svf_mesh.metadata['svf'] if ('svf' in building_svf_mesh.metadata) else np.zeros(n_faces, dtype=np.float64)
1783
+ )
1784
+ grid_bounds_real = precomputed_geometry.get("grid_bounds_real", None)
1785
+ boundary_epsilon = precomputed_geometry.get("boundary_epsilon", None)
1786
+ else:
1787
+ face_centers = building_svf_mesh.triangles_center
1788
+ face_normals = building_svf_mesh.face_normals
1789
+ face_svf = building_svf_mesh.metadata['svf'] if ('svf' in building_svf_mesh.metadata) else np.zeros(n_faces, dtype=np.float64)
1790
+ grid_bounds_real = None
1791
+ boundary_epsilon = None
1792
+
1793
+ if grid_bounds_real is None or boundary_epsilon is None:
1794
+ grid_shape = voxel_data.shape
1795
+ grid_bounds_voxel = np.array([[0, 0, 0], [grid_shape[0], grid_shape[1], grid_shape[2]]], dtype=np.float64)
1796
+ grid_bounds_real = grid_bounds_voxel * meshsize
1797
+ boundary_epsilon = meshsize * 0.05
1767
1798
 
1768
1799
  # Params used in Numba kernel
1769
1800
  hit_values = (0,) # sky
@@ -1779,20 +1810,28 @@ def get_cumulative_building_solar_irradiance(
1779
1810
  progress_every = max(1, total_steps // 20) # ~5% steps
1780
1811
 
1781
1812
  # Pre-cast stable arrays to avoid repeated allocations
1782
- face_centers64 = building_svf_mesh.triangles_center.astype(np.float64)
1783
- face_normals64 = building_svf_mesh.face_normals.astype(np.float64)
1813
+ face_centers64 = (face_centers if isinstance(face_centers, np.ndarray) else building_svf_mesh.triangles_center).astype(np.float64)
1814
+ face_normals64 = (face_normals if isinstance(face_normals, np.ndarray) else building_svf_mesh.face_normals).astype(np.float64)
1784
1815
  face_svf64 = face_svf.astype(np.float64)
1785
1816
  x_min, y_min, z_min = grid_bounds_real[0, 0], grid_bounds_real[0, 1], grid_bounds_real[0, 2]
1786
1817
  x_max, y_max, z_max = grid_bounds_real[1, 0], grid_bounds_real[1, 1], grid_bounds_real[1, 2]
1787
1818
 
1788
1819
  if fast_path:
1789
1820
  # Use masked cumulative kernel with chunking to minimize Python overhead
1790
- vox_is_tree = (voxel_data == -2)
1791
- vox_is_opaque = (voxel_data != 0) & (~vox_is_tree)
1792
- att = float(np.exp(-tree_k * tree_lad * meshsize))
1821
+ precomputed_masks = kwargs.get("precomputed_masks", None)
1822
+ if precomputed_masks is not None:
1823
+ vox_is_tree = precomputed_masks.get("vox_is_tree", (voxel_data == -2))
1824
+ vox_is_opaque = precomputed_masks.get("vox_is_opaque", (voxel_data != 0) & (voxel_data != -2))
1825
+ att = float(precomputed_masks.get("att", np.exp(-tree_k * tree_lad * meshsize)))
1826
+ else:
1827
+ vox_is_tree = (voxel_data == -2)
1828
+ vox_is_opaque = (voxel_data != 0) & (~vox_is_tree)
1829
+ att = float(np.exp(-tree_k * tree_lad * meshsize))
1793
1830
 
1794
1831
  # Auto-tune chunk size if user didn't pass one
1795
1832
  time_batch_size = _auto_time_batch_size(n_faces, total_steps, kwargs.get("time_batch_size", None))
1833
+ if progress_report:
1834
+ print(f"Faces: {n_faces:,}, Timesteps: {total_steps:,}, Batch size: {time_batch_size}")
1796
1835
 
1797
1836
  for start in range(0, total_steps, time_batch_size):
1798
1837
  end = min(start + time_batch_size, total_steps)
@@ -2023,6 +2062,38 @@ def get_building_global_solar_irradiance_using_epw(
2023
2062
  )
2024
2063
 
2025
2064
  if progress_report:
2065
+ print(f"SVF ready. Faces: {len(building_svf_mesh.faces):,}")
2066
+
2067
+ # Step 2: Build precomputed caches (geometry, masks, attenuation) for speed
2068
+ precomputed_geometry = {}
2069
+ try:
2070
+ grid_shape = voxel_data.shape
2071
+ grid_bounds_voxel = np.array([[0, 0, 0], [grid_shape[0], grid_shape[1], grid_shape[2]]], dtype=np.float64)
2072
+ grid_bounds_real = grid_bounds_voxel * meshsize
2073
+ boundary_epsilon = meshsize * 0.05
2074
+ precomputed_geometry = {
2075
+ 'face_centers': building_svf_mesh.triangles_center,
2076
+ 'face_normals': building_svf_mesh.face_normals,
2077
+ 'face_svf': building_svf_mesh.metadata['svf'] if ('svf' in building_svf_mesh.metadata) else None,
2078
+ 'grid_bounds_real': grid_bounds_real,
2079
+ 'boundary_epsilon': boundary_epsilon,
2080
+ }
2081
+ except Exception:
2082
+ # Fallback silently
2083
+ precomputed_geometry = {}
2084
+
2085
+ tree_k = kwargs.get("tree_k", 0.6)
2086
+ tree_lad = kwargs.get("tree_lad", 1.0)
2087
+ precomputed_masks = {
2088
+ 'vox_is_tree': (voxel_data == -2),
2089
+ 'vox_is_opaque': (voxel_data != 0) & (voxel_data != -2),
2090
+ 'att': float(np.exp(-tree_k * tree_lad * meshsize)),
2091
+ }
2092
+
2093
+ if progress_report:
2094
+ t_cnt = int(np.count_nonzero(precomputed_masks['vox_is_tree']))
2095
+ b_cnt = int(np.count_nonzero(voxel_data == -3)) if hasattr(voxel_data, 'shape') else 0
2096
+ print(f"Precomputed caches: trees={t_cnt:,}, buildings={b_cnt:,}, tree_att_per_voxel={precomputed_masks['att']:.4f}")
2026
2097
  print(f"Processing Solar Irradiance for building surfaces...")
2027
2098
  result_mesh = None
2028
2099
 
@@ -2084,6 +2155,9 @@ def get_building_global_solar_irradiance_using_epw(
2084
2155
  direct_normal_irradiance,
2085
2156
  diffuse_irradiance,
2086
2157
  progress_report=progress_report,
2158
+ fast_path=fast_path,
2159
+ precomputed_geometry=precomputed_geometry,
2160
+ precomputed_masks=precomputed_masks,
2087
2161
  **_call_kwargs
2088
2162
  )
2089
2163
 
@@ -2146,6 +2220,8 @@ def get_building_global_solar_irradiance_using_epw(
2146
2220
  progress_report=progress_report,
2147
2221
  fast_path=fast_path,
2148
2222
  precomputed_solar_positions=solar_positions,
2223
+ precomputed_geometry=precomputed_geometry,
2224
+ precomputed_masks=precomputed_masks,
2149
2225
  colormap=kwargs.get('colormap', 'jet'),
2150
2226
  show_each_timestep=kwargs.get('show_each_timestep', False),
2151
2227
  obj_export=kwargs.get('obj_export', False),