Source code for pywasp.wasp.wind_dir_crs

"""Align wind-climate direction frames between coordinate reference systems."""

import numpy as np
import pyproj
import xarray as xr
import windkit as wk

from pywasp._batch import batch_layout
from pywasp._windkit_internal import _spatial_dims_in_order
from pywasp.wasp.binned_wind_climate import bwc_resample_sectors
from pywasp.wasp.time_series_wind_climate import tswc_rotate
from pywasp.wasp.weibull_wind_climate import wwc_rotate

__all__ = ["align_direction_crs"]


_WIND_DIR_CRS_CAPABILITIES = (
    ("geowc", wk.is_geowc),
    ("tswc", wk.is_tswc),
    ("bwc", wk.is_bwc),
    ("wwc", wk.is_wwc),
    ("gwc", wk.is_gwc),
)


[docs] def align_direction_crs( wind_climate, target_wind_dir_crs, source_wind_dir_crs=None, conf=None, ): """Align a wind climate to a target wind-direction CRS. Wind directions may be expressed relative to true north or relative to the grid north of a projected CRS. This helper rotates the directional part of a wind climate from ``source_wind_dir_crs`` into ``target_wind_dir_crs`` at the climate locations and stores the target frame in the ``wind_dir_crs`` attribute. The simpler the wind climate, the harder this operation is to perform without information loss. TSWC direction values are rotated directly and therefore retain the original time-series information. BWC sector histograms are resampled, so information is limited by sector and wind-speed bin resolution. WWC and GWC contain only sectoral Weibull parameters and frequencies, so rotation is the most compressed approximation and relies on conserving moments during sector rotation. Parameters ---------- wind_climate : xarray.Dataset TSWC, BWC, WWC, or GWC dataset. GeoWC is not supported because it contains additional directional variables that cannot be rotated by the WWC sector-rotation routine. target_wind_dir_crs : CRS-like Coordinate reference system whose grid north should define the output wind-direction frame. source_wind_dir_crs : CRS-like, optional Coordinate reference system whose grid north defines the input wind-direction frame. If omitted, the function reads ``wind_climate.attrs["wind_dir_crs"]``. All supported wind climates require explicit source-frame metadata. conf : pywasp.wasp.Config, optional Configuration object passed to ``wwc_rotate`` for WWC and GWC input. Returns ------- xarray.Dataset Wind climate with directions aligned to ``target_wind_dir_crs`` and a ``wind_dir_crs`` attribute containing the target CRS WKT. The spatial CRS metadata is preserved from the input. Raises ------ ValueError If ``wind_climate`` is unsupported, if source-frame metadata is missing, or if a GeoWC is passed. """ target_crs = pyproj.CRS.from_user_input(target_wind_dir_crs) _wind_dir_crs_capability(wind_climate) source_crs = _source_wind_dir_crs( wind_climate, source_wind_dir_crs=source_wind_dir_crs, ) if source_crs.equals(target_crs): result = wind_climate.copy() else: rotation = _direction_frame_rotation(wind_climate, target_crs, source_crs) result = _rotate_wind_climate( wind_climate, rotation_angle_deg=-rotation, conf=conf, ) result.attrs["wind_dir_crs"] = target_crs.to_wkt() return result
def _source_wind_dir_crs(wind_climate, source_wind_dir_crs): """Return the source wind-direction CRS from input or metadata. Parameters ---------- wind_climate : xarray.Dataset Wind climate dataset that may contain a ``wind_dir_crs`` attribute. source_wind_dir_crs : CRS-like or None Explicit source wind-direction CRS. Returns ------- pyproj.CRS Source wind-direction CRS. Raises ------ ValueError If no source CRS is provided and ``wind_climate`` has no ``wind_dir_crs`` attribute. """ if source_wind_dir_crs is not None: return pyproj.CRS.from_user_input(source_wind_dir_crs) wind_dir_crs = wind_climate.attrs.get("wind_dir_crs") if wind_dir_crs is not None: return pyproj.CRS.from_user_input(wind_dir_crs) raise ValueError( "source_wind_dir_crs is required when wind_climate has no wind_dir_crs " "attribute." ) def _direction_frame_rotation(wind_climate, target_crs, source_crs): """Return target-frame grid convergence on the input spatial structure. Grid convergence only depends on horizontal position, so the returned rotation carries no independent ``height`` dimension. The rotation consumers do not yet broadcast it over multi-height cuboid or stacked-point climates; that pre-existing limitation is tracked in #757. Restoration uses the input's exact coordinates because a CRS round trip introduces floating-point coordinate noise. Internal reprojection does not replace the input spatial CRS metadata. Parameters ---------- wind_climate : xarray.Dataset Wind climate dataset whose spatial structure should be preserved. target_crs : pyproj.CRS Target wind-direction CRS. source_crs : pyproj.CRS Source wind-direction CRS. Returns ------- xarray.DataArray Grid-convergence rotation on the original wind-climate spatial structure. """ horizontal = wind_climate[["west_east", "south_north"]] layout = batch_layout( horizontal, batch_dims=_spatial_dims_in_order(horizontal), core_dims=(), required_coords=("west_east", "south_north"), ) source_we, source_sn = layout.flat_coords(dtype=np.float64) # Use WindKit's direct reprojection once windkit#811 preserves the input # raster/cuboid structure instead of restacking it onto a point dimension. transformer = pyproj.Transformer.from_crs( wk.spatial.get_crs(wind_climate), target_crs, always_xy=True ) target_we, target_sn = transformer.transform(source_we, source_sn) target_points = wk.spatial.set_crs( xr.Dataset( {"west_east": ("_flat", target_we), "south_north": ("_flat", target_sn)} ).set_coords(["west_east", "south_north"]), target_crs, ) rotation = wk.spatial.grid_convergence(target_points, source=source_crs) return layout.restore( rotation.data, core_dims=(), name=rotation.name, attrs=rotation.attrs ) def _rotate_wind_climate( wind_climate, rotation_angle_deg, conf, ): """Rotate a wind climate using the climate-specific implementation. Parameters ---------- wind_climate : xarray.Dataset TSWC, BWC, WWC, or GWC dataset. rotation_angle_deg : float or xarray.DataArray Clockwise-positive rotation angle in degrees. conf : pywasp.wasp.Config or None Configuration object passed to ``wwc_rotate`` for WWC and GWC input. Returns ------- xarray.Dataset Rotated wind climate dataset. """ capability = _wind_dir_crs_capability(wind_climate) if capability == "tswc": return tswc_rotate(wind_climate, rotation_angle_deg) if capability == "bwc": return bwc_resample_sectors( wind_climate, n_sectors=wind_climate.sizes["sector"], offset=rotation_angle_deg, ) if capability in ("wwc", "gwc"): return wwc_rotate(wind_climate, rotation_angle_deg, conf=conf) raise ValueError( "align_direction_crs only supports TSWC, BWC, WWC, and GWC datasets." ) def _wind_dir_crs_capability(wind_climate): """Return the supported wind-climate capability for direction alignment. Parameters ---------- wind_climate : xarray.Dataset Wind climate dataset to classify. Returns ------- str One of ``"tswc"``, ``"bwc"``, ``"wwc"``, or ``"gwc"``. Raises ------ ValueError If ``wind_climate`` is GeoWC or unsupported. """ for name, check in _WIND_DIR_CRS_CAPABILITIES: if check(wind_climate): if name == "geowc": raise ValueError( "GeoWC is not supported by align_direction_crs because its " "geostrophic variables cannot be rotated by the WWC sector " "rotation routine." ) return name raise ValueError( "align_direction_crs only supports TSWC, BWC, WWC, and GWC datasets." )