voxcity 0.6.15__py3-none-any.whl → 0.6.16__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.
- voxcity/exporter/cityles.py +7 -4
- voxcity/generator.py +1196 -1136
- voxcity/geoprocessor/grid.py +166 -1
- {voxcity-0.6.15.dist-info → voxcity-0.6.16.dist-info}/METADATA +1 -1
- {voxcity-0.6.15.dist-info → voxcity-0.6.16.dist-info}/RECORD +8 -8
- {voxcity-0.6.15.dist-info → voxcity-0.6.16.dist-info}/AUTHORS.rst +0 -0
- {voxcity-0.6.15.dist-info → voxcity-0.6.16.dist-info}/LICENSE +0 -0
- {voxcity-0.6.15.dist-info → voxcity-0.6.16.dist-info}/WHEEL +0 -0
voxcity/geoprocessor/grid.py
CHANGED
|
@@ -1565,4 +1565,169 @@ def create_dem_grid_from_gdf_polygon(terrain_gdf, mesh_size, polygon):
|
|
|
1565
1565
|
# By default, row=0 is the "north/top" row, row=-1 is "south/bottom" row.
|
|
1566
1566
|
# If you prefer the bottom row as index=0, you'd do: np.flipud(dem_grid)
|
|
1567
1567
|
|
|
1568
|
-
return np.flipud(dem_grid)
|
|
1568
|
+
return np.flipud(dem_grid)
|
|
1569
|
+
|
|
1570
|
+
def create_canopy_grids_from_tree_gdf(tree_gdf, meshsize, rectangle_vertices):
|
|
1571
|
+
"""
|
|
1572
|
+
Create canopy top and bottom height grids from a tree GeoDataFrame.
|
|
1573
|
+
|
|
1574
|
+
Assumptions:
|
|
1575
|
+
- Each tree is a point with attributes: 'top_height', 'bottom_height', 'crown_diameter'.
|
|
1576
|
+
- The crown is modeled as a solid of revolution with an ellipsoidal vertical profile.
|
|
1577
|
+
For a tree with top H_t, bottom H_b and crown radius R = crown_diameter/2,
|
|
1578
|
+
at a horizontal distance r (r <= R) from the tree center:
|
|
1579
|
+
z_top(r) = z0 + a * sqrt(1 - (r/R)^2)
|
|
1580
|
+
z_bot(r) = z0 - a * sqrt(1 - (r/R)^2)
|
|
1581
|
+
where a = (H_t - H_b)/2 and z0 = (H_t + H_b)/2.
|
|
1582
|
+
|
|
1583
|
+
The function outputs two grids (shape: (nx, ny) consistent with other grid functions):
|
|
1584
|
+
- canopy_height_grid: maximum canopy top height per cell across trees
|
|
1585
|
+
- canopy_bottom_height_grid: maximum canopy bottom height per cell across trees
|
|
1586
|
+
|
|
1587
|
+
Args:
|
|
1588
|
+
tree_gdf (geopandas.GeoDataFrame): Tree points with required columns.
|
|
1589
|
+
meshsize (float): Grid spacing in meters.
|
|
1590
|
+
rectangle_vertices (list[tuple]): 4 vertices [(lon,lat), ...] defining the grid rectangle.
|
|
1591
|
+
|
|
1592
|
+
Returns:
|
|
1593
|
+
tuple[np.ndarray, np.ndarray]: (canopy_height_grid, canopy_bottom_height_grid)
|
|
1594
|
+
"""
|
|
1595
|
+
|
|
1596
|
+
# Validate and prepare input GeoDataFrame
|
|
1597
|
+
if tree_gdf is None or len(tree_gdf) == 0:
|
|
1598
|
+
return np.array([]), np.array([])
|
|
1599
|
+
|
|
1600
|
+
required_cols = ['top_height', 'bottom_height', 'crown_diameter', 'geometry']
|
|
1601
|
+
for col in required_cols:
|
|
1602
|
+
if col not in tree_gdf.columns:
|
|
1603
|
+
raise ValueError(f"tree_gdf must contain '{col}' column.")
|
|
1604
|
+
|
|
1605
|
+
# Ensure CRS is WGS84
|
|
1606
|
+
if tree_gdf.crs is None:
|
|
1607
|
+
warnings.warn("tree_gdf has no CRS. Assuming EPSG:4326.")
|
|
1608
|
+
tree_gdf = tree_gdf.set_crs(epsg=4326)
|
|
1609
|
+
elif tree_gdf.crs.to_epsg() != 4326:
|
|
1610
|
+
tree_gdf = tree_gdf.to_crs(epsg=4326)
|
|
1611
|
+
|
|
1612
|
+
# Grid setup consistent with building/land cover grid functions
|
|
1613
|
+
geod = initialize_geod()
|
|
1614
|
+
vertex_0, vertex_1, vertex_3 = rectangle_vertices[0], rectangle_vertices[1], rectangle_vertices[3]
|
|
1615
|
+
|
|
1616
|
+
dist_side_1 = calculate_distance(geod, vertex_0[0], vertex_0[1], vertex_1[0], vertex_1[1])
|
|
1617
|
+
dist_side_2 = calculate_distance(geod, vertex_0[0], vertex_0[1], vertex_3[0], vertex_3[1])
|
|
1618
|
+
|
|
1619
|
+
side_1 = np.array(vertex_1) - np.array(vertex_0)
|
|
1620
|
+
side_2 = np.array(vertex_3) - np.array(vertex_0)
|
|
1621
|
+
u_vec = normalize_to_one_meter(side_1, dist_side_1)
|
|
1622
|
+
v_vec = normalize_to_one_meter(side_2, dist_side_2)
|
|
1623
|
+
|
|
1624
|
+
origin = np.array(rectangle_vertices[0])
|
|
1625
|
+
grid_size, adjusted_meshsize = calculate_grid_size(side_1, side_2, u_vec, v_vec, meshsize)
|
|
1626
|
+
|
|
1627
|
+
nx, ny = grid_size[0], grid_size[1]
|
|
1628
|
+
|
|
1629
|
+
# Precompute metric cell-center coordinates in grid's (u,v) metric space (meters from origin)
|
|
1630
|
+
i_centers_m = (np.arange(nx) + 0.5) * adjusted_meshsize[0]
|
|
1631
|
+
j_centers_m = (np.arange(ny) + 0.5) * adjusted_meshsize[1]
|
|
1632
|
+
|
|
1633
|
+
# Initialize output grids
|
|
1634
|
+
canopy_top = np.zeros((nx, ny), dtype=float)
|
|
1635
|
+
canopy_bottom = np.zeros((nx, ny), dtype=float)
|
|
1636
|
+
|
|
1637
|
+
# Matrix to convert lon/lat offsets to metric (u,v) using u_vec, v_vec
|
|
1638
|
+
# delta_lonlat ≈ [u_vec v_vec] @ [alpha; beta], where alpha/beta are meters along u/v
|
|
1639
|
+
transform_mat = np.column_stack((u_vec, v_vec)) # shape (2,2)
|
|
1640
|
+
try:
|
|
1641
|
+
transform_inv = np.linalg.inv(transform_mat)
|
|
1642
|
+
except np.linalg.LinAlgError:
|
|
1643
|
+
# Fallback if u_vec/v_vec are degenerate (shouldn't happen for proper rectangles)
|
|
1644
|
+
transform_inv = np.linalg.pinv(transform_mat)
|
|
1645
|
+
|
|
1646
|
+
# Iterate trees and accumulate ellipsoidal canopy surfaces
|
|
1647
|
+
for _, row in tree_gdf.iterrows():
|
|
1648
|
+
geom = row['geometry']
|
|
1649
|
+
if geom is None or not hasattr(geom, 'x'):
|
|
1650
|
+
continue
|
|
1651
|
+
|
|
1652
|
+
top_h = float(row.get('top_height', 0.0) or 0.0)
|
|
1653
|
+
bot_h = float(row.get('bottom_height', 0.0) or 0.0)
|
|
1654
|
+
dia = float(row.get('crown_diameter', 0.0) or 0.0)
|
|
1655
|
+
|
|
1656
|
+
# Sanity checks and clamps
|
|
1657
|
+
if dia <= 0 or top_h <= 0:
|
|
1658
|
+
continue
|
|
1659
|
+
if bot_h < 0:
|
|
1660
|
+
bot_h = 0.0
|
|
1661
|
+
if bot_h > top_h:
|
|
1662
|
+
top_h, bot_h = bot_h, top_h
|
|
1663
|
+
|
|
1664
|
+
R = dia / 2.0 # radius (meters)
|
|
1665
|
+
a = max((top_h - bot_h) / 2.0, 0.0)
|
|
1666
|
+
z0 = (top_h + bot_h) / 2.0
|
|
1667
|
+
if a == 0:
|
|
1668
|
+
# Flat disk between bot_h and top_h collapses; treat as constant top/bottom
|
|
1669
|
+
a = 0.0
|
|
1670
|
+
|
|
1671
|
+
# Tree center in lon/lat
|
|
1672
|
+
tree_lon = float(geom.x)
|
|
1673
|
+
tree_lat = float(geom.y)
|
|
1674
|
+
|
|
1675
|
+
# Map tree center to (u,v) metric coordinates relative to origin
|
|
1676
|
+
delta = np.array([tree_lon, tree_lat]) - origin
|
|
1677
|
+
alpha_beta = transform_inv @ delta # meters along u (alpha) and v (beta)
|
|
1678
|
+
alpha_m = alpha_beta[0]
|
|
1679
|
+
beta_m = alpha_beta[1]
|
|
1680
|
+
|
|
1681
|
+
# Determine affected index ranges (bounding box in grid indices)
|
|
1682
|
+
# Convert radius in meters to index offsets along u and v
|
|
1683
|
+
du_cells = int(R / adjusted_meshsize[0] + 2)
|
|
1684
|
+
dv_cells = int(R / adjusted_meshsize[1] + 2)
|
|
1685
|
+
|
|
1686
|
+
i_center_idx = int(alpha_m / adjusted_meshsize[0])
|
|
1687
|
+
j_center_idx = int(beta_m / adjusted_meshsize[1])
|
|
1688
|
+
|
|
1689
|
+
i_min = max(0, i_center_idx - du_cells)
|
|
1690
|
+
i_max = min(nx - 1, i_center_idx + du_cells)
|
|
1691
|
+
j_min = max(0, j_center_idx - dv_cells)
|
|
1692
|
+
j_max = min(ny - 1, j_center_idx + dv_cells)
|
|
1693
|
+
|
|
1694
|
+
if i_min > i_max or j_min > j_max:
|
|
1695
|
+
continue
|
|
1696
|
+
|
|
1697
|
+
# Slice cell center coords for local window
|
|
1698
|
+
ic = i_centers_m[i_min:i_max + 1][:, None] # shape (Ii,1)
|
|
1699
|
+
jc = j_centers_m[j_min:j_max + 1][None, :] # shape (1,Jj)
|
|
1700
|
+
|
|
1701
|
+
# Compute radial distance in meters in grid metric space
|
|
1702
|
+
di = ic - alpha_m
|
|
1703
|
+
dj = jc - beta_m
|
|
1704
|
+
r = np.sqrt(di * di + dj * dj)
|
|
1705
|
+
|
|
1706
|
+
# Mask for points within crown radius
|
|
1707
|
+
within = r <= R
|
|
1708
|
+
if not np.any(within):
|
|
1709
|
+
continue
|
|
1710
|
+
|
|
1711
|
+
# Ellipsoidal vertical profile
|
|
1712
|
+
# Avoid numerical issues for r slightly > R due to precision
|
|
1713
|
+
ratio = np.clip(r / max(R, 1e-9), 0.0, 1.0)
|
|
1714
|
+
factor = np.sqrt(1.0 - ratio * ratio)
|
|
1715
|
+
local_top = z0 + a * factor
|
|
1716
|
+
local_bot = z0 - a * factor
|
|
1717
|
+
|
|
1718
|
+
# Apply mask; cells outside remain zero contribution
|
|
1719
|
+
local_top_masked = np.where(within, local_top, 0.0)
|
|
1720
|
+
local_bot_masked = np.where(within, local_bot, 0.0)
|
|
1721
|
+
|
|
1722
|
+
# Merge with maxima to represent union of crowns
|
|
1723
|
+
canopy_top[i_min:i_max + 1, j_min:j_max + 1] = np.maximum(
|
|
1724
|
+
canopy_top[i_min:i_max + 1, j_min:j_max + 1], local_top_masked
|
|
1725
|
+
)
|
|
1726
|
+
canopy_bottom[i_min:i_max + 1, j_min:j_max + 1] = np.maximum(
|
|
1727
|
+
canopy_bottom[i_min:i_max + 1, j_min:j_max + 1], local_bot_masked
|
|
1728
|
+
)
|
|
1729
|
+
|
|
1730
|
+
# Ensure bottom <= top everywhere
|
|
1731
|
+
canopy_bottom = np.minimum(canopy_bottom, canopy_top)
|
|
1732
|
+
|
|
1733
|
+
return canopy_top, canopy_bottom
|
|
@@ -9,14 +9,14 @@ voxcity/downloader/osm.py,sha256=3vyH5jdPNe1vDXBYObLB0mtzYw7Ax8sEoF2KVn6UQO4,455
|
|
|
9
9
|
voxcity/downloader/overture.py,sha256=4YG2DMwUSSyZKUw_o8cGhMmAkPJon82aPqOFBvrre-Y,11987
|
|
10
10
|
voxcity/downloader/utils.py,sha256=tz6wt4B9BhEOyvoF5OYXlr8rUd5cBEDedWL3j__oT70,3099
|
|
11
11
|
voxcity/exporter/__init__.py,sha256=dvyWJ184Eik9tFc0VviGbzTQzZi7O0JNyrqi_n39pVI,94
|
|
12
|
-
voxcity/exporter/cityles.py,sha256=
|
|
12
|
+
voxcity/exporter/cityles.py,sha256=Kfl2PAn4WquqCdjOlyPrHysxPLaudh8QfsoC6WAXlvs,18325
|
|
13
13
|
voxcity/exporter/envimet.py,sha256=Sh7s1JdQ6SgT_L2Xd_c4gtEGWK2hTS87bccaoIqik-s,31105
|
|
14
14
|
voxcity/exporter/magicavoxel.py,sha256=SfGEgTZRlossKx3Xrv9d3iKSX-HmfQJEL9lZHgWMDX4,12782
|
|
15
15
|
voxcity/exporter/obj.py,sha256=h1_aInpemcsu96fSTwjKMqX2VZAFYbZbElWd4M1ogyI,27973
|
|
16
|
-
voxcity/generator.py,sha256=
|
|
16
|
+
voxcity/generator.py,sha256=l_oqUPwhZbQNPvEVznJ1ALC5Je3q0yFYRFa3BuF49hA,60873
|
|
17
17
|
voxcity/geoprocessor/__init__.py,sha256=WYxcAQrjGucIvGHF0JTC6rONZzL3kCms1S2vpzS6KaU,127
|
|
18
18
|
voxcity/geoprocessor/draw.py,sha256=AZMWq23wxxDJygNloCbVzWAAr1JO7nC94umf9LSxJ5o,49248
|
|
19
|
-
voxcity/geoprocessor/grid.py,sha256=
|
|
19
|
+
voxcity/geoprocessor/grid.py,sha256=eJjT86P0lOFBdPdz420r63bcH2HCA3uQEBZcPBrRt14,78184
|
|
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
|
|
@@ -30,8 +30,8 @@ voxcity/utils/lc.py,sha256=722Gz3lPbgAp0mmTZ-g-QKBbAnbxrcgaYwb1sa7q8Sk,16189
|
|
|
30
30
|
voxcity/utils/material.py,sha256=H8K8Lq4wBL6dQtgj7esUW2U6wLCOTeOtelkTDJoRgMo,10007
|
|
31
31
|
voxcity/utils/visualization.py,sha256=TQKMmATpAqSDTPMvMi1a-n2CvvzE2KcYVsss7iiShS4,116877
|
|
32
32
|
voxcity/utils/weather.py,sha256=2Jtg-rIVJcsTtiKE-KuDnhIqS1-MSS16_zFRzj6zmu4,36435
|
|
33
|
-
voxcity-0.6.
|
|
34
|
-
voxcity-0.6.
|
|
35
|
-
voxcity-0.6.
|
|
36
|
-
voxcity-0.6.
|
|
37
|
-
voxcity-0.6.
|
|
33
|
+
voxcity-0.6.16.dist-info/AUTHORS.rst,sha256=m82vkI5QokEGdcHof2OxK39lf81w1P58kG9ZNNAKS9U,175
|
|
34
|
+
voxcity-0.6.16.dist-info/LICENSE,sha256=s_jE1Df1nTPL4A_5GCGic5Zwex0CVaPKcAmSilxJPPE,1089
|
|
35
|
+
voxcity-0.6.16.dist-info/METADATA,sha256=ef5yw6coxYhtZtCeYwZAh_1f9J0Em7f01ZRnty2PN7c,26092
|
|
36
|
+
voxcity-0.6.16.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
37
|
+
voxcity-0.6.16.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|