voxcity 0.4.2__tar.gz → 0.4.3__tar.gz

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.

Files changed (53) hide show
  1. {voxcity-0.4.2 → voxcity-0.4.3}/PKG-INFO +1 -1
  2. {voxcity-0.4.2 → voxcity-0.4.3}/pyproject.toml +1 -1
  3. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity/simulator/solar.py +17 -62
  4. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity/simulator/view.py +361 -98
  5. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity.egg-info/PKG-INFO +1 -1
  6. {voxcity-0.4.2 → voxcity-0.4.3}/AUTHORS.rst +0 -0
  7. {voxcity-0.4.2 → voxcity-0.4.3}/CONTRIBUTING.rst +0 -0
  8. {voxcity-0.4.2 → voxcity-0.4.3}/HISTORY.rst +0 -0
  9. {voxcity-0.4.2 → voxcity-0.4.3}/LICENSE +0 -0
  10. {voxcity-0.4.2 → voxcity-0.4.3}/MANIFEST.in +0 -0
  11. {voxcity-0.4.2 → voxcity-0.4.3}/README.md +0 -0
  12. {voxcity-0.4.2 → voxcity-0.4.3}/docs/Makefile +0 -0
  13. {voxcity-0.4.2 → voxcity-0.4.3}/docs/archive/README.rst +0 -0
  14. {voxcity-0.4.2 → voxcity-0.4.3}/docs/authors.rst +0 -0
  15. {voxcity-0.4.2 → voxcity-0.4.3}/docs/conf.py +0 -0
  16. {voxcity-0.4.2 → voxcity-0.4.3}/docs/index.rst +0 -0
  17. {voxcity-0.4.2 → voxcity-0.4.3}/docs/make.bat +0 -0
  18. {voxcity-0.4.2 → voxcity-0.4.3}/setup.cfg +0 -0
  19. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity/__init__.py +0 -0
  20. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity/downloader/__init__.py +0 -0
  21. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity/downloader/eubucco.py +0 -0
  22. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity/downloader/gee.py +0 -0
  23. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity/downloader/mbfp.py +0 -0
  24. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity/downloader/oemj.py +0 -0
  25. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity/downloader/omt.py +0 -0
  26. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity/downloader/osm.py +0 -0
  27. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity/downloader/overture.py +0 -0
  28. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity/downloader/utils.py +0 -0
  29. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity/exporter/__init_.py +0 -0
  30. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity/exporter/envimet.py +0 -0
  31. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity/exporter/magicavoxel.py +0 -0
  32. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity/exporter/obj.py +0 -0
  33. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity/generator.py +0 -0
  34. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity/geoprocessor/__init_.py +0 -0
  35. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity/geoprocessor/draw.py +0 -0
  36. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity/geoprocessor/grid.py +0 -0
  37. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity/geoprocessor/mesh.py +0 -0
  38. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity/geoprocessor/network.py +0 -0
  39. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity/geoprocessor/polygon.py +0 -0
  40. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity/geoprocessor/utils.py +0 -0
  41. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity/simulator/__init_.py +0 -0
  42. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity/simulator/utils.py +0 -0
  43. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity/utils/__init_.py +0 -0
  44. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity/utils/lc.py +0 -0
  45. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity/utils/material.py +0 -0
  46. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity/utils/visualization.py +0 -0
  47. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity/utils/weather.py +0 -0
  48. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity.egg-info/SOURCES.txt +0 -0
  49. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity.egg-info/dependency_links.txt +0 -0
  50. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity.egg-info/requires.txt +0 -0
  51. {voxcity-0.4.2 → voxcity-0.4.3}/src/voxcity.egg-info/top_level.txt +0 -0
  52. {voxcity-0.4.2 → voxcity-0.4.3}/tests/__init__.py +0 -0
  53. {voxcity-0.4.2 → voxcity-0.4.3}/tests/voxelcity.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: voxcity
3
- Version: 0.4.2
3
+ Version: 0.4.3
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
5
  Author-email: Kunihiko Fujiwara <kunihiko@nus.edu.sg>
6
6
  Maintainer-email: Kunihiko Fujiwara <kunihiko@nus.edu.sg>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "voxcity"
3
- version = "0.4.02"
3
+ version = "0.4.03"
4
4
  requires-python = ">=3.10,<3.13"
5
5
  classifiers = [
6
6
  "Programming Language :: Python :: 3.10",
@@ -7,7 +7,7 @@ import pytz
7
7
  from astral import Observer
8
8
  from astral.sun import elevation, azimuth
9
9
 
10
- from .view import trace_ray_generic, compute_vi_map_generic, get_sky_view_factor_map, get_building_surface_svf
10
+ from .view import trace_ray_generic, compute_vi_map_generic, get_sky_view_factor_map, get_surface_view_factor
11
11
  from ..utils.weather import get_nearest_epw_from_climate_onebuilding, read_epw_for_solar_simulation
12
12
  from ..exporter.obj import grid_to_obj, export_obj
13
13
 
@@ -776,7 +776,7 @@ from numba import njit
776
776
  def compute_solar_irradiance_for_all_faces(
777
777
  face_centers,
778
778
  face_normals,
779
- face_svf_values,
779
+ face_svf,
780
780
  sun_direction,
781
781
  direct_normal_irradiance,
782
782
  diffuse_irradiance,
@@ -796,7 +796,7 @@ def compute_solar_irradiance_for_all_faces(
796
796
  Args:
797
797
  face_centers (float64[:, :]): (N x 3) array of face center points
798
798
  face_normals (float64[:, :]): (N x 3) array of face normals
799
- face_svf_values (float64[:]): (N) array of SVF values for each face
799
+ face_svf (float64[:]): (N) array of SVF values for each face
800
800
  sun_direction (float64[:]): (3) array for sun direction (dx, dy, dz)
801
801
  direct_normal_irradiance (float): Direct normal irradiance (DNI) in W/m²
802
802
  diffuse_irradiance (float): Diffuse horizontal irradiance (DHI) in W/m²
@@ -824,7 +824,7 @@ def compute_solar_irradiance_for_all_faces(
824
824
  for fidx in range(n_faces):
825
825
  center = face_centers[fidx]
826
826
  normal = face_normals[fidx]
827
- svf = face_svf_values[fidx]
827
+ svf = face_svf[fidx]
828
828
 
829
829
  # -- 1) Check for vertical boundary face
830
830
  is_vertical = (abs(normal[2]) < 0.01)
@@ -932,8 +932,8 @@ def get_building_solar_irradiance(
932
932
  # Convert angles -> direction
933
933
  az_rad = np.deg2rad(180 - azimuth_degrees)
934
934
  el_rad = np.deg2rad(elevation_degrees)
935
- sun_dx = np.cos(el_rad) * np.sin(az_rad)
936
- sun_dy = np.cos(el_rad) * np.cos(az_rad)
935
+ sun_dx = np.cos(el_rad) * np.cos(az_rad)
936
+ sun_dy = np.cos(el_rad) * np.sin(az_rad)
937
937
  sun_dz = np.sin(el_rad)
938
938
  sun_direction = np.array([sun_dx, sun_dy, sun_dz], dtype=np.float64)
939
939
 
@@ -942,10 +942,10 @@ def get_building_solar_irradiance(
942
942
  face_normals = building_svf_mesh.face_normals
943
943
 
944
944
  # Get SVF from metadata
945
- if hasattr(building_svf_mesh, 'metadata') and ('svf_values' in building_svf_mesh.metadata):
946
- face_svf_values = building_svf_mesh.metadata['svf_values']
945
+ if hasattr(building_svf_mesh, 'metadata') and ('svf' in building_svf_mesh.metadata):
946
+ face_svf = building_svf_mesh.metadata['svf']
947
947
  else:
948
- face_svf_values = np.zeros(len(building_svf_mesh.faces), dtype=np.float64)
948
+ face_svf = np.zeros(len(building_svf_mesh.faces), dtype=np.float64)
949
949
 
950
950
  # Prepare boundary checks
951
951
  grid_shape = voxel_data.shape
@@ -958,7 +958,7 @@ def get_building_solar_irradiance(
958
958
  face_direct, face_diffuse, face_global = compute_solar_irradiance_for_all_faces(
959
959
  face_centers,
960
960
  face_normals,
961
- face_svf_values,
961
+ face_svf,
962
962
  sun_direction,
963
963
  direct_normal_irradiance,
964
964
  diffuse_irradiance,
@@ -981,7 +981,7 @@ def get_building_solar_irradiance(
981
981
  irradiance_mesh.metadata = {}
982
982
 
983
983
  # Store results
984
- irradiance_mesh.metadata['svf'] = face_svf_values
984
+ irradiance_mesh.metadata['svf'] = face_svf
985
985
  irradiance_mesh.metadata['direct'] = face_direct
986
986
  irradiance_mesh.metadata['diffuse'] = face_diffuse
987
987
  irradiance_mesh.metadata['global'] = face_global
@@ -999,54 +999,6 @@ def get_building_solar_irradiance(
999
999
 
1000
1000
  return irradiance_mesh
1001
1001
 
1002
-
1003
- # ##############################################################################
1004
- # # 3) Small helper for OBJ export & color-mapping
1005
- # ##############################################################################
1006
- # def _export_solar_irradiance_mesh(mesh_obj, face_global_values, **kwargs):
1007
- # import os
1008
- # import matplotlib.cm as cm
1009
- # import matplotlib.colors as mcolors
1010
-
1011
- # output_dir = kwargs.get("output_directory", "output")
1012
- # output_file_name = kwargs.get("output_file_name", "building_solar_irradiance")
1013
- # os.makedirs(output_dir, exist_ok=True)
1014
-
1015
- # vmin = kwargs.get("vmin", 0.0)
1016
- # vmax = kwargs.get("vmax", None)
1017
- # valid_mask = ~np.isnan(face_global_values)
1018
- # if vmax is None and np.any(valid_mask):
1019
- # vmax = max(np.nanmax(face_global_values), 0.1)
1020
- # elif vmax is None:
1021
- # vmax = 1.0 # fallback
1022
-
1023
- # colormap = kwargs.get("colormap", "magma")
1024
- # cmap = cm.get_cmap(colormap)
1025
- # norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
1026
-
1027
- # face_colors = np.zeros((len(face_global_values), 4))
1028
- # nan_mask = np.isnan(face_global_values)
1029
-
1030
- # # Use 'gray' or user-specified color for NaN
1031
- # nan_color = kwargs.get("nan_color", "gray")
1032
- # if isinstance(nan_color, str):
1033
- # nan_rgba = np.array(mcolors.to_rgba(nan_color))
1034
- # else:
1035
- # nan_rgba = np.array(nan_color)
1036
-
1037
- # face_colors[valid_mask] = cmap(norm(face_global_values[valid_mask]))
1038
- # face_colors[nan_mask] = nan_rgba
1039
-
1040
- # # Apply and export
1041
- # export_mesh = mesh_obj.copy()
1042
- # export_mesh.visual.face_colors = face_colors
1043
- # try:
1044
- # export_mesh.export(f"{output_dir}/{output_file_name}.obj")
1045
- # print(f"Exported solar irradiance mesh to {output_dir}/{output_file_name}.obj")
1046
- # except Exception as e:
1047
- # print(f"Error exporting mesh: {e}")
1048
-
1049
-
1050
1002
  ##############################################################################
1051
1003
  # 4) Modified get_cumulative_building_solar_irradiance
1052
1004
  ##############################################################################
@@ -1177,8 +1129,8 @@ def get_cumulative_building_solar_irradiance(
1177
1129
  cumulative_mesh.metadata = {}
1178
1130
 
1179
1131
  # If original mesh had SVF
1180
- if 'svf_values' in building_svf_mesh.metadata:
1181
- cumulative_mesh.metadata['svf'] = building_svf_mesh.metadata['svf_values']
1132
+ if 'svf' in building_svf_mesh.metadata:
1133
+ cumulative_mesh.metadata['svf'] = building_svf_mesh.metadata['svf']
1182
1134
 
1183
1135
  cumulative_mesh.metadata['direct'] = face_cum_direct
1184
1136
  cumulative_mesh.metadata['diffuse'] = face_cum_diffuse
@@ -1285,9 +1237,12 @@ def get_building_global_solar_irradiance_using_epw(
1285
1237
 
1286
1238
  # Step 1: Calculate Sky View Factor for building surfaces
1287
1239
  print(f"Processing Sky View Factor for building surfaces...")
1288
- building_svf_mesh = get_building_surface_svf(
1240
+ building_svf_mesh = get_surface_view_factor(
1289
1241
  voxel_data, # Your 3D voxel grid
1290
1242
  meshsize, # Size of each voxel in meters
1243
+ value_name = 'svf',
1244
+ target_values = (0,),
1245
+ inclusion_mode = False,
1291
1246
  building_id_grid=building_id_grid,
1292
1247
  )
1293
1248
 
@@ -46,6 +46,7 @@ import matplotlib.pyplot as plt
46
46
  import matplotlib.patches as mpatches
47
47
  from numba import njit, prange
48
48
  import time
49
+ import trimesh
49
50
 
50
51
  from ..geoprocessor.polygon import find_building_containing_point, get_buildings_in_drawn_polygon
51
52
  from ..geoprocessor.mesh import create_voxel_mesh
@@ -1231,11 +1232,263 @@ def rotate_vector_axis_angle(vec, axis, angle):
1231
1232
  return v_rot
1232
1233
 
1233
1234
 
1234
- ##############################################################################
1235
- # 2) New Numba helper: vectorized SVF computation for each face
1236
- ##############################################################################
1235
+ # ##############################################################################
1236
+ # # 2) New Numba helper: vectorized SVF computation for each face
1237
+ # ##############################################################################
1238
+ # @njit
1239
+ # def compute_svf_for_all_faces(
1240
+ # face_centers,
1241
+ # face_normals,
1242
+ # hemisphere_dirs,
1243
+ # voxel_data,
1244
+ # meshsize,
1245
+ # tree_k,
1246
+ # tree_lad,
1247
+ # hit_values,
1248
+ # inclusion_mode,
1249
+ # grid_bounds_real,
1250
+ # boundary_epsilon
1251
+ # ):
1252
+ # """
1253
+ # Per-face SVF calculation in Numba:
1254
+ # - Checks boundary conditions & sets NaN for boundary-vertical faces
1255
+ # - Builds local hemisphere (rotates from +Z to face normal)
1256
+ # - Filters directions that actually face outward (+ dot>0) and have z>0
1257
+ # - Calls compute_vi_generic to get fraction that sees sky
1258
+ # - Returns array of SVF values (same length as face_centers)
1259
+ # """
1260
+ # n_faces = face_centers.shape[0]
1261
+ # face_svf_values = np.zeros(n_faces, dtype=np.float64)
1262
+
1263
+ # z_axis = np.array([0.0, 0.0, 1.0])
1264
+
1265
+ # for fidx in range(n_faces):
1266
+ # center = face_centers[fidx]
1267
+ # normal = face_normals[fidx]
1268
+
1269
+ # # -- 1) Check for boundary + vertical face => NaN
1270
+ # is_vertical = (abs(normal[2]) < 0.01)
1271
+
1272
+ # on_x_min = (abs(center[0] - grid_bounds_real[0,0]) < boundary_epsilon)
1273
+ # on_y_min = (abs(center[1] - grid_bounds_real[0,1]) < boundary_epsilon)
1274
+ # on_x_max = (abs(center[0] - grid_bounds_real[1,0]) < boundary_epsilon)
1275
+ # on_y_max = (abs(center[1] - grid_bounds_real[1,1]) < boundary_epsilon)
1276
+
1277
+ # is_boundary_vertical = is_vertical and (on_x_min or on_y_min or on_x_max or on_y_max)
1278
+ # if is_boundary_vertical:
1279
+ # face_svf_values[fidx] = np.nan
1280
+ # continue
1281
+
1282
+ # # -- 2) Compute rotation that aligns face normal -> +Z
1283
+ # norm_n = np.sqrt(normal[0]**2 + normal[1]**2 + normal[2]**2)
1284
+ # if norm_n < 1e-12:
1285
+ # # Degenerate normal
1286
+ # face_svf_values[fidx] = 0.0
1287
+ # continue
1288
+
1289
+ # dot_zn = z_axis[0]*normal[0] + z_axis[1]*normal[1] + z_axis[2]*normal[2]
1290
+ # cos_angle = dot_zn / (norm_n)
1291
+ # if cos_angle > 1.0: cos_angle = 1.0
1292
+ # if cos_angle < -1.0: cos_angle = -1.0
1293
+ # angle = np.arccos(cos_angle)
1294
+
1295
+ # # Distinguish near +Z vs near -Z vs general case
1296
+ # if abs(cos_angle - 1.0) < 1e-9:
1297
+ # # normal ~ +Z => no rotation
1298
+ # local_dirs = hemisphere_dirs
1299
+ # elif abs(cos_angle + 1.0) < 1e-9:
1300
+ # # normal ~ -Z => rotate 180 around X (or Y) axis
1301
+ # axis_180 = np.array([1.0, 0.0, 0.0])
1302
+ # local_dirs = np.empty_like(hemisphere_dirs)
1303
+ # for i in range(hemisphere_dirs.shape[0]):
1304
+ # local_dirs[i] = rotate_vector_axis_angle(hemisphere_dirs[i], axis_180, np.pi)
1305
+ # else:
1306
+ # # normal is neither up nor down -> do standard axis-angle
1307
+ # axis_x = z_axis[1]*normal[2] - z_axis[2]*normal[1]
1308
+ # axis_y = z_axis[2]*normal[0] - z_axis[0]*normal[2]
1309
+ # axis_z = z_axis[0]*normal[1] - z_axis[1]*normal[0]
1310
+ # rot_axis = np.array([axis_x, axis_y, axis_z], dtype=np.float64)
1311
+
1312
+ # local_dirs = np.empty_like(hemisphere_dirs)
1313
+ # for i in range(hemisphere_dirs.shape[0]):
1314
+ # local_dirs[i] = rotate_vector_axis_angle(
1315
+ # hemisphere_dirs[i],
1316
+ # rot_axis,
1317
+ # angle
1318
+ # )
1319
+
1320
+ # # -- 3) Count how many directions are outward & upward
1321
+ # total_outward = 0
1322
+ # num_upward = 0
1323
+ # for i in range(local_dirs.shape[0]):
1324
+ # dvec = local_dirs[i]
1325
+ # dp = dvec[0]*normal[0] + dvec[1]*normal[1] + dvec[2]*normal[2]
1326
+ # if dp > 0.0:
1327
+ # total_outward += 1
1328
+ # if dvec[2] > 0.0:
1329
+ # num_upward += 1
1330
+
1331
+ # # If no outward directions at all => SVF=0
1332
+ # if total_outward == 0:
1333
+ # face_svf_values[fidx] = 0.0
1334
+ # continue
1335
+
1336
+ # # If no upward directions among them => SVF=0
1337
+ # if num_upward == 0:
1338
+ # face_svf_values[fidx] = 0.0
1339
+ # continue
1340
+
1341
+ # # -- 4) Create an array for only the upward directions
1342
+ # valid_dirs_arr = np.empty((num_upward, 3), dtype=np.float64)
1343
+ # out_idx = 0
1344
+ # for i in range(local_dirs.shape[0]):
1345
+ # dvec = local_dirs[i]
1346
+ # dp = dvec[0]*normal[0] + dvec[1]*normal[1] + dvec[2]*normal[2]
1347
+ # if dp > 0.0 and dvec[2] > 0.0:
1348
+ # valid_dirs_arr[out_idx, 0] = dvec[0]
1349
+ # valid_dirs_arr[out_idx, 1] = dvec[1]
1350
+ # valid_dirs_arr[out_idx, 2] = dvec[2]
1351
+ # out_idx += 1
1352
+
1353
+ # # -- 5) Ray origin in voxel coords, offset along face normal
1354
+ # offset_vox = 0.1
1355
+ # ray_origin = (center / meshsize) + (normal / norm_n) * offset_vox
1356
+
1357
+ # # -- 6) Compute fraction of rays that see sky
1358
+ # upward_svf = compute_vi_generic(
1359
+ # ray_origin,
1360
+ # voxel_data,
1361
+ # valid_dirs_arr,
1362
+ # hit_values,
1363
+ # meshsize,
1364
+ # tree_k,
1365
+ # tree_lad,
1366
+ # inclusion_mode
1367
+ # )
1368
+
1369
+ # # Scale by fraction of directions that were outward
1370
+ # fraction_up = num_upward / total_outward
1371
+ # face_svf_values[fidx] = upward_svf * fraction_up
1372
+
1373
+ # return face_svf_values
1374
+
1375
+
1376
+ # ##############################################################################
1377
+ # # 3) Modified get_building_surface_svf (only numeric loop changed)
1378
+ # ##############################################################################
1379
+ # def get_building_surface_svf(voxel_data, meshsize, **kwargs):
1380
+ # """
1381
+ # Compute and visualize the Sky View Factor (SVF) for building surface meshes.
1382
+
1383
+ # Args:
1384
+ # voxel_data (ndarray): 3D array of voxel values.
1385
+ # meshsize (float): Size of each voxel in meters.
1386
+ # **kwargs: Additional parameters (colormap, ray counts, etc.)
1387
+
1388
+ # Returns:
1389
+ # trimesh.Trimesh: Mesh of building surfaces with SVF values stored in metadata.
1390
+ # """
1391
+ # import matplotlib.pyplot as plt
1392
+ # import matplotlib.cm as cm
1393
+ # import matplotlib.colors as mcolors
1394
+ # import os
1395
+
1396
+ # # Default parameters
1397
+ # colormap = kwargs.get("colormap", 'BuPu_r')
1398
+ # vmin = kwargs.get("vmin", 0.0)
1399
+ # vmax = kwargs.get("vmax", 1.0)
1400
+ # N_azimuth = kwargs.get("N_azimuth", 60)
1401
+ # N_elevation = kwargs.get("N_elevation", 10)
1402
+ # debug = kwargs.get("debug", False)
1403
+ # progress_report = kwargs.get("progress_report", False)
1404
+ # building_id_grid = kwargs.get("building_id_grid", None)
1405
+
1406
+ # # Tree parameters
1407
+ # tree_k = kwargs.get("tree_k", 0.6)
1408
+ # tree_lad = kwargs.get("tree_lad", 1.0)
1409
+
1410
+ # # Sky detection parameters
1411
+ # hit_values = (0,) # '0' is sky
1412
+ # inclusion_mode = False # we want rays that DON'T hit obstacles (except sky)
1413
+
1414
+ # # Building ID in voxel data
1415
+ # building_class_id = kwargs.get("building_class_id", -3)
1416
+
1417
+ # start_time = time.time()
1418
+ # # 1) Extract building mesh from voxel_data
1419
+ # try:
1420
+ # # This function is presumably in your codebase (not shown):
1421
+ # building_mesh = create_voxel_mesh(voxel_data, building_class_id, meshsize, building_id_grid=building_id_grid)
1422
+ # if building_mesh is None or len(building_mesh.faces) == 0:
1423
+ # print("No building surfaces found in voxel data.")
1424
+ # return None
1425
+ # except Exception as e:
1426
+ # print(f"Error during mesh extraction: {e}")
1427
+ # return None
1428
+
1429
+ # if progress_report:
1430
+ # print(f"Processing SVF for {len(building_mesh.faces)} building faces...")
1431
+
1432
+ # # 2) Get face centers + normals as NumPy arrays
1433
+ # face_centers = building_mesh.triangles_center
1434
+ # face_normals = building_mesh.face_normals
1435
+
1436
+ # # 3) Precompute hemisphere directions (global, pointing up)
1437
+ # azimuth_angles = np.linspace(0, 2*np.pi, N_azimuth, endpoint=False)
1438
+ # elevation_angles = np.linspace(0, np.pi/2, N_elevation)
1439
+ # hemisphere_list = []
1440
+ # for elev in elevation_angles:
1441
+ # sin_elev = np.sin(elev)
1442
+ # cos_elev = np.cos(elev)
1443
+ # for az in azimuth_angles:
1444
+ # x = cos_elev * np.cos(az)
1445
+ # y = cos_elev * np.sin(az)
1446
+ # z = sin_elev
1447
+ # hemisphere_list.append([x, y, z])
1448
+ # hemisphere_dirs = np.array(hemisphere_list, dtype=np.float64)
1449
+
1450
+ # # 4) Domain bounds in real coordinates
1451
+ # grid_shape = voxel_data.shape
1452
+ # grid_bounds_voxel = np.array([[0,0,0],[grid_shape[0],grid_shape[1],grid_shape[2]]], dtype=np.float64)
1453
+ # grid_bounds_real = grid_bounds_voxel * meshsize
1454
+ # boundary_epsilon = meshsize * 0.05
1455
+
1456
+ # # 5) Call Numba-accelerated routine
1457
+ # face_svf_values = compute_svf_for_all_faces(
1458
+ # face_centers,
1459
+ # face_normals,
1460
+ # hemisphere_dirs,
1461
+ # voxel_data,
1462
+ # meshsize,
1463
+ # tree_k,
1464
+ # tree_lad,
1465
+ # hit_values,
1466
+ # inclusion_mode,
1467
+ # grid_bounds_real,
1468
+ # boundary_epsilon
1469
+ # )
1470
+
1471
+ # # 6) Store SVF values in mesh metadata
1472
+ # if not hasattr(building_mesh, 'metadata'):
1473
+ # building_mesh.metadata = {}
1474
+ # building_mesh.metadata['svf_values'] = face_svf_values
1475
+
1476
+ # # OBJ export if desired
1477
+ # obj_export = kwargs.get("obj_export", False)
1478
+ # if obj_export:
1479
+ # output_dir = kwargs.get("output_directory", "output")
1480
+ # output_file_name = kwargs.get("output_file_name", "building_surface_svf")
1481
+ # os.makedirs(output_dir, exist_ok=True)
1482
+ # try:
1483
+ # building_mesh.export(f"{output_dir}/{output_file_name}.obj")
1484
+ # print(f"Exported building SVF mesh to {output_dir}/{output_file_name}.obj")
1485
+ # except Exception as e:
1486
+ # print(f"Error exporting mesh: {e}")
1487
+
1488
+ # return building_mesh
1489
+
1237
1490
  @njit
1238
- def compute_svf_for_all_faces(
1491
+ def compute_view_factor_for_all_faces(
1239
1492
  face_centers,
1240
1493
  face_normals,
1241
1494
  hemisphere_dirs,
@@ -1243,21 +1496,44 @@ def compute_svf_for_all_faces(
1243
1496
  meshsize,
1244
1497
  tree_k,
1245
1498
  tree_lad,
1246
- hit_values,
1499
+ target_values,
1247
1500
  inclusion_mode,
1248
1501
  grid_bounds_real,
1249
1502
  boundary_epsilon
1250
1503
  ):
1251
1504
  """
1252
- Per-face SVF calculation in Numba:
1253
- - Checks boundary conditions & sets NaN for boundary-vertical faces
1254
- - Builds local hemisphere (rotates from +Z to face normal)
1255
- - Filters directions that actually face outward (+ dot>0) and have z>0
1256
- - Calls compute_vi_generic to get fraction that sees sky
1257
- - Returns array of SVF values (same length as face_centers)
1505
+ Compute a per-face "view factor" for a specified set of target voxel classes.
1506
+
1507
+ By default (as in the old SVF case), you would pass:
1508
+ target_values = (0,) # voxel value for 'sky'
1509
+ inclusion_mode = False # i.e. any *non*-sky voxel will block the ray
1510
+
1511
+ But you can pass any other combination:
1512
+ - E.g. target_values = (-2,), inclusion_mode=True
1513
+ to measure fraction of directions that intersect 'trees' (-2).
1514
+ - E.g. target_values = (-3,), inclusion_mode=True
1515
+ to measure fraction of directions that intersect 'buildings' (-3).
1516
+
1517
+ Args:
1518
+ face_centers (np.ndarray): (n_faces, 3) face centroid positions.
1519
+ face_normals (np.ndarray): (n_faces, 3) face normals.
1520
+ hemisphere_dirs (np.ndarray): (N, 3) set of direction vectors in the hemisphere.
1521
+ voxel_data (np.ndarray): 3D array of voxel values.
1522
+ meshsize (float): Size of each voxel in meters.
1523
+ tree_k (float): Tree extinction coefficient.
1524
+ tree_lad (float): Leaf area density in m^-1.
1525
+ target_values (tuple[int]): Voxel classes that define a 'hit'.
1526
+ inclusion_mode (bool): If True, hitting any of target_values is considered "visible."
1527
+ If False, hitting anything *not* in target_values (except -2 trees) blocks the ray.
1528
+ grid_bounds_real (np.ndarray): [[x_min,y_min,z_min],[x_max,y_max,z_max]] in real coords.
1529
+ boundary_epsilon (float): tolerance for marking boundary vertical faces.
1530
+
1531
+ Returns:
1532
+ np.ndarray of shape (n_faces,):
1533
+ The computed view factor for each face (NaN for boundary‐vertical faces).
1258
1534
  """
1259
1535
  n_faces = face_centers.shape[0]
1260
- face_svf_values = np.zeros(n_faces, dtype=np.float64)
1536
+ face_vf_values = np.zeros(n_faces, dtype=np.float64)
1261
1537
 
1262
1538
  z_axis = np.array([0.0, 0.0, 1.0])
1263
1539
 
@@ -1275,14 +1551,14 @@ def compute_svf_for_all_faces(
1275
1551
 
1276
1552
  is_boundary_vertical = is_vertical and (on_x_min or on_y_min or on_x_max or on_y_max)
1277
1553
  if is_boundary_vertical:
1278
- face_svf_values[fidx] = np.nan
1554
+ face_vf_values[fidx] = np.nan
1279
1555
  continue
1280
1556
 
1281
1557
  # -- 2) Compute rotation that aligns face normal -> +Z
1282
1558
  norm_n = np.sqrt(normal[0]**2 + normal[1]**2 + normal[2]**2)
1283
1559
  if norm_n < 1e-12:
1284
1560
  # Degenerate normal
1285
- face_svf_values[fidx] = 0.0
1561
+ face_vf_values[fidx] = 0.0
1286
1562
  continue
1287
1563
 
1288
1564
  dot_zn = z_axis[0]*normal[0] + z_axis[1]*normal[1] + z_axis[2]*normal[2]
@@ -1293,7 +1569,7 @@ def compute_svf_for_all_faces(
1293
1569
 
1294
1570
  # Distinguish near +Z vs near -Z vs general case
1295
1571
  if abs(cos_angle - 1.0) < 1e-9:
1296
- # normal ~ +Z => no rotation
1572
+ # normal ~ +Z => no rotation needed
1297
1573
  local_dirs = hemisphere_dirs
1298
1574
  elif abs(cos_angle + 1.0) < 1e-9:
1299
1575
  # normal ~ -Z => rotate 180 around X (or Y) axis
@@ -1327,14 +1603,14 @@ def compute_svf_for_all_faces(
1327
1603
  if dvec[2] > 0.0:
1328
1604
  num_upward += 1
1329
1605
 
1330
- # If no outward directions at all => SVF=0
1606
+ # If no outward directions at all => view factor = 0
1331
1607
  if total_outward == 0:
1332
- face_svf_values[fidx] = 0.0
1608
+ face_vf_values[fidx] = 0.0
1333
1609
  continue
1334
1610
 
1335
- # If no upward directions among them => SVF=0
1611
+ # If no upward directions => view factor = 0
1336
1612
  if num_upward == 0:
1337
- face_svf_values[fidx] = 0.0
1613
+ face_vf_values[fidx] = 0.0
1338
1614
  continue
1339
1615
 
1340
1616
  # -- 4) Create an array for only the upward directions
@@ -1353,12 +1629,13 @@ def compute_svf_for_all_faces(
1353
1629
  offset_vox = 0.1
1354
1630
  ray_origin = (center / meshsize) + (normal / norm_n) * offset_vox
1355
1631
 
1356
- # -- 6) Compute fraction of rays that see sky
1357
- upward_svf = compute_vi_generic(
1632
+ # -- 6) Compute fraction of rays that "see" the target
1633
+ # (in the old code, "seeing the sky" meant the ray was NOT blocked by non‐sky voxels)
1634
+ upward_vf = compute_vi_generic(
1358
1635
  ray_origin,
1359
1636
  voxel_data,
1360
1637
  valid_dirs_arr,
1361
- hit_values,
1638
+ target_values,
1362
1639
  meshsize,
1363
1640
  tree_k,
1364
1641
  tree_lad,
@@ -1367,25 +1644,33 @@ def compute_svf_for_all_faces(
1367
1644
 
1368
1645
  # Scale by fraction of directions that were outward
1369
1646
  fraction_up = num_upward / total_outward
1370
- face_svf_values[fidx] = upward_svf * fraction_up
1647
+ face_vf_values[fidx] = upward_vf * fraction_up
1371
1648
 
1372
- return face_svf_values
1649
+ return face_vf_values
1373
1650
 
1374
-
1375
- ##############################################################################
1376
- # 3) Modified get_building_surface_svf (only numeric loop changed)
1377
- ##############################################################################
1378
- def get_building_surface_svf(voxel_data, meshsize, **kwargs):
1651
+ def get_surface_view_factor(voxel_data, meshsize, **kwargs):
1379
1652
  """
1380
- Compute and visualize the Sky View Factor (SVF) for building surface meshes.
1653
+ Compute and optionally visualize the "view factor" for surface meshes
1654
+ with respect to a chosen target voxel class (or classes).
1381
1655
 
1656
+ By default, it computes Sky View Factor (target_values=(0,), inclusion_mode=False).
1657
+ But you can pass different arguments for other view factors:
1658
+ - target_values=(-2,), inclusion_mode=True => Tree view factor
1659
+ - target_values=(-3,), inclusion_mode=True => Building view factor
1660
+ etc.
1661
+
1382
1662
  Args:
1383
- voxel_data (ndarray): 3D array of voxel values.
1384
- meshsize (float): Size of each voxel in meters.
1663
+ voxel_data (ndarray): 3D array of voxel values
1664
+ meshsize (float): Size of each voxel in meters
1385
1665
  **kwargs: Additional parameters (colormap, ray counts, etc.)
1666
+ including:
1667
+ target_values (tuple[int]): voxel classes that define 'hits'
1668
+ inclusion_mode (bool): interpretation of hits
1669
+ building_class_id (int): which class to mesh for surface extraction
1670
+ ...
1386
1671
 
1387
1672
  Returns:
1388
- trimesh.Trimesh: Mesh of building surfaces with SVF values stored in metadata.
1673
+ trimesh.Trimesh: The surface mesh with per-face view-factor values in metadata.
1389
1674
  """
1390
1675
  import matplotlib.pyplot as plt
1391
1676
  import matplotlib.cm as cm
@@ -1393,46 +1678,53 @@ def get_building_surface_svf(voxel_data, meshsize, **kwargs):
1393
1678
  import os
1394
1679
 
1395
1680
  # Default parameters
1396
- colormap = kwargs.get("colormap", 'BuPu_r')
1397
- vmin = kwargs.get("vmin", 0.0)
1398
- vmax = kwargs.get("vmax", 1.0)
1399
- N_azimuth = kwargs.get("N_azimuth", 60)
1400
- N_elevation = kwargs.get("N_elevation", 10)
1401
- debug = kwargs.get("debug", False)
1402
- progress_report = kwargs.get("progress_report", False)
1681
+ value_name = kwargs.get("value_name", 'view_factor_values')
1682
+ colormap = kwargs.get("colormap", 'BuPu_r')
1683
+ vmin = kwargs.get("vmin", 0.0)
1684
+ vmax = kwargs.get("vmax", 1.0)
1685
+ N_azimuth = kwargs.get("N_azimuth", 60)
1686
+ N_elevation = kwargs.get("N_elevation", 10)
1687
+ debug = kwargs.get("debug", False)
1688
+ progress_report= kwargs.get("progress_report", False)
1403
1689
  building_id_grid = kwargs.get("building_id_grid", None)
1404
1690
 
1405
- # Tree parameters
1406
- tree_k = kwargs.get("tree_k", 0.6)
1407
- tree_lad = kwargs.get("tree_lad", 1.0)
1691
+ # Tree & bounding params
1692
+ tree_k = kwargs.get("tree_k", 0.6)
1693
+ tree_lad = kwargs.get("tree_lad", 1.0)
1408
1694
 
1409
- # Sky detection parameters
1410
- hit_values = (0,) # '0' is sky
1411
- inclusion_mode = False # we want rays that DON'T hit obstacles (except sky)
1695
+ # ----------------------------------------
1696
+ # NEW: user can override target classes
1697
+ # defaults for "sky" factor:
1698
+ target_values = kwargs.get("target_values", (0,))
1699
+ inclusion_mode = kwargs.get("inclusion_mode", False)
1700
+ # ----------------------------------------
1412
1701
 
1413
- # Building ID in voxel data
1702
+ # Voxel class used for building (or other) surface
1414
1703
  building_class_id = kwargs.get("building_class_id", -3)
1415
1704
 
1416
- start_time = time.time()
1417
- # 1) Extract building mesh from voxel_data
1705
+ # 1) Extract mesh from voxel_data
1418
1706
  try:
1419
- # This function is presumably in your codebase (not shown):
1420
- building_mesh = create_voxel_mesh(voxel_data, building_class_id, meshsize, building_id_grid=building_id_grid)
1707
+ building_mesh = create_voxel_mesh(
1708
+ voxel_data,
1709
+ building_class_id,
1710
+ meshsize,
1711
+ building_id_grid=building_id_grid
1712
+ )
1421
1713
  if building_mesh is None or len(building_mesh.faces) == 0:
1422
- print("No building surfaces found in voxel data.")
1714
+ print("No surfaces found in voxel data for the specified class.")
1423
1715
  return None
1424
1716
  except Exception as e:
1425
1717
  print(f"Error during mesh extraction: {e}")
1426
1718
  return None
1427
1719
 
1428
1720
  if progress_report:
1429
- print(f"Processing SVF for {len(building_mesh.faces)} building faces...")
1430
-
1431
- # 2) Get face centers + normals as NumPy arrays
1721
+ print(f"Processing view factor for {len(building_mesh.faces)} faces...")
1722
+
1723
+ # 2) Get face centers + normals
1432
1724
  face_centers = building_mesh.triangles_center
1433
1725
  face_normals = building_mesh.face_normals
1434
1726
 
1435
- # 3) Precompute hemisphere directions (global, pointing up)
1727
+ # 3) Precompute hemisphere directions
1436
1728
  azimuth_angles = np.linspace(0, 2*np.pi, N_azimuth, endpoint=False)
1437
1729
  elevation_angles = np.linspace(0, np.pi/2, N_elevation)
1438
1730
  hemisphere_list = []
@@ -1447,13 +1739,13 @@ def get_building_surface_svf(voxel_data, meshsize, **kwargs):
1447
1739
  hemisphere_dirs = np.array(hemisphere_list, dtype=np.float64)
1448
1740
 
1449
1741
  # 4) Domain bounds in real coordinates
1450
- grid_shape = voxel_data.shape
1451
- grid_bounds_voxel = np.array([[0,0,0],[grid_shape[0],grid_shape[1],grid_shape[2]]], dtype=np.float64)
1742
+ nx, ny, nz = voxel_data.shape
1743
+ grid_bounds_voxel = np.array([[0,0,0],[nx, ny, nz]], dtype=np.float64)
1452
1744
  grid_bounds_real = grid_bounds_voxel * meshsize
1453
1745
  boundary_epsilon = meshsize * 0.05
1454
1746
 
1455
- # 5) Call Numba-accelerated routine
1456
- face_svf_values = compute_svf_for_all_faces(
1747
+ # 5) Call the new Numba routine for per-face view factor
1748
+ face_vf_values = compute_view_factor_for_all_faces(
1457
1749
  face_centers,
1458
1750
  face_normals,
1459
1751
  hemisphere_dirs,
@@ -1461,55 +1753,26 @@ def get_building_surface_svf(voxel_data, meshsize, **kwargs):
1461
1753
  meshsize,
1462
1754
  tree_k,
1463
1755
  tree_lad,
1464
- hit_values,
1465
- inclusion_mode,
1756
+ target_values, # <--- new
1757
+ inclusion_mode, # <--- new
1466
1758
  grid_bounds_real,
1467
1759
  boundary_epsilon
1468
1760
  )
1469
1761
 
1470
- # 6) Store SVF values in mesh metadata
1762
+ # 6) Store these values in the mesh metadata
1471
1763
  if not hasattr(building_mesh, 'metadata'):
1472
1764
  building_mesh.metadata = {}
1473
- building_mesh.metadata['svf_values'] = face_svf_values
1474
-
1475
- # 7) Optional: visualization & export
1476
- show_plot = kwargs.get("show_plot", False)
1477
- if show_plot:
1478
- # Replace NaN with a sentinel for color mapping
1479
- vis_values = face_svf_values.copy()
1480
- nan_mask = np.isnan(vis_values)
1481
- if np.any(nan_mask):
1482
- vis_values[nan_mask] = vmin - 0.1 # force them to min color
1483
-
1484
- cmap = cm.get_cmap(colormap)
1485
- norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
1486
- face_colors = cmap(norm(vis_values))
1487
- building_mesh.visual.face_colors = face_colors
1488
-
1489
- # Show in Trimesh viewer
1490
- scene = trimesh.Scene()
1491
- scene.add_geometry(building_mesh)
1492
- scene.show()
1493
-
1494
- # Also a histogram
1495
- valid_svf = face_svf_values[~np.isnan(face_svf_values)]
1496
- plt.figure(figsize=(8, 5))
1497
- plt.hist(valid_svf, bins=50, alpha=0.7)
1498
- plt.title("Distribution of Sky View Factor")
1499
- plt.xlabel("SVF")
1500
- plt.ylabel("Count")
1501
- plt.grid(True, alpha=0.3)
1502
- plt.show()
1503
-
1504
- # OBJ export if desired
1765
+ building_mesh.metadata[value_name] = face_vf_values
1766
+
1767
+ # Optionally export to OBJ
1505
1768
  obj_export = kwargs.get("obj_export", False)
1506
1769
  if obj_export:
1507
- output_dir = kwargs.get("output_directory", "output")
1508
- output_file_name = kwargs.get("output_file_name", "building_surface_svf")
1770
+ output_dir = kwargs.get("output_directory", "output")
1771
+ output_file_name= kwargs.get("output_file_name", "surface_view_factor")
1509
1772
  os.makedirs(output_dir, exist_ok=True)
1510
1773
  try:
1511
1774
  building_mesh.export(f"{output_dir}/{output_file_name}.obj")
1512
- print(f"Exported building SVF mesh to {output_dir}/{output_file_name}.obj")
1775
+ print(f"Exported surface mesh to {output_dir}/{output_file_name}.obj")
1513
1776
  except Exception as e:
1514
1777
  print(f"Error exporting mesh: {e}")
1515
1778
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: voxcity
3
- Version: 0.4.2
3
+ Version: 0.4.3
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
5
  Author-email: Kunihiko Fujiwara <kunihiko@nus.edu.sg>
6
6
  Maintainer-email: Kunihiko Fujiwara <kunihiko@nus.edu.sg>
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes