voxcity 0.5.30__py3-none-any.whl → 0.5.31__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/simulator/view.py CHANGED
@@ -1333,4 +1333,391 @@ def get_surface_view_factor(voxel_data, meshsize, **kwargs):
1333
1333
  except Exception as e:
1334
1334
  print(f"Error exporting mesh: {e}")
1335
1335
 
1336
- return building_mesh
1336
+ return building_mesh
1337
+
1338
+ @njit
1339
+ def trace_ray_to_landmark(voxel_data, origin, target, opaque_values, tree_k, tree_lad, meshsize):
1340
+ """Trace a ray from origin to target through voxel data with tree transmittance.
1341
+
1342
+ Uses DDA algorithm to efficiently traverse voxels along ray path.
1343
+ Checks for opaque voxels and handles tree transmittance using Beer-Lambert law.
1344
+
1345
+ Args:
1346
+ voxel_data (ndarray): 3D array of voxel values
1347
+ origin (ndarray): Starting point (x,y,z) in voxel coordinates
1348
+ target (ndarray): End point (x,y,z) in voxel coordinates
1349
+ opaque_values (ndarray): Array of voxel values that block the ray
1350
+ tree_k (float): Tree extinction coefficient for Beer-Lambert law
1351
+ tree_lad (float): Leaf area density in m^-1 for tree transmittance
1352
+ meshsize (float): Size of each voxel in meters
1353
+
1354
+ Returns:
1355
+ bool: True if target is visible from origin, False otherwise
1356
+ """
1357
+ nx, ny, nz = voxel_data.shape
1358
+ x0, y0, z0 = origin[0], origin[1], origin[2]
1359
+ x1, y1, z1 = target[0], target[1], target[2]
1360
+
1361
+ dx = x1 - x0
1362
+ dy = y1 - y0
1363
+ dz = z1 - z0
1364
+
1365
+ # Normalize direction vector
1366
+ length = np.sqrt(dx*dx + dy*dy + dz*dz)
1367
+ if length == 0.0:
1368
+ return True # Origin and target are at the same location
1369
+
1370
+ dx /= length
1371
+ dy /= length
1372
+ dz /= length
1373
+
1374
+ # Initialize ray position
1375
+ x, y, z = x0 + 0.5, y0 + 0.5, z0 + 0.5
1376
+ i, j, k = int(x0), int(y0), int(z0)
1377
+
1378
+ # Determine step direction
1379
+ step_x = 1 if dx >= 0 else -1
1380
+ step_y = 1 if dy >= 0 else -1
1381
+ step_z = 1 if dz >= 0 else -1
1382
+
1383
+ # Calculate distances to next voxel boundaries
1384
+ if dx != 0:
1385
+ t_max_x = ((i + (step_x > 0)) - x) / dx
1386
+ t_delta_x = abs(1 / dx)
1387
+ else:
1388
+ t_max_x = np.inf
1389
+ t_delta_x = np.inf
1390
+
1391
+ if dy != 0:
1392
+ t_max_y = ((j + (step_y > 0)) - y) / dy
1393
+ t_delta_y = abs(1 / dy)
1394
+ else:
1395
+ t_max_y = np.inf
1396
+ t_delta_y = np.inf
1397
+
1398
+ if dz != 0:
1399
+ t_max_z = ((k + (step_z > 0)) - z) / dz
1400
+ t_delta_z = abs(1 / dz)
1401
+ else:
1402
+ t_max_z = np.inf
1403
+ t_delta_z = np.inf
1404
+
1405
+ # Track cumulative tree transmittance
1406
+ tree_transmittance = 1.0
1407
+
1408
+ # Main ray traversal loop
1409
+ while True:
1410
+ # Check if current voxel is within bounds
1411
+ if (0 <= i < nx) and (0 <= j < ny) and (0 <= k < nz):
1412
+ voxel_value = voxel_data[i, j, k]
1413
+
1414
+ # Check for tree voxels (-2)
1415
+ if voxel_value == -2:
1416
+ # Apply Beer-Lambert law for tree transmittance
1417
+ tree_transmittance *= np.exp(-tree_k * tree_lad * meshsize)
1418
+ if tree_transmittance < 0.01: # Ray effectively blocked
1419
+ return False
1420
+ # Check for other opaque voxels
1421
+ elif voxel_value in opaque_values:
1422
+ return False # Ray is blocked
1423
+ else:
1424
+ return False # Ray went out of bounds
1425
+
1426
+ # Check if we've reached the target voxel
1427
+ if i == int(x1) and j == int(y1) and k == int(z1):
1428
+ return True # Ray successfully reached the target
1429
+
1430
+ # Move to next voxel using DDA algorithm
1431
+ if t_max_x < t_max_y:
1432
+ if t_max_x < t_max_z:
1433
+ t_max_x += t_delta_x
1434
+ i += step_x
1435
+ else:
1436
+ t_max_z += t_delta_z
1437
+ k += step_z
1438
+ else:
1439
+ if t_max_y < t_max_z:
1440
+ t_max_y += t_delta_y
1441
+ j += step_y
1442
+ else:
1443
+ t_max_z += t_delta_z
1444
+ k += step_z
1445
+
1446
+
1447
+ @njit
1448
+ def compute_face_landmark_visibility(face_center, face_normal, landmark_positions,
1449
+ voxel_data, meshsize, tree_k, tree_lad,
1450
+ grid_bounds_real, boundary_epsilon):
1451
+ """Compute binary landmark visibility for a single face.
1452
+
1453
+ Args:
1454
+ face_center (ndarray): Face centroid position in real coordinates
1455
+ face_normal (ndarray): Face normal vector (outward pointing)
1456
+ landmark_positions (ndarray): Array of landmark positions in voxel coordinates
1457
+ voxel_data (ndarray): 3D array of voxel values
1458
+ meshsize (float): Size of each voxel in meters
1459
+ tree_k (float): Tree extinction coefficient
1460
+ tree_lad (float): Leaf area density
1461
+ grid_bounds_real (ndarray): Domain bounds in real coordinates
1462
+ boundary_epsilon (float): Tolerance for boundary detection
1463
+
1464
+ Returns:
1465
+ float: 1.0 if any landmark is visible, 0.0 if none visible, np.nan for boundary faces
1466
+ """
1467
+ # Check for boundary vertical faces
1468
+ is_vertical = (abs(face_normal[2]) < 0.01)
1469
+
1470
+ on_x_min = (abs(face_center[0] - grid_bounds_real[0,0]) < boundary_epsilon)
1471
+ on_y_min = (abs(face_center[1] - grid_bounds_real[0,1]) < boundary_epsilon)
1472
+ on_x_max = (abs(face_center[0] - grid_bounds_real[1,0]) < boundary_epsilon)
1473
+ on_y_max = (abs(face_center[1] - grid_bounds_real[1,1]) < boundary_epsilon)
1474
+
1475
+ is_boundary_vertical = is_vertical and (on_x_min or on_y_min or on_x_max or on_y_max)
1476
+ if is_boundary_vertical:
1477
+ return np.nan
1478
+
1479
+ # Convert face center to voxel coordinates with small offset
1480
+ norm_n = np.sqrt(face_normal[0]**2 + face_normal[1]**2 + face_normal[2]**2)
1481
+ if norm_n < 1e-12:
1482
+ return 0.0
1483
+
1484
+ offset_vox = 0.1 # Offset in voxel units to avoid self-intersection
1485
+ ray_origin = (face_center / meshsize) + (face_normal / norm_n) * offset_vox
1486
+
1487
+ # Define opaque values (everything except empty space and landmarks)
1488
+ unique_values = np.unique(voxel_data)
1489
+ landmark_value = -30 # Standard landmark value
1490
+ opaque_values = np.array([v for v in unique_values if v != 0 and v != landmark_value], dtype=np.int32)
1491
+
1492
+ # Check visibility to each landmark
1493
+ for idx in range(landmark_positions.shape[0]):
1494
+ target = landmark_positions[idx]
1495
+
1496
+ # Check if ray direction points towards the face (not away from it)
1497
+ ray_dir = target - ray_origin
1498
+ ray_length = np.sqrt(ray_dir[0]**2 + ray_dir[1]**2 + ray_dir[2]**2)
1499
+ if ray_length > 0:
1500
+ ray_dir = ray_dir / ray_length
1501
+ dot_product = (ray_dir[0]*face_normal[0] +
1502
+ ray_dir[1]*face_normal[1] +
1503
+ ray_dir[2]*face_normal[2])
1504
+
1505
+ # Only trace ray if it points outward from the face
1506
+ if dot_product > 0:
1507
+ is_visible = trace_ray_to_landmark(voxel_data, ray_origin, target,
1508
+ opaque_values, tree_k, tree_lad, meshsize)
1509
+ if is_visible:
1510
+ return 1.0 # Return immediately when first visible landmark is found
1511
+
1512
+ return 0.0 # No landmarks were visible
1513
+
1514
+
1515
+ @njit(parallel=True)
1516
+ def compute_landmark_visibility_for_all_faces(face_centers, face_normals, landmark_positions,
1517
+ voxel_data, meshsize, tree_k, tree_lad,
1518
+ grid_bounds_real, boundary_epsilon):
1519
+ """Compute binary landmark visibility for all building surface faces.
1520
+
1521
+ Args:
1522
+ face_centers (ndarray): (n_faces, 3) face centroid positions in real coordinates
1523
+ face_normals (ndarray): (n_faces, 3) face normal vectors
1524
+ landmark_positions (ndarray): (n_landmarks, 3) landmark positions in voxel coordinates
1525
+ voxel_data (ndarray): 3D array of voxel values
1526
+ meshsize (float): Size of each voxel in meters
1527
+ tree_k (float): Tree extinction coefficient
1528
+ tree_lad (float): Leaf area density
1529
+ grid_bounds_real (ndarray): Domain bounds in real coordinates
1530
+ boundary_epsilon (float): Tolerance for boundary detection
1531
+
1532
+ Returns:
1533
+ ndarray: Binary visibility values for each face (1=visible, 0=not visible, nan=boundary)
1534
+ """
1535
+ n_faces = face_centers.shape[0]
1536
+ visibility_values = np.zeros(n_faces, dtype=np.float64)
1537
+
1538
+ # Process each face in parallel
1539
+ for fidx in prange(n_faces):
1540
+ visibility_values[fidx] = compute_face_landmark_visibility(
1541
+ face_centers[fidx],
1542
+ face_normals[fidx],
1543
+ landmark_positions,
1544
+ voxel_data,
1545
+ meshsize,
1546
+ tree_k,
1547
+ tree_lad,
1548
+ grid_bounds_real,
1549
+ boundary_epsilon
1550
+ )
1551
+
1552
+ return visibility_values
1553
+
1554
+
1555
+ def get_surface_landmark_visibility(voxel_data, building_id_grid, building_gdf, meshsize, **kwargs):
1556
+ """
1557
+ Compute binary landmark visibility for building surface meshes.
1558
+
1559
+ This function extracts building surface meshes and computes whether each face
1560
+ can see any landmark voxel. It uses direct ray tracing from face centers to
1561
+ all landmark positions, accounting for tree transmittance.
1562
+
1563
+ Args:
1564
+ voxel_data (ndarray): 3D array of voxel values
1565
+ building_id_grid (ndarray): 3D array mapping voxels to building IDs
1566
+ building_gdf (GeoDataFrame): GeoDataFrame containing building features
1567
+ meshsize (float): Size of each voxel in meters
1568
+ **kwargs: Configuration options including:
1569
+ landmark_building_ids (list): List of building IDs to mark as landmarks
1570
+ landmark_polygon (list): Polygon vertices to identify landmarks
1571
+ rectangle_vertices (list): Rectangle vertices to identify landmarks
1572
+ building_class_id (int): Voxel class for buildings (default: -3)
1573
+ tree_k (float): Tree extinction coefficient (default: 0.6)
1574
+ tree_lad (float): Leaf area density (default: 1.0)
1575
+ colormap (str): Matplotlib colormap (default: 'RdYlGn')
1576
+ obj_export (bool): Whether to export mesh as OBJ file
1577
+ output_directory (str): Directory for OBJ export
1578
+ output_file_name (str): Base filename for OBJ export
1579
+ progress_report (bool): Whether to print progress
1580
+
1581
+ Returns:
1582
+ tuple: (surface_mesh, modified_voxel_data)
1583
+ - surface_mesh: trimesh.Trimesh with binary visibility values in metadata
1584
+ - modified_voxel_data: voxel data with landmarks marked
1585
+ Returns (None, None) if no surfaces or landmarks found
1586
+ """
1587
+ import matplotlib.pyplot as plt
1588
+ import os
1589
+
1590
+ # Get landmark building IDs
1591
+ landmark_ids = kwargs.get('landmark_building_ids', None)
1592
+ landmark_polygon = kwargs.get('landmark_polygon', None)
1593
+ if landmark_ids is None:
1594
+ if landmark_polygon is not None:
1595
+ landmark_ids = get_buildings_in_drawn_polygon(building_gdf, landmark_polygon, operation='within')
1596
+ else:
1597
+ rectangle_vertices = kwargs.get("rectangle_vertices", None)
1598
+ if rectangle_vertices is None:
1599
+ print("Cannot set landmark buildings. You need to input either of rectangle_vertices or landmark_ids.")
1600
+ return None, None
1601
+
1602
+ # Calculate center point of rectangle
1603
+ lons = [coord[0] for coord in rectangle_vertices]
1604
+ lats = [coord[1] for coord in rectangle_vertices]
1605
+ center_lon = (min(lons) + max(lons)) / 2
1606
+ center_lat = (min(lats) + max(lats)) / 2
1607
+ target_point = (center_lon, center_lat)
1608
+
1609
+ # Find buildings at center point
1610
+ landmark_ids = find_building_containing_point(building_gdf, target_point)
1611
+
1612
+ # Extract configuration parameters
1613
+ building_class_id = kwargs.get("building_class_id", -3)
1614
+ landmark_value = -30
1615
+ tree_k = kwargs.get("tree_k", 0.6)
1616
+ tree_lad = kwargs.get("tree_lad", 1.0)
1617
+ colormap = kwargs.get("colormap", 'RdYlGn')
1618
+ progress_report = kwargs.get("progress_report", False)
1619
+
1620
+ # Create a copy of voxel data for modifications
1621
+ voxel_data_for_mesh = voxel_data.copy()
1622
+ voxel_data_modified = voxel_data.copy()
1623
+
1624
+ # Mark landmark buildings with special value in modified data
1625
+ voxel_data_modified = mark_building_by_id(voxel_data_modified, building_id_grid, landmark_ids, landmark_value)
1626
+
1627
+ # In the mesh extraction data, change landmark buildings to a different value
1628
+ # so they won't be included in the surface mesh extraction
1629
+ voxel_data_for_mesh = mark_building_by_id(voxel_data_for_mesh, building_id_grid, landmark_ids, 0)
1630
+
1631
+ # Find positions of all landmark voxels
1632
+ landmark_positions = np.argwhere(voxel_data_modified == landmark_value).astype(np.float64)
1633
+ if landmark_positions.shape[0] == 0:
1634
+ print(f"No landmarks found after marking buildings with IDs: {landmark_ids}")
1635
+ return None, None
1636
+
1637
+ if progress_report:
1638
+ print(f"Found {landmark_positions.shape[0]} landmark voxels")
1639
+ print(f"Landmark building IDs: {landmark_ids}")
1640
+
1641
+ # Extract building surface mesh excluding landmark buildings
1642
+ try:
1643
+ building_mesh = create_voxel_mesh(
1644
+ voxel_data_for_mesh, # Use data where landmarks are excluded
1645
+ building_class_id,
1646
+ meshsize,
1647
+ building_id_grid=building_id_grid,
1648
+ mesh_type='open_air'
1649
+ )
1650
+ if building_mesh is None or len(building_mesh.faces) == 0:
1651
+ print("No non-landmark building surfaces found in voxel data.")
1652
+ return None, None
1653
+ except Exception as e:
1654
+ print(f"Error during mesh extraction: {e}")
1655
+ return None, None
1656
+
1657
+ if progress_report:
1658
+ print(f"Processing landmark visibility for {len(building_mesh.faces)} faces...")
1659
+
1660
+ # Get mesh properties
1661
+ face_centers = building_mesh.triangles_center
1662
+ face_normals = building_mesh.face_normals
1663
+
1664
+ # Calculate domain bounds
1665
+ nx, ny, nz = voxel_data_modified.shape
1666
+ grid_bounds_voxel = np.array([[0,0,0],[nx, ny, nz]], dtype=np.float64)
1667
+ grid_bounds_real = grid_bounds_voxel * meshsize
1668
+ boundary_epsilon = meshsize * 0.05
1669
+
1670
+ # Compute binary visibility for all faces using modified voxel data
1671
+ visibility_values = compute_landmark_visibility_for_all_faces(
1672
+ face_centers,
1673
+ face_normals,
1674
+ landmark_positions,
1675
+ voxel_data_modified, # Use modified data with landmarks marked
1676
+ meshsize,
1677
+ tree_k,
1678
+ tree_lad,
1679
+ grid_bounds_real,
1680
+ boundary_epsilon
1681
+ )
1682
+
1683
+ # Store visibility values in mesh metadata
1684
+ if not hasattr(building_mesh, 'metadata'):
1685
+ building_mesh.metadata = {}
1686
+ building_mesh.metadata['landmark_visibility'] = visibility_values
1687
+
1688
+ # Count visible faces (excluding NaN boundary faces)
1689
+ valid_mask = ~np.isnan(visibility_values)
1690
+ n_valid = np.sum(valid_mask)
1691
+ n_visible = np.sum(visibility_values[valid_mask] > 0.5)
1692
+
1693
+ if progress_report:
1694
+ print(f"Landmark visibility statistics:")
1695
+ print(f" Total faces: {len(visibility_values)}")
1696
+ print(f" Valid faces: {n_valid}")
1697
+ print(f" Faces with landmark visibility: {n_visible} ({n_visible/n_valid*100:.1f}%)")
1698
+
1699
+ # Optional OBJ export
1700
+ obj_export = kwargs.get("obj_export", False)
1701
+ if obj_export:
1702
+ output_dir = kwargs.get("output_directory", "output")
1703
+ output_file_name = kwargs.get("output_file_name", "surface_landmark_visibility")
1704
+ os.makedirs(output_dir, exist_ok=True)
1705
+
1706
+ try:
1707
+ # Apply colormap to visibility values for visualization
1708
+ cmap = plt.cm.get_cmap(colormap)
1709
+ face_colors = np.zeros((len(visibility_values), 4))
1710
+
1711
+ for i, val in enumerate(visibility_values):
1712
+ if np.isnan(val):
1713
+ face_colors[i] = [0.7, 0.7, 0.7, 1.0] # Gray for boundary faces
1714
+ else:
1715
+ face_colors[i] = cmap(val)
1716
+
1717
+ building_mesh.visual.face_colors = face_colors
1718
+ building_mesh.export(f"{output_dir}/{output_file_name}.obj")
1719
+ print(f"Exported surface mesh to {output_dir}/{output_file_name}.obj")
1720
+ except Exception as e:
1721
+ print(f"Error exporting mesh: {e}")
1722
+
1723
+ return building_mesh, voxel_data_modified
@@ -301,7 +301,7 @@ def get_voxel_color_map(color_scheme='default'):
301
301
  elif color_scheme == 'grayscale':
302
302
  return {
303
303
  -99: [0, 0, 0], # void (black)
304
- -30: [255, 0, 102], # (Pink) 'Landmark',
304
+ -30: [253, 231, 37], # (Pink) 'Landmark',
305
305
  -17: [240, 240, 240], # 'plaster'
306
306
  -16: [60, 60, 60], # 'glass'
307
307
  -15: [130, 130, 130], # 'stone'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voxcity
3
- Version: 0.5.30
3
+ Version: 0.5.31
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>
@@ -24,15 +24,15 @@ voxcity/geoprocessor/utils.py,sha256=DVg3EMRytLQLEQeXLvNgjt1Ynoa689EsD-To-14xgSQ
24
24
  voxcity/simulator/__init__.py,sha256=APdkcdaovj0v_RPOaA4SBvFUKT2RM7Hxuuz3Sux4gCo,65
25
25
  voxcity/simulator/solar.py,sha256=t7zOSxBObQMuWvQSiHN9AViJstzfkXqAg3BpIppn0KA,82694
26
26
  voxcity/simulator/utils.py,sha256=sEYBB2-hLJxTiXQps1_-Fi7t1HN3-1OPOvBCWtgIisA,130
27
- voxcity/simulator/view.py,sha256=F2c-XFdRN811_RJvsznRjbUu5Uv7C6iniezsUMD-Olw,59419
27
+ voxcity/simulator/view.py,sha256=Xnr-NoKGyO9D02bF104ZKzNhsnjNCkzc-B1yPtDEqpQ,75898
28
28
  voxcity/utils/__init__.py,sha256=Q-NYCqYnAAaF80KuNwpqIjbE7Ec3Gr4y_khMLIMhJrg,68
29
29
  voxcity/utils/lc.py,sha256=h2yOWLUIrrummkyMyhRK5VbyrsPtslS0MJov_y0WGIQ,18925
30
30
  voxcity/utils/material.py,sha256=H8K8Lq4wBL6dQtgj7esUW2U6wLCOTeOtelkTDJoRgMo,10007
31
- voxcity/utils/visualization.py,sha256=T-jKrCA4UMm93p-1O678RWM7e99iE0_Lj4wD07efcwI,112918
31
+ voxcity/utils/visualization.py,sha256=OvfZ67dg3rHr1LD0xTPYK_LBtoZwjJe-H0Nhcwkd_HA,112919
32
32
  voxcity/utils/weather.py,sha256=2Jtg-rIVJcsTtiKE-KuDnhIqS1-MSS16_zFRzj6zmu4,36435
33
- voxcity-0.5.30.dist-info/licenses/AUTHORS.rst,sha256=m82vkI5QokEGdcHof2OxK39lf81w1P58kG9ZNNAKS9U,175
34
- voxcity-0.5.30.dist-info/licenses/LICENSE,sha256=s_jE1Df1nTPL4A_5GCGic5Zwex0CVaPKcAmSilxJPPE,1089
35
- voxcity-0.5.30.dist-info/METADATA,sha256=ligwB8NhRKwRVj6F7G5wcTJjpdDBHTQteZK2MpDGo2s,26724
36
- voxcity-0.5.30.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
37
- voxcity-0.5.30.dist-info/top_level.txt,sha256=00b2U-LKfDllt6RL1R33MXie5MvxzUFye0NGD96t_8I,8
38
- voxcity-0.5.30.dist-info/RECORD,,
33
+ voxcity-0.5.31.dist-info/licenses/AUTHORS.rst,sha256=m82vkI5QokEGdcHof2OxK39lf81w1P58kG9ZNNAKS9U,175
34
+ voxcity-0.5.31.dist-info/licenses/LICENSE,sha256=s_jE1Df1nTPL4A_5GCGic5Zwex0CVaPKcAmSilxJPPE,1089
35
+ voxcity-0.5.31.dist-info/METADATA,sha256=1edKOnYTIW2Io9E0QuRMhEXmEqHEZnT4-lrShCMNUvM,26724
36
+ voxcity-0.5.31.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
37
+ voxcity-0.5.31.dist-info/top_level.txt,sha256=00b2U-LKfDllt6RL1R33MXie5MvxzUFye0NGD96t_8I,8
38
+ voxcity-0.5.31.dist-info/RECORD,,