voxcity 0.6.3__py3-none-any.whl → 0.6.4__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),
@@ -1654,7 +1654,8 @@ def visualize_numerical_grid_on_basemap(grid, rectangle_vertices, meshsize, valu
1654
1654
 
1655
1655
  def visualize_numerical_gdf_on_basemap(gdf, value_name="value", cmap='viridis', vmin=None, vmax=None,
1656
1656
  alpha=0.6, figsize=(12, 8), basemap='CartoDB light',
1657
- show_edge=False, edge_color='black', edge_width=0.5):
1657
+ show_edge=False, edge_color='black', edge_width=0.5,
1658
+ input_crs=None):
1658
1659
  """Visualizes a GeoDataFrame with numerical values on a basemap.
1659
1660
 
1660
1661
  Args:
@@ -1669,9 +1670,34 @@ def visualize_numerical_gdf_on_basemap(gdf, value_name="value", cmap='viridis',
1669
1670
  show_edge: Whether to show cell edges (default: False)
1670
1671
  edge_color: Color of cell edges (default: 'black')
1671
1672
  edge_width: Width of cell edges (default: 0.5)
1673
+ input_crs: Optional CRS to assign if the GeoDataFrame has no CRS. If not provided
1674
+ and CRS is missing, the function will attempt to infer WGS84 (EPSG:4326)
1675
+ when coordinates look like lon/lat; otherwise it will raise a clear error.
1672
1676
  """
1677
+ # Ensure CRS is defined; if missing, assign or infer
1678
+ if gdf.crs is None:
1679
+ if input_crs is not None:
1680
+ gdf = gdf.set_crs(input_crs, allow_override=True)
1681
+ else:
1682
+ # Try to infer WGS84 if bounds look like lon/lat
1683
+ try:
1684
+ minx, miny, maxx, maxy = gdf.total_bounds
1685
+ looks_like_lonlat = (
1686
+ -180.0 <= minx <= 180.0 and -180.0 <= maxx <= 180.0 and
1687
+ -90.0 <= miny <= 90.0 and -90.0 <= maxy <= 90.0
1688
+ )
1689
+ except Exception:
1690
+ looks_like_lonlat = False
1691
+ if looks_like_lonlat:
1692
+ gdf = gdf.set_crs("EPSG:4326", allow_override=True)
1693
+ else:
1694
+ raise ValueError(
1695
+ "Input GeoDataFrame has no CRS. Provide 'input_crs' (e.g., 'EPSG:4326' or 'EPSG:XXXX') "
1696
+ "or set gdf.crs before calling visualize_numerical_gdf_on_basemap."
1697
+ )
1698
+
1673
1699
  # Convert to Web Mercator if not already in that CRS
1674
- if gdf.crs != 'EPSG:3857':
1700
+ if str(gdf.crs) != 'EPSG:3857':
1675
1701
  gdf_web = gdf.to_crs(epsg=3857)
1676
1702
  else:
1677
1703
  gdf_web = gdf
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voxcity
3
- Version: 0.6.3
3
+ Version: 0.6.4
4
4
  Summary: voxcity is an easy and one-stop tool to output 3d city models for microclimate simulation by integrating multiple geospatial open-data
5
- Author-email: Kunihiko Fujiwara <kunihiko@nus.edu.sg>
6
- Maintainer-email: Kunihiko Fujiwara <kunihiko@nus.edu.sg>
5
+ Author-email: Kunihiko Fujiwara <fujiwara.kunihiko@takenaka.co.jp>
6
+ Maintainer-email: Kunihiko Fujiwara <fujiwara.kunihiko@takenaka.co.jp>
7
7
  License: MIT
8
8
  Project-URL: bugs, https://github.com/kunifujiwara/voxcity/issues
9
9
  Project-URL: changelog, https://github.com/kunifujiwara/voxcity/blob/master/changelog.md
@@ -3,7 +3,7 @@ voxcity/generator.py,sha256=J61i6-bvgOlNQWgxlkSvOZ7CLAjRgh_XRYwslWkKxVM,55756
3
3
  voxcity/downloader/__init__.py,sha256=o_T_EU7hZLGyXxX9wVWn1x-OAa3ThGYdnpgB1_2v3AE,151
4
4
  voxcity/downloader/citygml.py,sha256=jVeHCLlJTf7k55OQGX0lZGQAngz_DD2V5TldSqRFlvc,36024
5
5
  voxcity/downloader/eubucco.py,sha256=ln1YNaaOgJfxNfCtVbYaMm775-bUvpAA_LDv60_i22w,17875
6
- voxcity/downloader/gee.py,sha256=O6HhQnUUumg_tTm4pP_cuyu5YjupDA1uKFxZWxD-i2E,23205
6
+ voxcity/downloader/gee.py,sha256=nvJvYqcSZyyontRtG2cFeb__ZJfeY4rRN1NBPORxLwQ,23557
7
7
  voxcity/downloader/mbfp.py,sha256=UXDVjsO0fnb0fSal9yqrSFEIBThnRmnutnp08kZTmCA,6595
8
8
  voxcity/downloader/oemj.py,sha256=iDacTpiqn7RAXuqyEtHP29m0Cycwta5sMy9-GdvX3Fg,12293
9
9
  voxcity/downloader/osm.py,sha256=9nOVcVE50N76F5uquJbNIFr8Xajff4ac2Uj2oSGcFrc,42591
@@ -20,19 +20,19 @@ voxcity/geoprocessor/grid.py,sha256=wrlOsX8cD0W5xCnOS5IOHy8DNqDGTM1I2270eVs787c,
20
20
  voxcity/geoprocessor/mesh.py,sha256=A7uaCMWfm82KEoYPfQYpxv6xMtQKaU2PBVDfKTpngqg,32027
21
21
  voxcity/geoprocessor/network.py,sha256=YynqR0nq_NUra_cQ3Z_56KxfRia1b6-hIzGCj3QT-wE,25137
22
22
  voxcity/geoprocessor/polygon.py,sha256=DfzXf6R-qoWXEZv1z1aHCVfr-DCuCFw6lieQT5cNHPA,61188
23
- voxcity/geoprocessor/utils.py,sha256=DVg3EMRytLQLEQeXLvNgjt1Ynoa689EsD-To-14xgSQ,30369
23
+ voxcity/geoprocessor/utils.py,sha256=s17XpgkLBelmNCk2wcUwTK1tEiFpguWR2BF_n7K17jg,31378
24
24
  voxcity/simulator/__init__.py,sha256=APdkcdaovj0v_RPOaA4SBvFUKT2RM7Hxuuz3Sux4gCo,65
25
- voxcity/simulator/solar.py,sha256=h_jVPjJ8wKl8z8Tx0EcVJBzNRKSr8WXBL9bVCgJpyZE,101788
25
+ voxcity/simulator/solar.py,sha256=4WBFgMm25-9rZ5bSGBmIpaxq2mma9X46Fom7UvGEnT8,106361
26
26
  voxcity/simulator/utils.py,sha256=sEYBB2-hLJxTiXQps1_-Fi7t1HN3-1OPOvBCWtgIisA,130
27
27
  voxcity/simulator/view.py,sha256=ClmRL0Yw3-4_jVF2c_w5KRxGWloVbkpcr9qwwL4OVJc,94258
28
28
  voxcity/utils/__init__.py,sha256=Q-NYCqYnAAaF80KuNwpqIjbE7Ec3Gr4y_khMLIMhJrg,68
29
29
  voxcity/utils/lc.py,sha256=722Gz3lPbgAp0mmTZ-g-QKBbAnbxrcgaYwb1sa7q8Sk,16189
30
30
  voxcity/utils/material.py,sha256=H8K8Lq4wBL6dQtgj7esUW2U6wLCOTeOtelkTDJoRgMo,10007
31
- voxcity/utils/visualization.py,sha256=Hk31pVOZ3q8K0QaudAMSqKUquF1Hd1Hug8626D4JJ74,115143
31
+ voxcity/utils/visualization.py,sha256=ZR9N-XKfydeSStO53IM2hGXyZJoeBiAyIMWw9Cb2MPM,116449
32
32
  voxcity/utils/weather.py,sha256=2Jtg-rIVJcsTtiKE-KuDnhIqS1-MSS16_zFRzj6zmu4,36435
33
- voxcity-0.6.3.dist-info/licenses/AUTHORS.rst,sha256=m82vkI5QokEGdcHof2OxK39lf81w1P58kG9ZNNAKS9U,175
34
- voxcity-0.6.3.dist-info/licenses/LICENSE,sha256=s_jE1Df1nTPL4A_5GCGic5Zwex0CVaPKcAmSilxJPPE,1089
35
- voxcity-0.6.3.dist-info/METADATA,sha256=S5Zctnc0Kyudy7rQxK8ziE2wYzKKDGWniUhwFhmMnOI,26723
36
- voxcity-0.6.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
37
- voxcity-0.6.3.dist-info/top_level.txt,sha256=00b2U-LKfDllt6RL1R33MXie5MvxzUFye0NGD96t_8I,8
38
- voxcity-0.6.3.dist-info/RECORD,,
33
+ voxcity-0.6.4.dist-info/licenses/AUTHORS.rst,sha256=m82vkI5QokEGdcHof2OxK39lf81w1P58kG9ZNNAKS9U,175
34
+ voxcity-0.6.4.dist-info/licenses/LICENSE,sha256=s_jE1Df1nTPL4A_5GCGic5Zwex0CVaPKcAmSilxJPPE,1089
35
+ voxcity-0.6.4.dist-info/METADATA,sha256=DXSMc39qi5ebhxnONiV2LWsPX-nCWiau4yMstqwl-jA,26749
36
+ voxcity-0.6.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
37
+ voxcity-0.6.4.dist-info/top_level.txt,sha256=00b2U-LKfDllt6RL1R33MXie5MvxzUFye0NGD96t_8I,8
38
+ voxcity-0.6.4.dist-info/RECORD,,