voxcity 0.6.16__tar.gz → 0.6.18__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.6.16 → voxcity-0.6.18}/PKG-INFO +4 -2
- {voxcity-0.6.16 → voxcity-0.6.18}/README.md +1 -1
- {voxcity-0.6.16 → voxcity-0.6.18}/pyproject.toml +3 -1
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/downloader/osm.py +23 -7
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/downloader/overture.py +26 -1
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/exporter/__init__.py +2 -1
- voxcity-0.6.18/src/voxcity/exporter/netcdf.py +211 -0
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/exporter/obj.py +538 -1
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/generator.py +102 -7
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/geoprocessor/grid.py +1738 -1732
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/utils/visualization.py +31 -0
- {voxcity-0.6.16 → voxcity-0.6.18}/AUTHORS.rst +0 -0
- {voxcity-0.6.16 → voxcity-0.6.18}/LICENSE +0 -0
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/__init__.py +0 -0
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/downloader/__init__.py +0 -0
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/downloader/citygml.py +0 -0
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/downloader/eubucco.py +0 -0
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/downloader/gee.py +0 -0
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/downloader/mbfp.py +0 -0
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/downloader/oemj.py +0 -0
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/downloader/utils.py +0 -0
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/exporter/cityles.py +0 -0
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/exporter/envimet.py +0 -0
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/exporter/magicavoxel.py +0 -0
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/geoprocessor/__init__.py +0 -0
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/geoprocessor/draw.py +0 -0
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/geoprocessor/mesh.py +0 -0
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/geoprocessor/network.py +0 -0
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/geoprocessor/polygon.py +0 -0
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/geoprocessor/utils.py +0 -0
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/simulator/__init__.py +0 -0
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/simulator/solar.py +0 -0
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/simulator/utils.py +0 -0
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/simulator/view.py +0 -0
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/utils/__init__.py +0 -0
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/utils/lc.py +0 -0
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/utils/material.py +0 -0
- {voxcity-0.6.16 → voxcity-0.6.18}/src/voxcity/utils/weather.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: voxcity
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.18
|
|
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
|
License: MIT
|
|
6
6
|
Author: Kunihiko Fujiwara
|
|
@@ -27,6 +27,7 @@ Requires-Dist: ipyleaflet
|
|
|
27
27
|
Requires-Dist: joblib
|
|
28
28
|
Requires-Dist: lxml
|
|
29
29
|
Requires-Dist: matplotlib
|
|
30
|
+
Requires-Dist: netCDF4
|
|
30
31
|
Requires-Dist: numba
|
|
31
32
|
Requires-Dist: numpy
|
|
32
33
|
Requires-Dist: osmnx
|
|
@@ -51,6 +52,7 @@ Requires-Dist: timezonefinder
|
|
|
51
52
|
Requires-Dist: tqdm
|
|
52
53
|
Requires-Dist: trimesh
|
|
53
54
|
Requires-Dist: typer
|
|
55
|
+
Requires-Dist: xarray
|
|
54
56
|
Project-URL: bugs, https://github.com/kunifujiwara/voxcity/issues
|
|
55
57
|
Project-URL: changelog, https://github.com/kunifujiwara/voxcity/blob/master/changelog.md
|
|
56
58
|
Project-URL: homepage, https://github.com/kunifujiwara/voxcity
|
|
@@ -232,7 +234,7 @@ Generate voxel data grids and corresponding building geoJSON:
|
|
|
232
234
|
from voxcity.generator import get_voxcity
|
|
233
235
|
|
|
234
236
|
voxcity_grid, building_height_grid, building_min_height_grid, \
|
|
235
|
-
building_id_grid, canopy_height_grid, land_cover_grid, dem_grid, \
|
|
237
|
+
building_id_grid, canopy_height_grid, canopy_bottom_height_grid, land_cover_grid, dem_grid, \
|
|
236
238
|
building_gdf = get_voxcity(
|
|
237
239
|
rectangle_vertices,
|
|
238
240
|
building_source,
|
|
@@ -174,7 +174,7 @@ Generate voxel data grids and corresponding building geoJSON:
|
|
|
174
174
|
from voxcity.generator import get_voxcity
|
|
175
175
|
|
|
176
176
|
voxcity_grid, building_height_grid, building_min_height_grid, \
|
|
177
|
-
building_id_grid, canopy_height_grid, land_cover_grid, dem_grid, \
|
|
177
|
+
building_id_grid, canopy_height_grid, canopy_bottom_height_grid, land_cover_grid, dem_grid, \
|
|
178
178
|
building_gdf = get_voxcity(
|
|
179
179
|
rectangle_vertices,
|
|
180
180
|
building_source,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "voxcity"
|
|
3
|
-
version = "0.6.
|
|
3
|
+
version = "0.6.18"
|
|
4
4
|
description = "voxcity is an easy and one-stop tool to output 3d city models for microclimate simulation by integrating multiple geospatial open-data"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -56,6 +56,8 @@ pyvista = "*"
|
|
|
56
56
|
IPython = "*"
|
|
57
57
|
lxml = "*"
|
|
58
58
|
scikit-learn = "*"
|
|
59
|
+
xarray = "*"
|
|
60
|
+
netCDF4 = "*"
|
|
59
61
|
|
|
60
62
|
[tool.poetry.group.dev.dependencies]
|
|
61
63
|
coverage = "*"
|
|
@@ -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
|
|
|
@@ -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
|
+
|