voxcity 0.3.4__tar.gz → 0.3.6__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 (54) hide show
  1. {voxcity-0.3.4 → voxcity-0.3.6}/PKG-INFO +36 -3
  2. {voxcity-0.3.4 → voxcity-0.3.6}/README.md +34 -2
  3. {voxcity-0.3.4 → voxcity-0.3.6}/pyproject.toml +2 -1
  4. voxcity-0.3.6/src/voxcity/geo/__init_.py +4 -0
  5. {voxcity-0.3.4 → voxcity-0.3.6}/src/voxcity/geo/grid.py +59 -2
  6. voxcity-0.3.6/src/voxcity/geo/network.py +194 -0
  7. {voxcity-0.3.4 → voxcity-0.3.6}/src/voxcity/sim/solar.py +5 -5
  8. voxcity-0.3.6/src/voxcity/utils/__init_.py +4 -0
  9. voxcity-0.3.6/src/voxcity/utils/material.py +139 -0
  10. {voxcity-0.3.4 → voxcity-0.3.6}/src/voxcity/utils/visualization.py +128 -354
  11. {voxcity-0.3.4 → voxcity-0.3.6}/src/voxcity/utils/weather.py +2 -2
  12. {voxcity-0.3.4 → voxcity-0.3.6}/src/voxcity/voxcity.py +56 -8
  13. {voxcity-0.3.4 → voxcity-0.3.6}/src/voxcity.egg-info/PKG-INFO +36 -3
  14. {voxcity-0.3.4 → voxcity-0.3.6}/src/voxcity.egg-info/SOURCES.txt +2 -0
  15. {voxcity-0.3.4 → voxcity-0.3.6}/src/voxcity.egg-info/requires.txt +1 -0
  16. voxcity-0.3.4/src/voxcity/geo/__init_.py +0 -3
  17. voxcity-0.3.4/src/voxcity/utils/__init_.py +0 -3
  18. {voxcity-0.3.4 → voxcity-0.3.6}/AUTHORS.rst +0 -0
  19. {voxcity-0.3.4 → voxcity-0.3.6}/CONTRIBUTING.rst +0 -0
  20. {voxcity-0.3.4 → voxcity-0.3.6}/HISTORY.rst +0 -0
  21. {voxcity-0.3.4 → voxcity-0.3.6}/LICENSE +0 -0
  22. {voxcity-0.3.4 → voxcity-0.3.6}/MANIFEST.in +0 -0
  23. {voxcity-0.3.4 → voxcity-0.3.6}/docs/Makefile +0 -0
  24. {voxcity-0.3.4 → voxcity-0.3.6}/docs/archive/README.rst +0 -0
  25. {voxcity-0.3.4 → voxcity-0.3.6}/docs/authors.rst +0 -0
  26. {voxcity-0.3.4 → voxcity-0.3.6}/docs/conf.py +0 -0
  27. {voxcity-0.3.4 → voxcity-0.3.6}/docs/index.rst +0 -0
  28. {voxcity-0.3.4 → voxcity-0.3.6}/docs/make.bat +0 -0
  29. {voxcity-0.3.4 → voxcity-0.3.6}/setup.cfg +0 -0
  30. {voxcity-0.3.4 → voxcity-0.3.6}/src/voxcity/__init__.py +0 -0
  31. {voxcity-0.3.4 → voxcity-0.3.6}/src/voxcity/download/__init__.py +0 -0
  32. {voxcity-0.3.4 → voxcity-0.3.6}/src/voxcity/download/eubucco.py +0 -0
  33. {voxcity-0.3.4 → voxcity-0.3.6}/src/voxcity/download/gee.py +0 -0
  34. {voxcity-0.3.4 → voxcity-0.3.6}/src/voxcity/download/mbfp.py +0 -0
  35. {voxcity-0.3.4 → voxcity-0.3.6}/src/voxcity/download/oemj.py +0 -0
  36. {voxcity-0.3.4 → voxcity-0.3.6}/src/voxcity/download/omt.py +0 -0
  37. {voxcity-0.3.4 → voxcity-0.3.6}/src/voxcity/download/osm.py +0 -0
  38. {voxcity-0.3.4 → voxcity-0.3.6}/src/voxcity/download/overture.py +0 -0
  39. {voxcity-0.3.4 → voxcity-0.3.6}/src/voxcity/download/utils.py +0 -0
  40. {voxcity-0.3.4 → voxcity-0.3.6}/src/voxcity/file/__init_.py +0 -0
  41. {voxcity-0.3.4 → voxcity-0.3.6}/src/voxcity/file/envimet.py +0 -0
  42. {voxcity-0.3.4 → voxcity-0.3.6}/src/voxcity/file/geojson.py +0 -0
  43. {voxcity-0.3.4 → voxcity-0.3.6}/src/voxcity/file/magicavoxel.py +0 -0
  44. {voxcity-0.3.4 → voxcity-0.3.6}/src/voxcity/file/obj.py +0 -0
  45. {voxcity-0.3.4 → voxcity-0.3.6}/src/voxcity/geo/draw.py +0 -0
  46. {voxcity-0.3.4 → voxcity-0.3.6}/src/voxcity/geo/utils.py +0 -0
  47. {voxcity-0.3.4 → voxcity-0.3.6}/src/voxcity/sim/__init_.py +0 -0
  48. {voxcity-0.3.4 → voxcity-0.3.6}/src/voxcity/sim/utils.py +0 -0
  49. {voxcity-0.3.4 → voxcity-0.3.6}/src/voxcity/sim/view.py +0 -0
  50. {voxcity-0.3.4 → voxcity-0.3.6}/src/voxcity/utils/lc.py +0 -0
  51. {voxcity-0.3.4 → voxcity-0.3.6}/src/voxcity.egg-info/dependency_links.txt +0 -0
  52. {voxcity-0.3.4 → voxcity-0.3.6}/src/voxcity.egg-info/top_level.txt +0 -0
  53. {voxcity-0.3.4 → voxcity-0.3.6}/tests/__init__.py +0 -0
  54. {voxcity-0.3.4 → voxcity-0.3.6}/tests/voxelcity.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: voxcity
3
- Version: 0.3.4
3
+ Version: 0.3.6
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>
@@ -48,6 +48,7 @@ Requires-Dist: overturemaps
48
48
  Requires-Dist: protobuf==3.20.3
49
49
  Requires-Dist: timezonefinder
50
50
  Requires-Dist: astral
51
+ Requires-Dist: osmnx
51
52
  Provides-Extra: dev
52
53
  Requires-Dist: coverage; extra == "dev"
53
54
  Requires-Dist: mypy; extra == "dev"
@@ -329,7 +330,7 @@ solar_kwargs = {
329
330
  }
330
331
 
331
332
  # Compute global solar irradiance map (direct + diffuse radiation)
332
- global_map = get_global_solar_irradiance_using_epw(
333
+ solar_grid = get_global_solar_irradiance_using_epw(
333
334
  voxcity_grid, # 3D voxel grid representing the urban environment
334
335
  meshsize, # Size of each voxel in meters
335
336
  calc_type='instantaneous', # Calculate instantaneous irradiance at specified time
@@ -344,7 +345,7 @@ solar_kwargs["end_time"] = "01-31 23:00:00" # End time for cumulative calculatio
344
345
  solar_kwargs["output_file_name"] = 'cummulative_solar_irradiance', # Base filename for outputs (without extension)
345
346
 
346
347
  # Calculate cumulative solar irradiance over the specified time period
347
- global_map = get_global_solar_irradiance_using_epw(
348
+ cum_solar_grid = get_global_solar_irradiance_using_epw(
348
349
  voxcity_grid, # 3D voxel grid representing the urban environment
349
350
  meshsize, # Size of each voxel in meters
350
351
  calc_type='cumulative', # Calculate cumulative irradiance over time period instead of instantaneous
@@ -417,6 +418,38 @@ landmark_vis_map = get_landmark_visibility_map(voxcity_grid, building_id_grid, b
417
418
  <em>Example Result Saved as OBJ and Rendered in Rhino</em>
418
419
  </p>
419
420
 
421
+ #### Network Analysis:
422
+
423
+ ```python
424
+ from voxcity.geo.network import get_network_values
425
+
426
+ network_kwargs = {
427
+ "network_type": "walk", # Type of network to download from OSM (walk, drive, all, etc.)
428
+ "colormap": "magma", # Matplotlib colormap for visualization
429
+ "vis_graph": True, # Whether to display the network visualization
430
+ "vmin": 0.0, # Minimum value for color scaling
431
+ "vmax": 600000, # Maximum value for color scaling
432
+ "edge_width": 2, # Width of network edges in visualization
433
+ "alpha": 0.8, # Transparency of network edges
434
+ "zoom": 16 # Zoom level for basemap
435
+ }
436
+
437
+ G, edge_gdf = get_network_values(
438
+ cum_solar_grid, # Grid of cumulative solar irradiance values
439
+ rectangle_vertices, # Coordinates defining simulation domain boundary
440
+ meshsize, # Size of each grid cell in meters
441
+ value_name='Cumulative Global Solar Irradiance (W/m²·hour)', # Label for values in visualization
442
+ **network_kwargs # Additional visualization and network parameters
443
+ )
444
+ ```
445
+
446
+ <p align="center">
447
+ <img src="https://raw.githubusercontent.com/kunifujiwara/VoxCity/main/images/network.png" alt="Example of Graph Output" width="500">
448
+ </p>
449
+ <p align="center">
450
+ <em>Example Result Saved as OBJ and Rendered in Rhino</em>
451
+ </p>
452
+
420
453
  ## References of Data Sources
421
454
 
422
455
  ### Building
@@ -273,7 +273,7 @@ solar_kwargs = {
273
273
  }
274
274
 
275
275
  # Compute global solar irradiance map (direct + diffuse radiation)
276
- global_map = get_global_solar_irradiance_using_epw(
276
+ solar_grid = get_global_solar_irradiance_using_epw(
277
277
  voxcity_grid, # 3D voxel grid representing the urban environment
278
278
  meshsize, # Size of each voxel in meters
279
279
  calc_type='instantaneous', # Calculate instantaneous irradiance at specified time
@@ -288,7 +288,7 @@ solar_kwargs["end_time"] = "01-31 23:00:00" # End time for cumulative calculatio
288
288
  solar_kwargs["output_file_name"] = 'cummulative_solar_irradiance', # Base filename for outputs (without extension)
289
289
 
290
290
  # Calculate cumulative solar irradiance over the specified time period
291
- global_map = get_global_solar_irradiance_using_epw(
291
+ cum_solar_grid = get_global_solar_irradiance_using_epw(
292
292
  voxcity_grid, # 3D voxel grid representing the urban environment
293
293
  meshsize, # Size of each voxel in meters
294
294
  calc_type='cumulative', # Calculate cumulative irradiance over time period instead of instantaneous
@@ -361,6 +361,38 @@ landmark_vis_map = get_landmark_visibility_map(voxcity_grid, building_id_grid, b
361
361
  <em>Example Result Saved as OBJ and Rendered in Rhino</em>
362
362
  </p>
363
363
 
364
+ #### Network Analysis:
365
+
366
+ ```python
367
+ from voxcity.geo.network import get_network_values
368
+
369
+ network_kwargs = {
370
+ "network_type": "walk", # Type of network to download from OSM (walk, drive, all, etc.)
371
+ "colormap": "magma", # Matplotlib colormap for visualization
372
+ "vis_graph": True, # Whether to display the network visualization
373
+ "vmin": 0.0, # Minimum value for color scaling
374
+ "vmax": 600000, # Maximum value for color scaling
375
+ "edge_width": 2, # Width of network edges in visualization
376
+ "alpha": 0.8, # Transparency of network edges
377
+ "zoom": 16 # Zoom level for basemap
378
+ }
379
+
380
+ G, edge_gdf = get_network_values(
381
+ cum_solar_grid, # Grid of cumulative solar irradiance values
382
+ rectangle_vertices, # Coordinates defining simulation domain boundary
383
+ meshsize, # Size of each grid cell in meters
384
+ value_name='Cumulative Global Solar Irradiance (W/m²·hour)', # Label for values in visualization
385
+ **network_kwargs # Additional visualization and network parameters
386
+ )
387
+ ```
388
+
389
+ <p align="center">
390
+ <img src="https://raw.githubusercontent.com/kunifujiwara/VoxCity/main/images/network.png" alt="Example of Graph Output" width="500">
391
+ </p>
392
+ <p align="center">
393
+ <em>Example Result Saved as OBJ and Rendered in Rhino</em>
394
+ </p>
395
+
364
396
  ## References of Data Sources
365
397
 
366
398
  ### Building
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "voxcity"
3
- version = "0.3.4"
3
+ version = "0.3.6"
4
4
  requires-python = ">=3.10,<3.13"
5
5
  classifiers = [
6
6
  "Programming Language :: Python :: 3.10",
@@ -50,6 +50,7 @@ dependencies = [
50
50
  "protobuf==3.20.3",
51
51
  "timezonefinder",
52
52
  "astral",
53
+ "osmnx",
53
54
  ]
54
55
 
55
56
  [project.optional-dependencies]
@@ -0,0 +1,4 @@
1
+ from .draw import *
2
+ from .grid import *
3
+ from .utils import *
4
+ from .network import *
@@ -4,7 +4,7 @@ This module provides functions for creating and manipulating grids of building h
4
4
 
5
5
  import numpy as np
6
6
  import os
7
- from shapely.geometry import Polygon
7
+ from shapely.geometry import Polygon, box
8
8
  from scipy.ndimage import label, generate_binary_structure
9
9
  from pyproj import Geod, Transformer, CRS
10
10
  import rasterio
@@ -12,6 +12,7 @@ from affine import Affine
12
12
  from shapely.geometry import box
13
13
  from scipy.interpolate import griddata
14
14
  from shapely.errors import GEOSException
15
+ import geopandas as gpd
15
16
 
16
17
  from .utils import (
17
18
  initialize_geod,
@@ -748,4 +749,60 @@ def create_dem_grid_from_geotiff_polygon(tiff_path, mesh_size, rectangle_vertice
748
749
  # Use nearest neighbor interpolation for raw data
749
750
  grid = griddata(points, values, (xx, yy), method='nearest')
750
751
 
751
- return np.flipud(grid)
752
+ return np.flipud(grid)
753
+
754
+ def grid_to_geodataframe(grid_ori, rectangle_vertices, meshsize):
755
+ """Converts a 2D grid to a GeoDataFrame with cell polygons and values.
756
+
757
+ Args:
758
+ grid: 2D numpy array containing grid values
759
+ rectangle_vertices: List of [lon, lat] coordinates defining area corners
760
+ meshsize: Size of each grid cell in meters
761
+
762
+ Returns:
763
+ GeoDataFrame with columns:
764
+ - geometry: Polygon geometry of each grid cell
765
+ - value: Value from the grid
766
+ """
767
+ grid = np.flipud(grid_ori.copy())
768
+
769
+ # Extract bounds from rectangle vertices
770
+ min_lon = min(v[0] for v in rectangle_vertices)
771
+ max_lon = max(v[0] for v in rectangle_vertices)
772
+ min_lat = min(v[1] for v in rectangle_vertices)
773
+ max_lat = max(v[1] for v in rectangle_vertices)
774
+
775
+ rows, cols = grid.shape
776
+
777
+ # Calculate cell sizes in degrees (approximate)
778
+ # 111,111 meters = 1 degree at equator
779
+ cell_size_lon = meshsize / (111111 * np.cos(np.mean([min_lat, max_lat]) * np.pi / 180))
780
+ cell_size_lat = meshsize / 111111
781
+
782
+ # Create lists to store data
783
+ polygons = []
784
+ values = []
785
+
786
+ # Create grid cells
787
+ for i in range(rows):
788
+ for j in range(cols):
789
+ # Calculate cell bounds
790
+ cell_min_lon = min_lon + j * cell_size_lon
791
+ cell_max_lon = min_lon + (j + 1) * cell_size_lon
792
+ # Flip vertical axis since grid is stored with origin at top-left
793
+ cell_min_lat = max_lat - (i + 1) * cell_size_lat
794
+ cell_max_lat = max_lat - i * cell_size_lat
795
+
796
+ # Create polygon for cell
797
+ cell_poly = box(cell_min_lon, cell_min_lat, cell_max_lon, cell_max_lat)
798
+
799
+ polygons.append(cell_poly)
800
+ values.append(grid[i, j])
801
+
802
+ # Create GeoDataFrame
803
+ gdf = gpd.GeoDataFrame({
804
+ 'geometry': polygons,
805
+ 'value': values
806
+ }, crs=CRS.from_epsg(4326))
807
+
808
+ return gdf
@@ -0,0 +1,194 @@
1
+ import contextily as ctx
2
+ import matplotlib.pyplot as plt
3
+ import numpy as np
4
+ import pandas as pd
5
+ import geopandas as gpd
6
+ from shapely.geometry import LineString
7
+ import networkx as nx
8
+ import osmnx as ox
9
+
10
+ from .grid import grid_to_geodataframe
11
+
12
+ def calculate_edge_values(G, gdf, value_col='value'):
13
+ """
14
+ Calculate average values for graph edges based on intersection with polygons.
15
+
16
+ Parameters:
17
+ -----------
18
+ G : NetworkX Graph
19
+ Input graph with edges to analyze
20
+ gdf : GeoDataFrame
21
+ Grid containing polygons with values
22
+ value_col : str, default 'value'
23
+ Name of the column containing values in the grid
24
+
25
+ Returns:
26
+ --------
27
+ dict
28
+ Dictionary with edge identifiers (u,v,k) as keys and average values as values
29
+ """
30
+ edge_values = {}
31
+ for u, v, k, data in G.edges(data=True, keys=True):
32
+ if 'geometry' in data:
33
+ edge_line = data['geometry']
34
+ else:
35
+ start_node = G.nodes[u]
36
+ end_node = G.nodes[v]
37
+ edge_line = LineString([(start_node['x'], start_node['y']),
38
+ (end_node['x'], end_node['y'])])
39
+
40
+ intersecting_polys = gdf[gdf.geometry.intersects(edge_line)]
41
+
42
+ if len(intersecting_polys) > 0:
43
+ total_length = 0
44
+ weighted_sum = 0
45
+
46
+ for idx, poly in intersecting_polys.iterrows():
47
+ if pd.isna(poly[value_col]):
48
+ continue
49
+
50
+ intersection = edge_line.intersection(poly.geometry)
51
+ if not intersection.is_empty:
52
+ length = intersection.length
53
+ total_length += length
54
+ weighted_sum += length * poly[value_col]
55
+
56
+ if total_length > 0:
57
+ avg_value = weighted_sum / total_length
58
+ edge_values[(u, v, k)] = avg_value
59
+ else:
60
+ edge_values[(u, v, k)] = np.nan
61
+ else:
62
+ edge_values[(u, v, k)] = np.nan
63
+
64
+ return edge_values
65
+
66
+ def get_network_values(grid, rectangle_vertices, meshsize, value_name='value', **kwargs):
67
+ """
68
+ Analyze and visualize network values based on grid intersections.
69
+
70
+ Parameters:
71
+ -----------
72
+ grid : GeoDataFrame
73
+ Input grid with geometries and values
74
+ rectangle_vertices : list
75
+ List of coordinates defining the bounding box vertices
76
+ meshsize : float
77
+ Size of the mesh grid
78
+ value_name : str, default 'value'
79
+ Name of the column containing values in the grid
80
+ **kwargs : dict
81
+ Optional arguments including:
82
+ - network_type : str, default 'walk'
83
+ Type of network to download ('walk', 'drive', 'all', etc.)
84
+ - vis_graph : bool, default True
85
+ Whether to visualize the graph
86
+ - colormap : str, default 'viridis'
87
+ Matplotlib colormap name for visualization
88
+ - vmin : float, optional
89
+ Minimum value for color scaling
90
+ - vmax : float, optional
91
+ Maximum value for color scaling
92
+ - edge_width : float, default 1
93
+ Width of the edges in visualization
94
+ - fig_size : tuple, default (15,15)
95
+ Figure size for visualization
96
+ - zoom : int, default 16
97
+ Zoom level for the basemap
98
+ - basemap_style : ctx.providers, default CartoDB.Positron
99
+ Contextily basemap provider
100
+ - save_path : str, optional
101
+ Path to save the output GeoPackage
102
+
103
+ Returns:
104
+ --------
105
+ tuple : (NetworkX Graph, GeoDataFrame)
106
+ Returns the processed graph and edge GeoDataFrame
107
+ """
108
+ # Set default values for optional arguments
109
+ defaults = {
110
+ 'network_type': 'walk',
111
+ 'vis_graph': True,
112
+ 'colormap': 'viridis',
113
+ 'vmin': None,
114
+ 'vmax': None,
115
+ 'edge_width': 1,
116
+ 'fig_size': (15,15),
117
+ 'zoom': 16,
118
+ 'basemap_style': ctx.providers.CartoDB.Positron,
119
+ 'save_path': None
120
+ }
121
+
122
+ # Update defaults with provided kwargs
123
+ settings = defaults.copy()
124
+ settings.update(kwargs)
125
+
126
+ grid_gdf = grid_to_geodataframe(grid, rectangle_vertices, meshsize)
127
+
128
+ # Extract bounding box coordinates
129
+ north, south = rectangle_vertices[1][1], rectangle_vertices[0][1]
130
+ east, west = rectangle_vertices[2][0], rectangle_vertices[0][0]
131
+ bbox = (west, south, east, north)
132
+
133
+ # Download the road network
134
+ G = ox.graph.graph_from_bbox(bbox=bbox, network_type=settings['network_type'], simplify=True)
135
+
136
+ # Calculate edge values using the separate function
137
+ edge_values = calculate_edge_values(G, grid_gdf, "value")
138
+
139
+ # Add values to the graph
140
+ nx.set_edge_attributes(G, edge_values, value_name)
141
+
142
+ # Create GeoDataFrame from edges
143
+ edges_with_values = []
144
+ for u, v, k, data in G.edges(data=True, keys=True):
145
+ if 'geometry' in data:
146
+ edge_line = data['geometry']
147
+ else:
148
+ start_node = G.nodes[u]
149
+ end_node = G.nodes[v]
150
+ edge_line = LineString([(start_node['x'], start_node['y']),
151
+ (end_node['x'], end_node['y'])])
152
+
153
+ edges_with_values.append({
154
+ 'geometry': edge_line,
155
+ value_name: data.get(value_name, np.nan),
156
+ 'u': u,
157
+ 'v': v,
158
+ 'key': k
159
+ })
160
+
161
+ edge_gdf = gpd.GeoDataFrame(edges_with_values)
162
+
163
+ # Set CRS and save if requested
164
+ if edge_gdf.crs is None:
165
+ edge_gdf.set_crs(epsg=4326, inplace=True)
166
+
167
+ if settings['save_path']:
168
+ edge_gdf.to_file(settings['save_path'], driver="GPKG")
169
+
170
+ # Visualize if requested
171
+ if settings['vis_graph']:
172
+ edge_gdf_web = edge_gdf.to_crs(epsg=3857)
173
+
174
+ fig, ax = plt.subplots(figsize=settings['fig_size'])
175
+
176
+ plot = edge_gdf_web.plot(column=value_name,
177
+ ax=ax,
178
+ cmap=settings['colormap'],
179
+ legend=True,
180
+ vmin=settings['vmin'],
181
+ vmax=settings['vmax'],
182
+ linewidth=settings['edge_width'],
183
+ legend_kwds={'label': value_name,
184
+ 'shrink': 0.5}) # Make colorbar 50% smaller
185
+
186
+ ctx.add_basemap(ax,
187
+ source=settings['basemap_style'],
188
+ zoom=settings['zoom'])
189
+
190
+ ax.set_axis_off()
191
+ # plt.title(f'Network {value_name} Analysis', pad=20)
192
+ plt.show()
193
+
194
+ return G, edge_gdf
@@ -675,11 +675,11 @@ def get_global_solar_irradiance_using_epw(
675
675
  return None
676
676
  else:
677
677
  # Calculate center point of rectangle
678
- lats = [coord[0] for coord in rectangle_vertices]
679
- lons = [coord[1] for coord in rectangle_vertices]
680
- center_lat = (min(lats) + max(lats)) / 2
678
+ lons = [coord[0] for coord in rectangle_vertices]
679
+ lats = [coord[1] for coord in rectangle_vertices]
681
680
  center_lon = (min(lons) + max(lons)) / 2
682
- target_point = (center_lat, center_lon)
681
+ center_lat = (min(lats) + max(lats)) / 2
682
+ target_point = (center_lon, center_lat)
683
683
 
684
684
  # Optional: specify maximum distance in kilometers
685
685
  max_distance = 100 # None for no limit
@@ -687,8 +687,8 @@ def get_global_solar_irradiance_using_epw(
687
687
  output_dir = kwargs.get("output_dir", "output")
688
688
 
689
689
  epw_file_path, weather_data, metadata = get_nearest_epw_from_climate_onebuilding(
690
- latitude=center_lat,
691
690
  longitude=center_lon,
691
+ latitude=center_lat,
692
692
  output_dir=output_dir,
693
693
  max_distance=max_distance,
694
694
  extract_zip=True,
@@ -0,0 +1,4 @@
1
+ from .visualization import *
2
+ from .lc import *
3
+ from .weather import *
4
+ from .material import *
@@ -0,0 +1,139 @@
1
+ import numpy as np
2
+
3
+ def get_material_dict():
4
+ """
5
+ Returns a dictionary mapping material names to their corresponding ID values.
6
+ """
7
+ return {
8
+ "unknown": -3,
9
+ "brick": -11,
10
+ "wood": -12,
11
+ "concrete": -13,
12
+ "metal": -14,
13
+ "stone": -15,
14
+ "glass": -16,
15
+ "plaster": -17,
16
+ }
17
+
18
+ def get_modulo_numbers(window_ratio):
19
+ """
20
+ Determines the appropriate modulo numbers for x, y, z based on window_ratio.
21
+
22
+ Parameters:
23
+ window_ratio: float between 0 and 1.0
24
+
25
+ Returns:
26
+ tuple (x_mod, y_mod, z_mod): modulo numbers for each dimension
27
+ """
28
+ if window_ratio <= 0.125 + 0.0625: # around 0.125
29
+ return (2, 2, 2)
30
+ elif window_ratio <= 0.25 + 0.125: # around 0.25
31
+ combinations = [(2, 2, 1), (2, 1, 2), (1, 2, 2)]
32
+ return combinations[hash(str(window_ratio)) % len(combinations)]
33
+ elif window_ratio <= 0.5 + 0.125: # around 0.5
34
+ combinations = [(2, 1, 1), (1, 2, 1), (1, 1, 2)]
35
+ return combinations[hash(str(window_ratio)) % len(combinations)]
36
+ elif window_ratio <= 0.75 + 0.125: # around 0.75
37
+ combinations = [(2, 1, 1), (1, 2, 1), (1, 1, 2)]
38
+ return combinations[hash(str(window_ratio)) % len(combinations)]
39
+ else: # above 0.875
40
+ return (1, 1, 1)
41
+
42
+ def set_building_material_by_id(voxelcity_grid, building_id_grid_ori, ids, mark, window_ratio=0.125, glass_id=-16):
43
+ """
44
+ Marks cells in voxelcity_grid based on building IDs and window ratio.
45
+ Never sets glass_id to cells with maximum z index.
46
+
47
+ Parameters:
48
+ voxelcity_grid: 3D numpy array
49
+ building_id_grid_ori: 2D numpy array containing building IDs
50
+ ids: list/array of building IDs to check
51
+ mark: value to set for marked cells
52
+ window_ratio: float between 0 and 1.0, determines window density:
53
+ ~0.125: sparse windows (2,2,2)
54
+ ~0.25: medium-sparse windows (2,2,1), (2,1,2), or (1,2,2)
55
+ ~0.5: medium windows (2,1,1), (1,2,1), or (1,1,2)
56
+ ~0.75: dense windows (2,1,1), (1,2,1), or (1,1,2)
57
+ >0.875: maximum density (1,1,1)
58
+ glass_id: value to set for glass cells (default: -16)
59
+
60
+ Returns:
61
+ Modified voxelcity_grid
62
+ """
63
+ building_id_grid = np.flipud(building_id_grid_ori.copy())
64
+
65
+ # Get modulo numbers based on window_ratio
66
+ x_mod, y_mod, z_mod = get_modulo_numbers(window_ratio)
67
+
68
+ # Get positions where building IDs match
69
+ building_positions = np.where(np.isin(building_id_grid, ids))
70
+
71
+ # Loop through each position that matches building IDs
72
+ for i in range(len(building_positions[0])):
73
+ x, y = building_positions[0][i], building_positions[1][i]
74
+ z_mask = voxelcity_grid[x, y, :] == -3
75
+ voxelcity_grid[x, y, z_mask] = mark
76
+
77
+ # Check if x and y meet the modulo conditions
78
+ if x % x_mod == 0 and y % y_mod == 0:
79
+ z_mask = voxelcity_grid[x, y, :] == mark
80
+ if np.any(z_mask):
81
+ # Find the maximum z index where z_mask is True
82
+ z_indices = np.where(z_mask)[0]
83
+ max_z_index = np.max(z_indices)
84
+
85
+ # Create base mask excluding maximum z index
86
+ base_mask = z_mask.copy()
87
+ base_mask[max_z_index] = False
88
+
89
+ # Create pattern mask based on z modulo
90
+ pattern_mask = np.zeros_like(z_mask)
91
+ valid_z_indices = z_indices[z_indices != max_z_index] # Exclude max_z_index
92
+ if len(valid_z_indices) > 0:
93
+ pattern_mask[valid_z_indices[valid_z_indices % z_mod == 0]] = True
94
+
95
+ # For window_ratio around 0.75, add additional pattern
96
+ if 0.625 < window_ratio <= 0.875 and len(valid_z_indices) > 0:
97
+ additional_pattern = np.zeros_like(z_mask)
98
+ additional_pattern[valid_z_indices[valid_z_indices % (z_mod + 1) == 0]] = True
99
+ pattern_mask = np.logical_or(pattern_mask, additional_pattern)
100
+
101
+ # Final mask combines base_mask and pattern_mask
102
+ final_glass_mask = np.logical_and(base_mask, pattern_mask)
103
+
104
+ # Set glass_id for all positions in the final mask
105
+ voxelcity_grid[x, y, final_glass_mask] = glass_id
106
+
107
+ return voxelcity_grid
108
+
109
+ def set_building_material_by_gdf(voxelcity_grid_ori, building_id_grid, gdf_buildings, material_id_dict=None):
110
+ """
111
+ Sets building materials based on a GeoDataFrame containing building information.
112
+
113
+ Parameters:
114
+ voxelcity_grid_ori: 3D numpy array of the original voxel grid
115
+ building_id_grid: 2D numpy array containing building IDs
116
+ gdf_buildings: GeoDataFrame containing building information with columns:
117
+ 'building_id', 'surface_material', 'window_ratio'
118
+ material_id_dict: Dictionary mapping material names to their IDs (optional)
119
+
120
+ Returns:
121
+ Modified voxelcity_grid
122
+ """
123
+ voxelcity_grid = voxelcity_grid_ori.copy()
124
+ if material_id_dict == None:
125
+ material_id_dict = get_material_dict()
126
+
127
+ for index, row in gdf_buildings.iterrows():
128
+ # Access properties
129
+ osmid = row['building_id']
130
+ surface_material = row['surface_material']
131
+ window_ratio = row['window_ratio']
132
+ if surface_material is None:
133
+ surface_material = 'unknown'
134
+ set_building_material_by_id(voxelcity_grid, building_id_grid, osmid,
135
+ material_id_dict[surface_material],
136
+ window_ratio=window_ratio,
137
+ glass_id=material_id_dict['glass'])
138
+
139
+ return voxelcity_grid