voxcity 0.7.0__py3-none-any.whl → 1.0.13__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.
- voxcity/__init__.py +14 -14
- voxcity/downloader/ocean.py +559 -0
- voxcity/exporter/__init__.py +12 -12
- voxcity/exporter/cityles.py +633 -633
- voxcity/exporter/envimet.py +733 -728
- voxcity/exporter/magicavoxel.py +333 -333
- voxcity/exporter/netcdf.py +238 -238
- voxcity/exporter/obj.py +1480 -1480
- voxcity/generator/__init__.py +47 -44
- voxcity/generator/api.py +727 -675
- voxcity/generator/grids.py +394 -379
- voxcity/generator/io.py +94 -94
- voxcity/generator/pipeline.py +582 -282
- voxcity/generator/update.py +429 -0
- voxcity/generator/voxelizer.py +18 -6
- voxcity/geoprocessor/__init__.py +75 -75
- voxcity/geoprocessor/draw.py +1494 -1219
- voxcity/geoprocessor/merge_utils.py +91 -91
- voxcity/geoprocessor/mesh.py +806 -806
- voxcity/geoprocessor/network.py +708 -708
- voxcity/geoprocessor/raster/__init__.py +2 -0
- voxcity/geoprocessor/raster/buildings.py +435 -428
- voxcity/geoprocessor/raster/core.py +31 -0
- voxcity/geoprocessor/raster/export.py +93 -93
- voxcity/geoprocessor/raster/landcover.py +178 -51
- voxcity/geoprocessor/raster/raster.py +1 -1
- voxcity/geoprocessor/utils.py +824 -824
- voxcity/models.py +115 -113
- voxcity/simulator/solar/__init__.py +66 -43
- voxcity/simulator/solar/integration.py +336 -336
- voxcity/simulator/solar/sky.py +668 -0
- voxcity/simulator/solar/temporal.py +792 -434
- voxcity/simulator_gpu/__init__.py +115 -0
- voxcity/simulator_gpu/common/__init__.py +9 -0
- voxcity/simulator_gpu/common/geometry.py +11 -0
- voxcity/simulator_gpu/core.py +322 -0
- voxcity/simulator_gpu/domain.py +262 -0
- voxcity/simulator_gpu/environment.yml +11 -0
- voxcity/simulator_gpu/init_taichi.py +154 -0
- voxcity/simulator_gpu/integration.py +15 -0
- voxcity/simulator_gpu/kernels.py +56 -0
- voxcity/simulator_gpu/radiation.py +28 -0
- voxcity/simulator_gpu/raytracing.py +623 -0
- voxcity/simulator_gpu/sky.py +9 -0
- voxcity/simulator_gpu/solar/__init__.py +178 -0
- voxcity/simulator_gpu/solar/core.py +66 -0
- voxcity/simulator_gpu/solar/csf.py +1249 -0
- voxcity/simulator_gpu/solar/domain.py +561 -0
- voxcity/simulator_gpu/solar/epw.py +421 -0
- voxcity/simulator_gpu/solar/integration.py +2953 -0
- voxcity/simulator_gpu/solar/radiation.py +3019 -0
- voxcity/simulator_gpu/solar/raytracing.py +686 -0
- voxcity/simulator_gpu/solar/reflection.py +533 -0
- voxcity/simulator_gpu/solar/sky.py +907 -0
- voxcity/simulator_gpu/solar/solar.py +337 -0
- voxcity/simulator_gpu/solar/svf.py +446 -0
- voxcity/simulator_gpu/solar/volumetric.py +1151 -0
- voxcity/simulator_gpu/solar/voxcity.py +2953 -0
- voxcity/simulator_gpu/temporal.py +13 -0
- voxcity/simulator_gpu/utils.py +25 -0
- voxcity/simulator_gpu/view.py +32 -0
- voxcity/simulator_gpu/visibility/__init__.py +109 -0
- voxcity/simulator_gpu/visibility/geometry.py +278 -0
- voxcity/simulator_gpu/visibility/integration.py +808 -0
- voxcity/simulator_gpu/visibility/landmark.py +753 -0
- voxcity/simulator_gpu/visibility/view.py +944 -0
- voxcity/utils/__init__.py +11 -0
- voxcity/utils/classes.py +194 -0
- voxcity/utils/lc.py +80 -39
- voxcity/utils/shape.py +230 -0
- voxcity/visualizer/__init__.py +24 -24
- voxcity/visualizer/builder.py +43 -43
- voxcity/visualizer/grids.py +141 -141
- voxcity/visualizer/maps.py +187 -187
- voxcity/visualizer/renderer.py +1146 -928
- {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/METADATA +56 -52
- voxcity-1.0.13.dist-info/RECORD +116 -0
- voxcity-0.7.0.dist-info/RECORD +0 -77
- {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/WHEEL +0 -0
- {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/licenses/LICENSE +0 -0
voxcity/exporter/netcdf.py
CHANGED
|
@@ -1,238 +1,238 @@
|
|
|
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
|
-
"NetCDFExporter",
|
|
42
|
-
]
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def _ensure_parent_dir(path: Path) -> None:
|
|
46
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def voxel_to_xarray_dataset(
|
|
50
|
-
voxcity_grid: np.ndarray,
|
|
51
|
-
voxel_size_m: float,
|
|
52
|
-
rectangle_vertices: Optional[Sequence[Tuple[float, float]]] = None,
|
|
53
|
-
extra_attrs: Optional[Mapping[str, Any]] = None,
|
|
54
|
-
) -> "xr.Dataset":
|
|
55
|
-
"""Create an xarray Dataset from a VoxCity voxel grid.
|
|
56
|
-
|
|
57
|
-
Parameters
|
|
58
|
-
----------
|
|
59
|
-
voxcity_grid
|
|
60
|
-
3D numpy array with shape (rows, cols, levels) as returned by
|
|
61
|
-
`get_voxcity` (first element of the returned tuple).
|
|
62
|
-
voxel_size_m
|
|
63
|
-
Voxel size (mesh size) in meters.
|
|
64
|
-
rectangle_vertices
|
|
65
|
-
Optional polygon vertices defining the area of interest in
|
|
66
|
-
longitude/latitude pairs, typically the same list passed to
|
|
67
|
-
`get_voxcity`.
|
|
68
|
-
extra_attrs
|
|
69
|
-
Optional mapping of additional global attributes to store in the
|
|
70
|
-
dataset.
|
|
71
|
-
|
|
72
|
-
Returns
|
|
73
|
-
-------
|
|
74
|
-
xr.Dataset
|
|
75
|
-
Dataset containing one DataArray named "voxels" with dims (y, x, z)
|
|
76
|
-
and coordinate variables in meters from origin.
|
|
77
|
-
"""
|
|
78
|
-
if not XR_AVAILABLE: # pragma: no cover - optional dependency
|
|
79
|
-
raise ImportError(
|
|
80
|
-
"xarray is required to export NetCDF. Install with: \n"
|
|
81
|
-
" pip install xarray netCDF4\n"
|
|
82
|
-
"or: \n"
|
|
83
|
-
" pip install xarray h5netcdf"
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
if voxcity_grid.ndim != 3:
|
|
87
|
-
raise ValueError(
|
|
88
|
-
f"voxcity_grid must be 3D (rows, cols, levels); got shape={voxcity_grid.shape}"
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
rows, cols, levels = voxcity_grid.shape
|
|
92
|
-
|
|
93
|
-
# Coordinate vectors in meters relative to the grid origin
|
|
94
|
-
# y increases with row index, x increases with column index
|
|
95
|
-
y_m = np.arange(rows, dtype=float) * float(voxel_size_m)
|
|
96
|
-
x_m = np.arange(cols, dtype=float) * float(voxel_size_m)
|
|
97
|
-
z_m = np.arange(levels, dtype=float) * float(voxel_size_m)
|
|
98
|
-
|
|
99
|
-
ds_attrs: MutableMapping[str, Any] = {
|
|
100
|
-
"title": "VoxCity voxel grid",
|
|
101
|
-
"institution": "VoxCity",
|
|
102
|
-
"source": "voxcity.generator.create_3d_voxel",
|
|
103
|
-
"Conventions": "CF-1.10 (partial)",
|
|
104
|
-
# NetCDF attributes must be basic types; serialize complex structures as strings
|
|
105
|
-
"vox_value_meanings": [
|
|
106
|
-
"-3: building",
|
|
107
|
-
"-2: vegetation_canopy",
|
|
108
|
-
"-1: subsurface",
|
|
109
|
-
">=1: surface_land_cover_code (offset +1)",
|
|
110
|
-
],
|
|
111
|
-
"meshsize_m": float(voxel_size_m),
|
|
112
|
-
# Store vertices as JSON string for portability
|
|
113
|
-
"rectangle_vertices_lonlat_json": (
|
|
114
|
-
json.dumps([[float(v[0]), float(v[1])] for v in rectangle_vertices])
|
|
115
|
-
if rectangle_vertices is not None else ""
|
|
116
|
-
),
|
|
117
|
-
"vertical_reference": "z=0 corresponds to min(DEM) as used in voxel construction",
|
|
118
|
-
}
|
|
119
|
-
if extra_attrs:
|
|
120
|
-
ds_attrs.update(dict(extra_attrs))
|
|
121
|
-
|
|
122
|
-
da = xr.DataArray(
|
|
123
|
-
voxcity_grid,
|
|
124
|
-
dims=("y", "x", "z"),
|
|
125
|
-
coords={
|
|
126
|
-
"y": ("y", y_m, {"units": "m", "long_name": "row_distance_from_origin"}),
|
|
127
|
-
"x": ("x", x_m, {"units": "m", "long_name": "col_distance_from_origin"}),
|
|
128
|
-
"z": ("z", z_m, {"units": "m", "positive": "up", "long_name": "height_above_vertical_origin"}),
|
|
129
|
-
},
|
|
130
|
-
name="voxels",
|
|
131
|
-
attrs={
|
|
132
|
-
"units": "category",
|
|
133
|
-
"description": "VoxCity voxel values; see global attribute 'vox_value_meanings'",
|
|
134
|
-
},
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
ds = xr.Dataset({"voxels": da}, attrs=ds_attrs)
|
|
138
|
-
return ds
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def save_voxel_netcdf(
|
|
142
|
-
voxcity_grid: np.ndarray,
|
|
143
|
-
output_path: str | Path,
|
|
144
|
-
voxel_size_m: float,
|
|
145
|
-
rectangle_vertices: Optional[Sequence[Tuple[float, float]]] = None,
|
|
146
|
-
extra_attrs: Optional[Mapping[str, Any]] = None,
|
|
147
|
-
engine: Optional[str] = None,
|
|
148
|
-
) -> str:
|
|
149
|
-
"""Save a VoxCity voxel grid to a NetCDF file.
|
|
150
|
-
|
|
151
|
-
Parameters
|
|
152
|
-
----------
|
|
153
|
-
voxcity_grid
|
|
154
|
-
3D numpy array (rows, cols, levels) of voxel values.
|
|
155
|
-
output_path
|
|
156
|
-
Path to the NetCDF file to be written. Parent directories will be
|
|
157
|
-
created as needed.
|
|
158
|
-
voxel_size_m
|
|
159
|
-
Voxel size in meters.
|
|
160
|
-
rectangle_vertices
|
|
161
|
-
Optional list of (lon, lat) pairs defining the area of interest.
|
|
162
|
-
Stored as dataset metadata only.
|
|
163
|
-
extra_attrs
|
|
164
|
-
Optional additional global attributes to embed in the dataset.
|
|
165
|
-
engine
|
|
166
|
-
Optional xarray engine, e.g., "netcdf4" or "h5netcdf". If not provided,
|
|
167
|
-
xarray will choose a default; on failure we retry alternate engines.
|
|
168
|
-
|
|
169
|
-
Returns
|
|
170
|
-
-------
|
|
171
|
-
str
|
|
172
|
-
The string path to the written NetCDF file.
|
|
173
|
-
"""
|
|
174
|
-
if not XR_AVAILABLE: # pragma: no cover - optional dependency
|
|
175
|
-
raise ImportError(
|
|
176
|
-
"xarray is required to export NetCDF. Install with: \n"
|
|
177
|
-
" pip install xarray netCDF4\n"
|
|
178
|
-
"or: \n"
|
|
179
|
-
" pip install xarray h5netcdf"
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
path = Path(output_path)
|
|
183
|
-
_ensure_parent_dir(path)
|
|
184
|
-
|
|
185
|
-
ds = voxel_to_xarray_dataset(
|
|
186
|
-
voxcity_grid=voxcity_grid,
|
|
187
|
-
voxel_size_m=voxel_size_m,
|
|
188
|
-
rectangle_vertices=rectangle_vertices,
|
|
189
|
-
extra_attrs=extra_attrs,
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
# Attempt to save with the requested or default engine; on failure, try a fallback
|
|
193
|
-
tried_engines = []
|
|
194
|
-
try:
|
|
195
|
-
ds.to_netcdf(path, engine=engine) # type: ignore[call-arg]
|
|
196
|
-
except Exception as e_first: # pragma: no cover - I/O backend dependent
|
|
197
|
-
tried_engines.append(engine or "default")
|
|
198
|
-
for fallback in ("netcdf4", "h5netcdf"):
|
|
199
|
-
try:
|
|
200
|
-
ds.to_netcdf(path, engine=fallback) # type: ignore[call-arg]
|
|
201
|
-
break
|
|
202
|
-
except Exception:
|
|
203
|
-
tried_engines.append(fallback)
|
|
204
|
-
else:
|
|
205
|
-
raise RuntimeError(
|
|
206
|
-
f"Failed to write NetCDF using engines: {tried_engines}. "
|
|
207
|
-
f"Original error: {e_first}"
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
return str(path)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
class NetCDFExporter:
|
|
214
|
-
"""Exporter adapter to write a VoxCity voxel grid to NetCDF."""
|
|
215
|
-
|
|
216
|
-
def export(self, obj, output_directory: str, base_filename: str, **kwargs):
|
|
217
|
-
from ..models import VoxCity
|
|
218
|
-
path = Path(output_directory) / f"{base_filename}.nc"
|
|
219
|
-
if not isinstance(obj, VoxCity):
|
|
220
|
-
raise TypeError("NetCDFExporter expects a VoxCity instance")
|
|
221
|
-
rect = obj.extras.get("rectangle_vertices")
|
|
222
|
-
# Merge default attrs with user-provided extras
|
|
223
|
-
user_extra = kwargs.get("extra_attrs") or {}
|
|
224
|
-
attrs = {
|
|
225
|
-
"land_cover_source": obj.extras.get("land_cover_source", ""),
|
|
226
|
-
"building_source": obj.extras.get("building_source", ""),
|
|
227
|
-
"dem_source": obj.extras.get("dem_source", ""),
|
|
228
|
-
}
|
|
229
|
-
attrs.update(user_extra)
|
|
230
|
-
return save_voxel_netcdf(
|
|
231
|
-
voxcity_grid=obj.voxels.classes,
|
|
232
|
-
output_path=path,
|
|
233
|
-
voxel_size_m=obj.voxels.meta.meshsize,
|
|
234
|
-
rectangle_vertices=rect,
|
|
235
|
-
extra_attrs=attrs,
|
|
236
|
-
engine=kwargs.get("engine"),
|
|
237
|
-
)
|
|
238
|
-
|
|
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
|
+
"NetCDFExporter",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _ensure_parent_dir(path: Path) -> None:
|
|
46
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def voxel_to_xarray_dataset(
|
|
50
|
+
voxcity_grid: np.ndarray,
|
|
51
|
+
voxel_size_m: float,
|
|
52
|
+
rectangle_vertices: Optional[Sequence[Tuple[float, float]]] = None,
|
|
53
|
+
extra_attrs: Optional[Mapping[str, Any]] = None,
|
|
54
|
+
) -> "xr.Dataset":
|
|
55
|
+
"""Create an xarray Dataset from a VoxCity voxel grid.
|
|
56
|
+
|
|
57
|
+
Parameters
|
|
58
|
+
----------
|
|
59
|
+
voxcity_grid
|
|
60
|
+
3D numpy array with shape (rows, cols, levels) as returned by
|
|
61
|
+
`get_voxcity` (first element of the returned tuple).
|
|
62
|
+
voxel_size_m
|
|
63
|
+
Voxel size (mesh size) in meters.
|
|
64
|
+
rectangle_vertices
|
|
65
|
+
Optional polygon vertices defining the area of interest in
|
|
66
|
+
longitude/latitude pairs, typically the same list passed to
|
|
67
|
+
`get_voxcity`.
|
|
68
|
+
extra_attrs
|
|
69
|
+
Optional mapping of additional global attributes to store in the
|
|
70
|
+
dataset.
|
|
71
|
+
|
|
72
|
+
Returns
|
|
73
|
+
-------
|
|
74
|
+
xr.Dataset
|
|
75
|
+
Dataset containing one DataArray named "voxels" with dims (y, x, z)
|
|
76
|
+
and coordinate variables in meters from origin.
|
|
77
|
+
"""
|
|
78
|
+
if not XR_AVAILABLE: # pragma: no cover - optional dependency
|
|
79
|
+
raise ImportError(
|
|
80
|
+
"xarray is required to export NetCDF. Install with: \n"
|
|
81
|
+
" pip install xarray netCDF4\n"
|
|
82
|
+
"or: \n"
|
|
83
|
+
" pip install xarray h5netcdf"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if voxcity_grid.ndim != 3:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
f"voxcity_grid must be 3D (rows, cols, levels); got shape={voxcity_grid.shape}"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
rows, cols, levels = voxcity_grid.shape
|
|
92
|
+
|
|
93
|
+
# Coordinate vectors in meters relative to the grid origin
|
|
94
|
+
# y increases with row index, x increases with column index
|
|
95
|
+
y_m = np.arange(rows, dtype=float) * float(voxel_size_m)
|
|
96
|
+
x_m = np.arange(cols, dtype=float) * float(voxel_size_m)
|
|
97
|
+
z_m = np.arange(levels, dtype=float) * float(voxel_size_m)
|
|
98
|
+
|
|
99
|
+
ds_attrs: MutableMapping[str, Any] = {
|
|
100
|
+
"title": "VoxCity voxel grid",
|
|
101
|
+
"institution": "VoxCity",
|
|
102
|
+
"source": "voxcity.generator.create_3d_voxel",
|
|
103
|
+
"Conventions": "CF-1.10 (partial)",
|
|
104
|
+
# NetCDF attributes must be basic types; serialize complex structures as strings
|
|
105
|
+
"vox_value_meanings": [
|
|
106
|
+
"-3: building",
|
|
107
|
+
"-2: vegetation_canopy",
|
|
108
|
+
"-1: subsurface",
|
|
109
|
+
">=1: surface_land_cover_code (offset +1)",
|
|
110
|
+
],
|
|
111
|
+
"meshsize_m": float(voxel_size_m),
|
|
112
|
+
# Store vertices as JSON string for portability
|
|
113
|
+
"rectangle_vertices_lonlat_json": (
|
|
114
|
+
json.dumps([[float(v[0]), float(v[1])] for v in rectangle_vertices])
|
|
115
|
+
if rectangle_vertices is not None else ""
|
|
116
|
+
),
|
|
117
|
+
"vertical_reference": "z=0 corresponds to min(DEM) as used in voxel construction",
|
|
118
|
+
}
|
|
119
|
+
if extra_attrs:
|
|
120
|
+
ds_attrs.update(dict(extra_attrs))
|
|
121
|
+
|
|
122
|
+
da = xr.DataArray(
|
|
123
|
+
voxcity_grid,
|
|
124
|
+
dims=("y", "x", "z"),
|
|
125
|
+
coords={
|
|
126
|
+
"y": ("y", y_m, {"units": "m", "long_name": "row_distance_from_origin"}),
|
|
127
|
+
"x": ("x", x_m, {"units": "m", "long_name": "col_distance_from_origin"}),
|
|
128
|
+
"z": ("z", z_m, {"units": "m", "positive": "up", "long_name": "height_above_vertical_origin"}),
|
|
129
|
+
},
|
|
130
|
+
name="voxels",
|
|
131
|
+
attrs={
|
|
132
|
+
"units": "category",
|
|
133
|
+
"description": "VoxCity voxel values; see global attribute 'vox_value_meanings'",
|
|
134
|
+
},
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
ds = xr.Dataset({"voxels": da}, attrs=ds_attrs)
|
|
138
|
+
return ds
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def save_voxel_netcdf(
|
|
142
|
+
voxcity_grid: np.ndarray,
|
|
143
|
+
output_path: str | Path,
|
|
144
|
+
voxel_size_m: float,
|
|
145
|
+
rectangle_vertices: Optional[Sequence[Tuple[float, float]]] = None,
|
|
146
|
+
extra_attrs: Optional[Mapping[str, Any]] = None,
|
|
147
|
+
engine: Optional[str] = None,
|
|
148
|
+
) -> str:
|
|
149
|
+
"""Save a VoxCity voxel grid to a NetCDF file.
|
|
150
|
+
|
|
151
|
+
Parameters
|
|
152
|
+
----------
|
|
153
|
+
voxcity_grid
|
|
154
|
+
3D numpy array (rows, cols, levels) of voxel values.
|
|
155
|
+
output_path
|
|
156
|
+
Path to the NetCDF file to be written. Parent directories will be
|
|
157
|
+
created as needed.
|
|
158
|
+
voxel_size_m
|
|
159
|
+
Voxel size in meters.
|
|
160
|
+
rectangle_vertices
|
|
161
|
+
Optional list of (lon, lat) pairs defining the area of interest.
|
|
162
|
+
Stored as dataset metadata only.
|
|
163
|
+
extra_attrs
|
|
164
|
+
Optional additional global attributes to embed in the dataset.
|
|
165
|
+
engine
|
|
166
|
+
Optional xarray engine, e.g., "netcdf4" or "h5netcdf". If not provided,
|
|
167
|
+
xarray will choose a default; on failure we retry alternate engines.
|
|
168
|
+
|
|
169
|
+
Returns
|
|
170
|
+
-------
|
|
171
|
+
str
|
|
172
|
+
The string path to the written NetCDF file.
|
|
173
|
+
"""
|
|
174
|
+
if not XR_AVAILABLE: # pragma: no cover - optional dependency
|
|
175
|
+
raise ImportError(
|
|
176
|
+
"xarray is required to export NetCDF. Install with: \n"
|
|
177
|
+
" pip install xarray netCDF4\n"
|
|
178
|
+
"or: \n"
|
|
179
|
+
" pip install xarray h5netcdf"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
path = Path(output_path)
|
|
183
|
+
_ensure_parent_dir(path)
|
|
184
|
+
|
|
185
|
+
ds = voxel_to_xarray_dataset(
|
|
186
|
+
voxcity_grid=voxcity_grid,
|
|
187
|
+
voxel_size_m=voxel_size_m,
|
|
188
|
+
rectangle_vertices=rectangle_vertices,
|
|
189
|
+
extra_attrs=extra_attrs,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Attempt to save with the requested or default engine; on failure, try a fallback
|
|
193
|
+
tried_engines = []
|
|
194
|
+
try:
|
|
195
|
+
ds.to_netcdf(path, engine=engine) # type: ignore[call-arg]
|
|
196
|
+
except Exception as e_first: # pragma: no cover - I/O backend dependent
|
|
197
|
+
tried_engines.append(engine or "default")
|
|
198
|
+
for fallback in ("netcdf4", "h5netcdf"):
|
|
199
|
+
try:
|
|
200
|
+
ds.to_netcdf(path, engine=fallback) # type: ignore[call-arg]
|
|
201
|
+
break
|
|
202
|
+
except Exception:
|
|
203
|
+
tried_engines.append(fallback)
|
|
204
|
+
else:
|
|
205
|
+
raise RuntimeError(
|
|
206
|
+
f"Failed to write NetCDF using engines: {tried_engines}. "
|
|
207
|
+
f"Original error: {e_first}"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
return str(path)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class NetCDFExporter:
|
|
214
|
+
"""Exporter adapter to write a VoxCity voxel grid to NetCDF."""
|
|
215
|
+
|
|
216
|
+
def export(self, obj, output_directory: str, base_filename: str, **kwargs):
|
|
217
|
+
from ..models import VoxCity
|
|
218
|
+
path = Path(output_directory) / f"{base_filename}.nc"
|
|
219
|
+
if not isinstance(obj, VoxCity):
|
|
220
|
+
raise TypeError("NetCDFExporter expects a VoxCity instance")
|
|
221
|
+
rect = obj.extras.get("rectangle_vertices")
|
|
222
|
+
# Merge default attrs with user-provided extras
|
|
223
|
+
user_extra = kwargs.get("extra_attrs") or {}
|
|
224
|
+
attrs = {
|
|
225
|
+
"land_cover_source": obj.extras.get("land_cover_source", ""),
|
|
226
|
+
"building_source": obj.extras.get("building_source", ""),
|
|
227
|
+
"dem_source": obj.extras.get("dem_source", ""),
|
|
228
|
+
}
|
|
229
|
+
attrs.update(user_extra)
|
|
230
|
+
return save_voxel_netcdf(
|
|
231
|
+
voxcity_grid=obj.voxels.classes,
|
|
232
|
+
output_path=path,
|
|
233
|
+
voxel_size_m=obj.voxels.meta.meshsize,
|
|
234
|
+
rectangle_vertices=rect,
|
|
235
|
+
extra_attrs=attrs,
|
|
236
|
+
engine=kwargs.get("engine"),
|
|
237
|
+
)
|
|
238
|
+
|