voxcity 0.6.16__py3-none-any.whl → 0.6.18__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/downloader/osm.py CHANGED
@@ -370,7 +370,7 @@ def create_rings_from_ways(way_ids, ways, nodes):
370
370
 
371
371
  return rings
372
372
 
373
- def load_gdf_from_openstreetmap(rectangle_vertices):
373
+ def load_gdf_from_openstreetmap(rectangle_vertices, floor_height=3.0):
374
374
  """Download and process building footprint data from OpenStreetMap.
375
375
 
376
376
  This function:
@@ -471,7 +471,7 @@ def load_gdf_from_openstreetmap(rectangle_vertices):
471
471
  """
472
472
  return [coord for coord in geometry] # Keep original order since already (lon, lat)
473
473
 
474
- def get_height_from_properties(properties):
474
+ def get_height_from_properties(properties, floor_height=3.0):
475
475
  """Helper function to extract height from properties.
476
476
 
477
477
  Args:
@@ -487,9 +487,25 @@ def load_gdf_from_openstreetmap(rectangle_vertices):
487
487
  except ValueError:
488
488
  pass
489
489
 
490
+ # Infer from floors when available
491
+ floors_candidates = [
492
+ properties.get('building:levels'),
493
+ properties.get('levels'),
494
+ properties.get('num_floors')
495
+ ]
496
+ for floors in floors_candidates:
497
+ if floors is None:
498
+ continue
499
+ try:
500
+ floors_val = float(floors)
501
+ if floors_val > 0:
502
+ return float(floor_height) * floors_val
503
+ except ValueError:
504
+ continue
505
+
490
506
  return 0 # Default height if no valid height found
491
507
 
492
- def extract_properties(element):
508
+ def extract_properties(element, floor_height=3.0):
493
509
  """Helper function to extract and process properties from an element.
494
510
 
495
511
  Args:
@@ -501,7 +517,7 @@ def load_gdf_from_openstreetmap(rectangle_vertices):
501
517
  properties = element.get('tags', {})
502
518
 
503
519
  # Get height (now using the helper function)
504
- height = get_height_from_properties(properties)
520
+ height = get_height_from_properties(properties, floor_height=floor_height)
505
521
 
506
522
  # Get min_height and min_level
507
523
  min_height = properties.get('min_height', '0')
@@ -526,7 +542,7 @@ def load_gdf_from_openstreetmap(rectangle_vertices):
526
542
  "is_inner": False,
527
543
  "levels": levels,
528
544
  "height_source": "explicit" if properties.get('height') or properties.get('building:height')
529
- else "levels" if levels is not None
545
+ else "levels" if (levels is not None) or (properties.get('num_floors') is not None)
530
546
  else "default",
531
547
  "min_level": min_level if min_level != '0' else None,
532
548
  "building": properties.get('building', 'no'),
@@ -584,13 +600,13 @@ def load_gdf_from_openstreetmap(rectangle_vertices):
584
600
  if element['type'] == 'way':
585
601
  if 'geometry' in element:
586
602
  coords = [(node['lon'], node['lat']) for node in element['geometry']]
587
- properties = extract_properties(element)
603
+ properties = extract_properties(element, floor_height=floor_height)
588
604
  feature = create_polygon_feature(coords, properties)
589
605
  if feature:
590
606
  features.append(feature)
591
607
 
592
608
  elif element['type'] == 'relation':
593
- properties = extract_properties(element)
609
+ properties = extract_properties(element, floor_height=floor_height)
594
610
 
595
611
  # Process each member of the relation
596
612
  for member in element['members']:
@@ -254,7 +254,7 @@ def join_gdfs_vertically(gdf1, gdf2):
254
254
 
255
255
  return combined_gdf
256
256
 
257
- def load_gdf_from_overture(rectangle_vertices):
257
+ def load_gdf_from_overture(rectangle_vertices, floor_height=3.0):
258
258
  """
259
259
  Download and process building footprint data from Overture Maps.
260
260
 
@@ -287,6 +287,31 @@ def load_gdf_from_overture(rectangle_vertices):
287
287
  # Combine both datasets into a single comprehensive building dataset
288
288
  joined_building_gdf = join_gdfs_vertically(building_gdf, building_part_gdf)
289
289
 
290
+ # Ensure numeric height and infer from floors when missing
291
+ try:
292
+ joined_building_gdf['height'] = pd.to_numeric(joined_building_gdf.get('height', None), errors='coerce')
293
+ except Exception:
294
+ # Create height column if missing
295
+ joined_building_gdf['height'] = None
296
+ joined_building_gdf['height'] = pd.to_numeric(joined_building_gdf['height'], errors='coerce')
297
+
298
+ # Combine possible floors columns (first non-null among candidates)
299
+ floors_candidates = []
300
+ for col in ['building:levels', 'levels', 'num_floors', 'floors']:
301
+ if col in joined_building_gdf.columns:
302
+ floors_candidates.append(pd.to_numeric(joined_building_gdf[col], errors='coerce'))
303
+ if floors_candidates:
304
+ floors_series = floors_candidates[0]
305
+ for s in floors_candidates[1:]:
306
+ floors_series = floors_series.combine_first(s)
307
+ # Infer height where height is NaN/<=0 and floors > 0
308
+ mask_missing_height = (~joined_building_gdf['height'].notna()) | (joined_building_gdf['height'] <= 0)
309
+ if isinstance(floor_height, (int, float)):
310
+ inferred = floors_series * float(floor_height)
311
+ else:
312
+ inferred = floors_series * 3.0
313
+ joined_building_gdf.loc[mask_missing_height & (floors_series > 0), 'height'] = inferred
314
+
290
315
  # Assign sequential IDs based on the final dataset index
291
316
  joined_building_gdf['id'] = joined_building_gdf.index
292
317
 
@@ -1,4 +1,5 @@
1
1
  from .envimet import *
2
2
  from .magicavoxel import *
3
3
  from .obj import *
4
- from .cityles import *
4
+ from .cityles import *
5
+ from .netcdf import *
@@ -0,0 +1,211 @@
1
+ """
2
+ NetCDF export utilities for VoxCity.
3
+
4
+ This module provides functions to convert a 3D voxel grid produced by
5
+ `voxcity.generator.get_voxcity` into a NetCDF file for portable storage
6
+ and downstream analysis.
7
+
8
+ The voxel values follow VoxCity conventions (see generator.create_3d_voxel):
9
+ - -3: built structures (buildings)
10
+ - -2: vegetation canopy
11
+ - -1: subsurface/underground
12
+ - >= 1: ground-surface land cover code (offset by +1 from source classes)
13
+
14
+ Notes
15
+ -----
16
+ - This writer prefers xarray for NetCDF export. If xarray is not installed,
17
+ a clear error is raised with installation hints.
18
+ - Coordinates are stored as index-based distances in meters from the grid
19
+ origin along the y, x, and z axes. Geographic metadata such as the
20
+ `rectangle_vertices` and `meshsize_m` are stored as global attributes to
21
+ avoid making assumptions about map projection or geodesic conversions.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from pathlib import Path
27
+ from typing import Any, Dict, Mapping, MutableMapping, Optional, Sequence, Tuple
28
+ import json
29
+
30
+ import numpy as np
31
+
32
+ try: # xarray is the preferred backend for NetCDF writing
33
+ import xarray as xr # type: ignore
34
+ XR_AVAILABLE = True
35
+ except Exception: # pragma: no cover - optional dependency
36
+ XR_AVAILABLE = False
37
+
38
+ __all__ = [
39
+ "voxel_to_xarray_dataset",
40
+ "save_voxel_netcdf",
41
+ ]
42
+
43
+
44
+ def _ensure_parent_dir(path: Path) -> None:
45
+ path.parent.mkdir(parents=True, exist_ok=True)
46
+
47
+
48
+ def voxel_to_xarray_dataset(
49
+ voxcity_grid: np.ndarray,
50
+ voxel_size_m: float,
51
+ rectangle_vertices: Optional[Sequence[Tuple[float, float]]] = None,
52
+ extra_attrs: Optional[Mapping[str, Any]] = None,
53
+ ) -> "xr.Dataset":
54
+ """Create an xarray Dataset from a VoxCity voxel grid.
55
+
56
+ Parameters
57
+ ----------
58
+ voxcity_grid
59
+ 3D numpy array with shape (rows, cols, levels) as returned by
60
+ `get_voxcity` (first element of the returned tuple).
61
+ voxel_size_m
62
+ Voxel size (mesh size) in meters.
63
+ rectangle_vertices
64
+ Optional polygon vertices defining the area of interest in
65
+ longitude/latitude pairs, typically the same list passed to
66
+ `get_voxcity`.
67
+ extra_attrs
68
+ Optional mapping of additional global attributes to store in the
69
+ dataset.
70
+
71
+ Returns
72
+ -------
73
+ xr.Dataset
74
+ Dataset containing one DataArray named "voxels" with dims (y, x, z)
75
+ and coordinate variables in meters from origin.
76
+ """
77
+ if not XR_AVAILABLE: # pragma: no cover - optional dependency
78
+ raise ImportError(
79
+ "xarray is required to export NetCDF. Install with: \n"
80
+ " pip install xarray netCDF4\n"
81
+ "or: \n"
82
+ " pip install xarray h5netcdf"
83
+ )
84
+
85
+ if voxcity_grid.ndim != 3:
86
+ raise ValueError(
87
+ f"voxcity_grid must be 3D (rows, cols, levels); got shape={voxcity_grid.shape}"
88
+ )
89
+
90
+ rows, cols, levels = voxcity_grid.shape
91
+
92
+ # Coordinate vectors in meters relative to the grid origin
93
+ # y increases with row index, x increases with column index
94
+ y_m = np.arange(rows, dtype=float) * float(voxel_size_m)
95
+ x_m = np.arange(cols, dtype=float) * float(voxel_size_m)
96
+ z_m = np.arange(levels, dtype=float) * float(voxel_size_m)
97
+
98
+ ds_attrs: MutableMapping[str, Any] = {
99
+ "title": "VoxCity voxel grid",
100
+ "institution": "VoxCity",
101
+ "source": "voxcity.generator.create_3d_voxel",
102
+ "Conventions": "CF-1.10 (partial)",
103
+ # NetCDF attributes must be basic types; serialize complex structures as strings
104
+ "vox_value_meanings": [
105
+ "-3: building",
106
+ "-2: vegetation_canopy",
107
+ "-1: subsurface",
108
+ ">=1: surface_land_cover_code (offset +1)",
109
+ ],
110
+ "meshsize_m": float(voxel_size_m),
111
+ # Store vertices as JSON string for portability
112
+ "rectangle_vertices_lonlat_json": (
113
+ json.dumps([[float(v[0]), float(v[1])] for v in rectangle_vertices])
114
+ if rectangle_vertices is not None else ""
115
+ ),
116
+ "vertical_reference": "z=0 corresponds to min(DEM) as used in voxel construction",
117
+ }
118
+ if extra_attrs:
119
+ ds_attrs.update(dict(extra_attrs))
120
+
121
+ da = xr.DataArray(
122
+ voxcity_grid,
123
+ dims=("y", "x", "z"),
124
+ coords={
125
+ "y": ("y", y_m, {"units": "m", "long_name": "row_distance_from_origin"}),
126
+ "x": ("x", x_m, {"units": "m", "long_name": "col_distance_from_origin"}),
127
+ "z": ("z", z_m, {"units": "m", "positive": "up", "long_name": "height_above_vertical_origin"}),
128
+ },
129
+ name="voxels",
130
+ attrs={
131
+ "units": "category",
132
+ "description": "VoxCity voxel values; see global attribute 'vox_value_meanings'",
133
+ },
134
+ )
135
+
136
+ ds = xr.Dataset({"voxels": da}, attrs=ds_attrs)
137
+ return ds
138
+
139
+
140
+ def save_voxel_netcdf(
141
+ voxcity_grid: np.ndarray,
142
+ output_path: str | Path,
143
+ voxel_size_m: float,
144
+ rectangle_vertices: Optional[Sequence[Tuple[float, float]]] = None,
145
+ extra_attrs: Optional[Mapping[str, Any]] = None,
146
+ engine: Optional[str] = None,
147
+ ) -> str:
148
+ """Save a VoxCity voxel grid to a NetCDF file.
149
+
150
+ Parameters
151
+ ----------
152
+ voxcity_grid
153
+ 3D numpy array (rows, cols, levels) of voxel values.
154
+ output_path
155
+ Path to the NetCDF file to be written. Parent directories will be
156
+ created as needed.
157
+ voxel_size_m
158
+ Voxel size in meters.
159
+ rectangle_vertices
160
+ Optional list of (lon, lat) pairs defining the area of interest.
161
+ Stored as dataset metadata only.
162
+ extra_attrs
163
+ Optional additional global attributes to embed in the dataset.
164
+ engine
165
+ Optional xarray engine, e.g., "netcdf4" or "h5netcdf". If not provided,
166
+ xarray will choose a default; on failure we retry alternate engines.
167
+
168
+ Returns
169
+ -------
170
+ str
171
+ The string path to the written NetCDF file.
172
+ """
173
+ if not XR_AVAILABLE: # pragma: no cover - optional dependency
174
+ raise ImportError(
175
+ "xarray is required to export NetCDF. Install with: \n"
176
+ " pip install xarray netCDF4\n"
177
+ "or: \n"
178
+ " pip install xarray h5netcdf"
179
+ )
180
+
181
+ path = Path(output_path)
182
+ _ensure_parent_dir(path)
183
+
184
+ ds = voxel_to_xarray_dataset(
185
+ voxcity_grid=voxcity_grid,
186
+ voxel_size_m=voxel_size_m,
187
+ rectangle_vertices=rectangle_vertices,
188
+ extra_attrs=extra_attrs,
189
+ )
190
+
191
+ # Attempt to save with the requested or default engine; on failure, try a fallback
192
+ tried_engines = []
193
+ try:
194
+ ds.to_netcdf(path, engine=engine) # type: ignore[call-arg]
195
+ except Exception as e_first: # pragma: no cover - I/O backend dependent
196
+ tried_engines.append(engine or "default")
197
+ for fallback in ("netcdf4", "h5netcdf"):
198
+ try:
199
+ ds.to_netcdf(path, engine=fallback) # type: ignore[call-arg]
200
+ break
201
+ except Exception:
202
+ tried_engines.append(fallback)
203
+ else:
204
+ raise RuntimeError(
205
+ f"Failed to write NetCDF using engines: {tried_engines}. "
206
+ f"Original error: {e_first}"
207
+ )
208
+
209
+ return str(path)
210
+
211
+