voxcity 0.4.1__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.
- {voxcity-0.4.1 → voxcity-0.4.3}/PKG-INFO +1 -1
- {voxcity-0.4.1 → voxcity-0.4.3}/pyproject.toml +1 -1
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity/simulator/solar.py +17 -62
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity/simulator/view.py +361 -98
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity/utils/visualization.py +23 -10
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity/utils/weather.py +98 -5
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity.egg-info/PKG-INFO +1 -1
- {voxcity-0.4.1 → voxcity-0.4.3}/AUTHORS.rst +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/CONTRIBUTING.rst +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/HISTORY.rst +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/LICENSE +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/MANIFEST.in +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/README.md +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/docs/Makefile +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/docs/archive/README.rst +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/docs/authors.rst +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/docs/conf.py +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/docs/index.rst +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/docs/make.bat +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/setup.cfg +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity/__init__.py +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity/downloader/__init__.py +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity/downloader/eubucco.py +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity/downloader/gee.py +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity/downloader/mbfp.py +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity/downloader/oemj.py +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity/downloader/omt.py +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity/downloader/osm.py +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity/downloader/overture.py +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity/downloader/utils.py +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity/exporter/__init_.py +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity/exporter/envimet.py +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity/exporter/magicavoxel.py +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity/exporter/obj.py +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity/generator.py +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity/geoprocessor/__init_.py +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity/geoprocessor/draw.py +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity/geoprocessor/grid.py +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity/geoprocessor/mesh.py +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity/geoprocessor/network.py +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity/geoprocessor/polygon.py +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity/geoprocessor/utils.py +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity/simulator/__init_.py +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity/simulator/utils.py +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity/utils/__init_.py +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity/utils/lc.py +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity/utils/material.py +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity.egg-info/SOURCES.txt +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity.egg-info/dependency_links.txt +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity.egg-info/requires.txt +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/src/voxcity.egg-info/top_level.txt +0 -0
- {voxcity-0.4.1 → voxcity-0.4.3}/tests/__init__.py +0 -0
- {voxcity-0.4.1 → 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.
|
|
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>
|
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
936
|
-
sun_dy = np.cos(el_rad) * np.
|
|
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 ('
|
|
946
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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'] =
|
|
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 '
|
|
1181
|
-
cumulative_mesh.metadata['svf'] = building_svf_mesh.metadata['
|
|
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 =
|
|
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
|
|
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
|
-
|
|
1499
|
+
target_values,
|
|
1247
1500
|
inclusion_mode,
|
|
1248
1501
|
grid_bounds_real,
|
|
1249
1502
|
boundary_epsilon
|
|
1250
1503
|
):
|
|
1251
1504
|
"""
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =>
|
|
1606
|
+
# If no outward directions at all => view factor = 0
|
|
1331
1607
|
if total_outward == 0:
|
|
1332
|
-
|
|
1608
|
+
face_vf_values[fidx] = 0.0
|
|
1333
1609
|
continue
|
|
1334
1610
|
|
|
1335
|
-
# If no upward directions
|
|
1611
|
+
# If no upward directions => view factor = 0
|
|
1336
1612
|
if num_upward == 0:
|
|
1337
|
-
|
|
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
|
|
1357
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1647
|
+
face_vf_values[fidx] = upward_vf * fraction_up
|
|
1371
1648
|
|
|
1372
|
-
return
|
|
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
|
|
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:
|
|
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
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
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
|
|
1406
|
-
tree_k
|
|
1407
|
-
tree_lad
|
|
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
|
-
#
|
|
1410
|
-
|
|
1411
|
-
|
|
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
|
-
#
|
|
1702
|
+
# Voxel class used for building (or other) surface
|
|
1414
1703
|
building_class_id = kwargs.get("building_class_id", -3)
|
|
1415
1704
|
|
|
1416
|
-
|
|
1417
|
-
# 1) Extract building mesh from voxel_data
|
|
1705
|
+
# 1) Extract mesh from voxel_data
|
|
1418
1706
|
try:
|
|
1419
|
-
|
|
1420
|
-
|
|
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
|
|
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
|
|
1430
|
-
|
|
1431
|
-
# 2) Get face centers + normals
|
|
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
|
|
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
|
-
|
|
1451
|
-
grid_bounds_voxel = np.array([[0,0,0],[
|
|
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-
|
|
1456
|
-
|
|
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
|
-
|
|
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
|
|
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[
|
|
1474
|
-
|
|
1475
|
-
#
|
|
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
|
|
1508
|
-
output_file_name
|
|
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
|
|
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
|
|
|
@@ -1811,6 +1811,18 @@ def visualize_voxcity_with_sim_meshes(voxel_array, meshsize, custom_meshes=None,
|
|
|
1811
1811
|
# Create a copy with colors
|
|
1812
1812
|
vis_mesh = custom_mesh.copy()
|
|
1813
1813
|
vis_mesh.visual.face_colors = face_colors
|
|
1814
|
+
|
|
1815
|
+
# Prepare the colormap and create colorbar
|
|
1816
|
+
norm = mcolors.Normalize(vmin=local_vmin, vmax=local_vmax)
|
|
1817
|
+
scalar_map = cm.ScalarMappable(norm=norm, cmap=cmap_name)
|
|
1818
|
+
|
|
1819
|
+
# Create a figure and axis for the colorbar but don't display
|
|
1820
|
+
fig, ax = plt.subplots(figsize=(6, 1))
|
|
1821
|
+
cbar = plt.colorbar(scalar_map, cax=ax, orientation='horizontal')
|
|
1822
|
+
if colorbar_title:
|
|
1823
|
+
cbar.set_label(colorbar_title)
|
|
1824
|
+
plt.tight_layout()
|
|
1825
|
+
plt.show()
|
|
1814
1826
|
|
|
1815
1827
|
if class_id in meshes:
|
|
1816
1828
|
print(f"Replacing voxel class {class_id} with colored custom simulation mesh")
|
|
@@ -1849,17 +1861,17 @@ def visualize_voxcity_with_sim_meshes(voxel_array, meshsize, custom_meshes=None,
|
|
|
1849
1861
|
if sim_mesh is not None:
|
|
1850
1862
|
meshes["sim_surface"] = sim_mesh
|
|
1851
1863
|
|
|
1852
|
-
# Prepare the colormap and create colorbar
|
|
1853
|
-
norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
|
|
1854
|
-
scalar_map = cm.ScalarMappable(norm=norm, cmap=cmap_name)
|
|
1864
|
+
# # Prepare the colormap and create colorbar
|
|
1865
|
+
# norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
|
|
1866
|
+
# scalar_map = cm.ScalarMappable(norm=norm, cmap=cmap_name)
|
|
1855
1867
|
|
|
1856
|
-
# Create a figure and axis for the colorbar but don't display
|
|
1857
|
-
fig, ax = plt.subplots(figsize=(6, 1))
|
|
1858
|
-
cbar = plt.colorbar(scalar_map, cax=ax, orientation='horizontal')
|
|
1859
|
-
if colorbar_title:
|
|
1860
|
-
|
|
1861
|
-
plt.tight_layout()
|
|
1862
|
-
plt.show()
|
|
1868
|
+
# # Create a figure and axis for the colorbar but don't display
|
|
1869
|
+
# fig, ax = plt.subplots(figsize=(6, 1))
|
|
1870
|
+
# cbar = plt.colorbar(scalar_map, cax=ax, orientation='horizontal')
|
|
1871
|
+
# if colorbar_title:
|
|
1872
|
+
# cbar.set_label(colorbar_title)
|
|
1873
|
+
# plt.tight_layout()
|
|
1874
|
+
# plt.show()
|
|
1863
1875
|
|
|
1864
1876
|
# Export if filename provided
|
|
1865
1877
|
if base_filename is not None:
|
|
@@ -1868,6 +1880,7 @@ def visualize_voxcity_with_sim_meshes(voxel_array, meshsize, custom_meshes=None,
|
|
|
1868
1880
|
os.makedirs(output_directory, exist_ok=True)
|
|
1869
1881
|
export_meshes(meshes, output_directory, base_filename)
|
|
1870
1882
|
|
|
1883
|
+
|
|
1871
1884
|
# Create and save multiple views
|
|
1872
1885
|
print("Creating multiple views...")
|
|
1873
1886
|
# Create output directory if it doesn't exist
|
|
@@ -164,7 +164,7 @@ def process_epw(epw_path: Union[str, Path]) -> Tuple[pd.DataFrame, Dict]:
|
|
|
164
164
|
return df, headers
|
|
165
165
|
|
|
166
166
|
def get_nearest_epw_from_climate_onebuilding(longitude: float, latitude: float, output_dir: str = "./", max_distance: Optional[float] = None,
|
|
167
|
-
extract_zip: bool = True, load_data: bool = True) -> Tuple[Optional[str], Optional[pd.DataFrame], Optional[Dict]]:
|
|
167
|
+
extract_zip: bool = True, load_data: bool = True, region: Optional[Union[str, List[str]]] = None) -> Tuple[Optional[str], Optional[pd.DataFrame], Optional[Dict]]:
|
|
168
168
|
"""
|
|
169
169
|
Download and process EPW weather file from Climate.OneBuilding.Org based on coordinates.
|
|
170
170
|
|
|
@@ -175,6 +175,8 @@ def get_nearest_epw_from_climate_onebuilding(longitude: float, latitude: float,
|
|
|
175
175
|
max_distance (float, optional): Maximum distance in kilometers to search for stations
|
|
176
176
|
extract_zip (bool): Whether to extract the ZIP file (default True)
|
|
177
177
|
load_data (bool): Whether to load the EPW data into a DataFrame (default True)
|
|
178
|
+
region (str or List[str], optional): Specific region(s) to scan for stations (e.g., "Asia", ["Europe", "Africa"])
|
|
179
|
+
If None, will auto-detect region based on coordinates.
|
|
178
180
|
|
|
179
181
|
Returns:
|
|
180
182
|
Tuple containing:
|
|
@@ -197,6 +199,66 @@ def get_nearest_epw_from_climate_onebuilding(longitude: float, latitude: float,
|
|
|
197
199
|
"Europe": "https://climate.onebuilding.org/sources/Region6_Europe_TMYx_EPW_Processing_locations.kml",
|
|
198
200
|
"Antarctica": "https://climate.onebuilding.org/sources/Region7_Antarctica_TMYx_EPW_Processing_locations.kml"
|
|
199
201
|
}
|
|
202
|
+
|
|
203
|
+
# Define approximate geographical boundaries for regions
|
|
204
|
+
REGION_BOUNDS = {
|
|
205
|
+
"Africa": {"lon_min": -20, "lon_max": 55, "lat_min": -35, "lat_max": 40},
|
|
206
|
+
"Asia": {"lon_min": 25, "lon_max": 150, "lat_min": 0, "lat_max": 55},
|
|
207
|
+
"Japan": {"lon_min": 127, "lon_max": 146, "lat_min": 24, "lat_max": 46},
|
|
208
|
+
"India": {"lon_min": 68, "lon_max": 97, "lat_min": 6, "lat_max": 36},
|
|
209
|
+
"Argentina": {"lon_min": -75, "lon_max": -53, "lat_min": -55, "lat_max": -22},
|
|
210
|
+
"Canada": {"lon_min": -141, "lon_max": -52, "lat_min": 42, "lat_max": 83},
|
|
211
|
+
"USA": {"lon_min": -170, "lon_max": -65, "lat_min": 20, "lat_max": 72},
|
|
212
|
+
"Caribbean": {"lon_min": -90, "lon_max": -59, "lat_min": 10, "lat_max": 27},
|
|
213
|
+
"Southwest_Pacific": {"lon_min": 110, "lon_max": 180, "lat_min": -50, "lat_max": 0},
|
|
214
|
+
"Europe": {"lon_min": -25, "lon_max": 40, "lat_min": 35, "lat_max": 72},
|
|
215
|
+
"Antarctica": {"lon_min": -180, "lon_max": 180, "lat_min": -90, "lat_max": -60}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
def detect_regions(lon: float, lat: float) -> List[str]:
|
|
219
|
+
"""Detect which region(s) the coordinates belong to."""
|
|
220
|
+
matching_regions = []
|
|
221
|
+
|
|
222
|
+
# Handle special case of longitude wrap around 180/-180
|
|
223
|
+
lon_adjusted = lon
|
|
224
|
+
if lon < -180:
|
|
225
|
+
lon_adjusted = lon + 360
|
|
226
|
+
elif lon > 180:
|
|
227
|
+
lon_adjusted = lon - 360
|
|
228
|
+
|
|
229
|
+
for region_name, bounds in REGION_BOUNDS.items():
|
|
230
|
+
# Check if point is within region bounds
|
|
231
|
+
if (bounds["lon_min"] <= lon_adjusted <= bounds["lon_max"] and
|
|
232
|
+
bounds["lat_min"] <= lat <= bounds["lat_max"]):
|
|
233
|
+
matching_regions.append(region_name)
|
|
234
|
+
|
|
235
|
+
# If no regions matched, check the closest regions
|
|
236
|
+
if not matching_regions:
|
|
237
|
+
# Calculate "distance" to each region's boundary (simplified)
|
|
238
|
+
region_distances = []
|
|
239
|
+
for region_name, bounds in REGION_BOUNDS.items():
|
|
240
|
+
# Calculate distance to closest edge of region bounds
|
|
241
|
+
lon_dist = 0
|
|
242
|
+
if lon_adjusted < bounds["lon_min"]:
|
|
243
|
+
lon_dist = bounds["lon_min"] - lon_adjusted
|
|
244
|
+
elif lon_adjusted > bounds["lon_max"]:
|
|
245
|
+
lon_dist = lon_adjusted - bounds["lon_max"]
|
|
246
|
+
|
|
247
|
+
lat_dist = 0
|
|
248
|
+
if lat < bounds["lat_min"]:
|
|
249
|
+
lat_dist = bounds["lat_min"] - lat
|
|
250
|
+
elif lat > bounds["lat_max"]:
|
|
251
|
+
lat_dist = lat - bounds["lat_max"]
|
|
252
|
+
|
|
253
|
+
# Simple distance metric (not actual distance)
|
|
254
|
+
distance = (lon_dist**2 + lat_dist**2)**0.5
|
|
255
|
+
region_distances.append((region_name, distance))
|
|
256
|
+
|
|
257
|
+
# Get 3 closest regions
|
|
258
|
+
closest_regions = sorted(region_distances, key=lambda x: x[1])[:3]
|
|
259
|
+
matching_regions = [r[0] for r in closest_regions]
|
|
260
|
+
|
|
261
|
+
return matching_regions
|
|
200
262
|
|
|
201
263
|
def try_decode(content: bytes) -> str:
|
|
202
264
|
"""Try different encodings to decode content."""
|
|
@@ -353,15 +415,46 @@ def get_nearest_epw_from_climate_onebuilding(longitude: float, latitude: float,
|
|
|
353
415
|
# Create output directory if it doesn't exist
|
|
354
416
|
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
|
355
417
|
|
|
356
|
-
#
|
|
418
|
+
# Determine which regions to scan
|
|
419
|
+
regions_to_scan = {}
|
|
420
|
+
if region is None:
|
|
421
|
+
# Auto-detect regions based on coordinates
|
|
422
|
+
detected_regions = detect_regions(longitude, latitude)
|
|
423
|
+
|
|
424
|
+
if detected_regions:
|
|
425
|
+
print(f"Auto-detected regions: {', '.join(detected_regions)}")
|
|
426
|
+
for r in detected_regions:
|
|
427
|
+
regions_to_scan[r] = KML_SOURCES[r]
|
|
428
|
+
else:
|
|
429
|
+
# Fallback to all regions if detection fails
|
|
430
|
+
print("Could not determine region from coordinates. Scanning all regions.")
|
|
431
|
+
regions_to_scan = KML_SOURCES
|
|
432
|
+
elif isinstance(region, str):
|
|
433
|
+
# Handle string input
|
|
434
|
+
if region.lower() == "all":
|
|
435
|
+
regions_to_scan = KML_SOURCES
|
|
436
|
+
elif region in KML_SOURCES:
|
|
437
|
+
regions_to_scan[region] = KML_SOURCES[region]
|
|
438
|
+
else:
|
|
439
|
+
valid_regions = ", ".join(KML_SOURCES.keys())
|
|
440
|
+
raise ValueError(f"Invalid region: '{region}'. Valid regions are: {valid_regions}")
|
|
441
|
+
else:
|
|
442
|
+
# Handle list input
|
|
443
|
+
for r in region:
|
|
444
|
+
if r not in KML_SOURCES:
|
|
445
|
+
valid_regions = ", ".join(KML_SOURCES.keys())
|
|
446
|
+
raise ValueError(f"Invalid region: '{r}'. Valid regions are: {valid_regions}")
|
|
447
|
+
regions_to_scan[r] = KML_SOURCES[r]
|
|
448
|
+
|
|
449
|
+
# Get stations from selected KML sources
|
|
357
450
|
print("Fetching weather station data from Climate.OneBuilding.Org...")
|
|
358
451
|
all_stations = []
|
|
359
452
|
|
|
360
|
-
for
|
|
361
|
-
print(f"Scanning {
|
|
453
|
+
for region_name, url in regions_to_scan.items():
|
|
454
|
+
print(f"Scanning {region_name}...")
|
|
362
455
|
stations = get_stations_from_kml(url)
|
|
363
456
|
all_stations.extend(stations)
|
|
364
|
-
print(f"Found {len(stations)} stations in {
|
|
457
|
+
print(f"Found {len(stations)} stations in {region_name}")
|
|
365
458
|
|
|
366
459
|
print(f"\nTotal stations found: {len(all_stations)}")
|
|
367
460
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: voxcity
|
|
3
|
-
Version: 0.4.
|
|
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
|
|
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
|
|
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
|