Source code for bldfm.io

"""
NetCDF I/O for BLDFM footprint results.

Saves and loads multi-tower, multi-timestep footprint fields as CF-1.8
compliant xarray Datasets with zlib compression.
"""

import numpy as np
import xarray as xr
from pathlib import Path

from .utils import get_logger

logger = get_logger("io")


[docs] def save_footprints_to_netcdf(results, config, filepath): """Save multitower results to a CF-compliant NetCDF file. Parameters ---------- results : dict Output of run_bldfm_multitower: {tower_name: [result_dict, ...]}. config : BLDFMConfig Configuration used for the run. filepath : str or Path Output file path. """ filepath = Path(filepath) filepath.parent.mkdir(parents=True, exist_ok=True) tower_names = list(results.keys()) n_towers = len(tower_names) # Get dimensions from first result first_result = results[tower_names[0]][0] X, Y, Z_coord = first_result["grid"] is_3d = first_result["flx"].ndim == 3 n_time = len(results[tower_names[0]]) if is_3d: nz_out, ny, nx = first_result["flx"].shape else: ny, nx = first_result["flx"].shape # Extract coordinate arrays if is_3d: x = X[0, 0, :] y = Y[0, :, 0] z = Z_coord[:, 0, 0] else: x = X[0, :] if X.ndim == 2 else X y = Y[:, 0] if Y.ndim == 2 else Y # Build timestamps timestamps = [] for r in results[tower_names[0]]: ts = r["timestamp"] timestamps.append(str(ts)) # Collect data arrays if is_3d: flx_data = np.zeros((n_time, n_towers, nz_out, ny, nx)) conc_data = np.zeros((n_time, n_towers, nz_out, ny, nx)) dims = ["time", "tower", "z", "y", "x"] else: flx_data = np.zeros((n_time, n_towers, ny, nx)) conc_data = np.zeros((n_time, n_towers, ny, nx)) dims = ["time", "tower", "y", "x"] ustar_data = np.zeros((n_time,)) mol_data = np.zeros((n_time,)) wind_speed_data = np.zeros((n_time,)) wind_dir_data = np.zeros((n_time,)) for ti, tower_name in enumerate(tower_names): for t, r in enumerate(results[tower_name]): flx_data[t, ti] = r["flx"] conc_data[t, ti] = r["conc"] if ti == 0: # met params are the same for all towers ustar_data[t] = r["params"]["ustar"] mol_data[t] = r["params"]["mol"] wind_speed_data[t] = r["params"]["wind_speed"] wind_dir_data[t] = r["params"]["wind_dir"] # Tower metadata tower_lats = [t.lat for t in config.towers] tower_lons = [t.lon for t in config.towers] tower_z = [t.z_m for t in config.towers] coords = { "x": ("x", x, {"long_name": "easting", "units": "m"}), "y": ("y", y, {"long_name": "northing", "units": "m"}), "time": ("time", timestamps), "tower": ("tower", tower_names), } if is_3d: coords["z"] = ("z", z, {"long_name": "height", "units": "m"}) ds = xr.Dataset( { "footprint": ( dims, flx_data, { "long_name": "flux footprint", "units": "m^-2", }, ), "concentration": ( dims, conc_data, { "long_name": "concentration field", "units": "scalar_unit", }, ), "ustar": ( ["time"], ustar_data, {"long_name": "friction velocity", "units": "m s^-1"}, ), "mol": ( ["time"], mol_data, {"long_name": "Monin-Obukhov length", "units": "m"}, ), "wind_speed": ( ["time"], wind_speed_data, {"long_name": "wind speed", "units": "m s^-1"}, ), "wind_dir": ( ["time"], wind_dir_data, {"long_name": "wind direction", "units": "degrees"}, ), "tower_lat": ( ["tower"], tower_lats, {"long_name": "tower latitude", "units": "degrees_north"}, ), "tower_lon": ( ["tower"], tower_lons, {"long_name": "tower longitude", "units": "degrees_east"}, ), "tower_z": ( ["tower"], tower_z, {"long_name": "measurement height", "units": "m"}, ), }, coords=coords, attrs={ "Conventions": "CF-1.8", "title": "BLDFM footprint output", "source": "BLDFM v1.0", "closure": config.solver.closure, "domain_xmax": config.domain.xmax, "domain_ymax": config.domain.ymax, }, ) encoding = { "footprint": {"zlib": True, "complevel": 4}, "concentration": {"zlib": True, "complevel": 4}, } ds.to_netcdf(filepath, encoding=encoding) logger.info("Saved footprints to %s", filepath)
[docs] def load_footprints_from_netcdf(filepath): """Load footprint results from a NetCDF file. Parameters ---------- filepath : str or Path Path to the NetCDF file. Returns ------- xr.Dataset Dataset with footprint, concentration, and metadata. """ filepath = Path(filepath) if not filepath.exists(): raise FileNotFoundError(f"NetCDF file not found: {filepath}") ds = xr.open_dataset(filepath) logger.info( "Loaded footprints: %d times x %d towers", ds.sizes["time"], ds.sizes["tower"], ) return ds