Source code for pytomoatt.attarray

from collections.abc import Sequence
from typing import Literal

import xarray
import numpy as np
from scipy.interpolate import interpn
from pyproj import Geod

from pytomoatt.utils.rotate import rtp_rotation, rtp_rotation_reverse
from .utils.common import interpolation_lola_linear
from .utils import _EARTH_RADIUS_KM


[docs] class Dataset(xarray.Dataset): """Sub class of `xarray.Dataset <https://docs.xarray.dev/en/stable/generated/xarray.Dataset.html>`__ """ __slots__ = () def __init__(self, data_vars, coords, attrs=None) -> None: super().__init__(data_vars, coords, attrs)
[docs] @classmethod def from_xarray(cls, dataset): ds = cls(dataset.data_vars, dataset.coords) return ds
[docs] def interp_dep( self, depth: float, field: str, samp_interval: int | Sequence[int] = 0, rotate: Sequence[float] | None = None, ) -> np.ndarray: """Interpolate map view with given depth :param depth: Depth in km :type depth: float :param field: Field name in ATT model data :type field: str :param samp_interval: Sampling interval, defaults to 0. If a positive integer is provided, the same interval is used for both x/lon and y/lat directions. If a two-element sequence ``[Nx, Ny]`` is provided, ``Nx`` is used for x/lon and ``Ny`` is used for y/lat. :type samp_interval: int or sequence of int, optional :param rotate: Rotation parameters ``[central_lat, central_lon, rotation_angle]`` in degrees. ``central_lat`` and ``central_lon`` are physical coordinates. When provided, returned longitudes and latitudes are converted to physical coordinates. Defaults to None. :return: xyz data with 3 columns [lon, lat, value] :rtype: :class:`numpy.ndarray` """ if field not in self.data_vars.keys(): raise ValueError('Error field name of {}'.format(field)) # resample self of xarray with given interval of ``samp_interval`` if rotate is not None: try: # rotate reversely, from computational grid to physical grid central_lat, central_lon, rotation_angle = rotate except (TypeError, ValueError): raise ValueError( "rotate must be a 3-item sequence: [central_lat, central_lon, rotation_angle]" ) if isinstance(samp_interval, (list, tuple, np.ndarray)): if len(samp_interval) != 2: raise ValueError( "samp_interval must be an integer or a two-element " "sequence [Nx, Ny]" ) x_interval, y_interval = map(int, samp_interval) else: x_interval = y_interval = int(samp_interval) if x_interval > 0 and y_interval > 0: resampled = self.isel( t=slice(0, None, y_interval), p=slice(0, None, x_interval), ) elif x_interval == 0 and y_interval == 0: resampled = self else: raise ValueError( "samp_interval values must be positive, or 0 to disable " "resampling" ) idx = np.where(resampled.coords['dep'].values == depth)[0] if idx.size > 0: offset = 0 if (rotate is not None) and ((field == "xi") or (field == "eta") or ((field == "phi"))): # need do rotation correction, phi -> phi - rotation_angle data_phi = resampled.data_vars["phi"].values[idx[0], :, :] - rotation_angle data_xi = resampled.data_vars["epsilon"].values[idx[0], :, :] * np.cos(2*np.deg2rad(data_phi)) data_eta = resampled.data_vars["epsilon"].values[idx[0], :, :] * np.sin(2*np.deg2rad(data_phi)) if field == "xi": data_array = data_xi elif field == "eta": data_array = data_eta else: # field == "phi" data_array = data_phi data = np.zeros([resampled.coords['lat'].size*resampled.coords['lon'].size, 3]) for i, la in enumerate(resampled.coords['lat'].values): for j, lo in enumerate(resampled.coords['lon'].values): data[offset] = [lo, la, data_array[i, j]] offset += 1 else: data = np.zeros([resampled.coords['lat'].size*resampled.coords['lon'].size, 3]) for i, la in enumerate(resampled.coords['lat'].values): for j, lo in enumerate(resampled.coords['lon'].values): data[offset] = [lo, la, resampled.data_vars[field].values[idx[0], i, j]] offset += 1 else: rad = _EARTH_RADIUS_KM - depth points = np.zeros([resampled.coords['lat'].size*resampled.coords['lon'].size, 4]) offset = 0 for _, la in enumerate(resampled.coords['lat'].values): for _, lo in enumerate(resampled.coords['lon'].values): points[offset] = [rad, la, lo, 0.] offset += 1 # Angle can not be interpolated directly, 0 degree and 180 degree are the same. But the interpolation will give 90 degree. if (rotate is not None): if ((field == "xi") or (field == "eta") or ((field == "phi"))): data_xi = interpn( (resampled.coords['rad'].values, resampled.coords['lat'].values, resampled.coords['lon'].values), resampled.data_vars["xi"].values, points[:, 0:3] ) data_eta = interpn( (resampled.coords['rad'].values, resampled.coords['lat'].values, resampled.coords['lon'].values), resampled.data_vars["eta"].values, points[:, 0:3] ) data_phi = np.rad2deg(0.5*np.arctan2(data_eta, data_xi)) - rotation_angle # need rotation correction, phi -> phi - rotation_angle data_epsilon = np.sqrt(data_xi**2 + data_eta**2) data_xi = data_epsilon * np.cos(2*np.deg2rad(data_phi)) data_eta = data_epsilon * np.sin(2*np.deg2rad(data_phi)) if field == "xi": points[:, 3] = data_xi elif field == "eta": points[:, 3] = data_eta else: # field == "phi" points[:, 3] = data_phi else: # field = "vel", dlnv, epsilon points[:, 3] = interpn( (resampled.coords['rad'].values, resampled.coords['lat'].values, resampled.coords['lon'].values), resampled.data_vars[field].values, points[:, 0:3] ) # when rotate is not None, only "phi" need special treatment else: if (field == "phi"): data_xi = interpn( (resampled.coords['rad'].values, resampled.coords['lat'].values, resampled.coords['lon'].values), resampled.data_vars["xi"].values, points[:, 0:3] ) data_eta = interpn( (resampled.coords['rad'].values, resampled.coords['lat'].values, resampled.coords['lon'].values), resampled.data_vars["eta"].values, points[:, 0:3] ) data_phi = np.rad2deg(0.5*np.arctan2(data_eta, data_xi)) points[:, 3] = data_phi else: points[:, 3] = interpn( (resampled.coords['rad'].values, resampled.coords['lat'].values, resampled.coords['lon'].values), resampled.data_vars[field].values, points[:, 0:3] ) data = points[:, [2, 1, 3]] if rotate is not None: data[:, 1], data[:, 0] = rtp_rotation_reverse(data[:, 1], data[:, 0], central_lat, central_lon, rotation_angle) return data
[docs] def interp_sec( self, start_point: Sequence[float], end_point: Sequence[float], field: str, val: float = 10.0, flat_earth: bool = False, rotate: Sequence[float] | None = None, section_coord_type: Literal["cal", "phy"] = "cal", output_coord_type: Literal["cal", "phy"] = "cal", ) -> np.ndarray: """Interpolate value along a cross section :param start_point: start point with [lon1, lat1] :type start_point: list or tuple :param end_point: end points with [lon2, lat2] :type end_point: list or tuple :param field: Field name in ATT model data :type field: str :param val: interval between successive points in km :type val: float :param flat_earth: whether to use flat earth model, defaults to False :type flat_earth: bool, optional :param rotate: Rotation parameters ``[central_lat, central_lon, rotation_angle]`` in degrees. ``central_lat`` and ``central_lon`` are physical coordinates. Defaults to None. :param section_coord_type: Coordinate type of ``start_point`` and ``end_point``. Use "cal" for TomoATT computational coordinates or "phy" for physical coordinates. Defaults to "cal". :type section_coord_type: str, optional :param output_coord_type: Coordinate type of longitude and latitude in the returned array. Use "cal" for TomoATT computational coordinates or "phy" for physical coordinates. Defaults to "cal". :type output_coord_type: str, optional :return: xyz data with 5 columns [lon, lat, dis, dep, value] :rtype: :class:`numpy.ndarray` """ # Check section and output coordinate types valid_coord_types = {"cal", "phy"} if section_coord_type not in valid_coord_types: raise ValueError( "section_coord_type must be 'cal' or 'phy'" ) if output_coord_type not in valid_coord_types: raise ValueError( "output_coord_type must be 'cal' or 'phy'" ) if section_coord_type == "phy" and rotate is None: raise ValueError( "rotate must be provided when section_coord_type is 'phy'" ) if output_coord_type == "phy" and rotate is None: raise ValueError( "rotate must be provided when output_coord_type is 'phy'" ) if rotate is not None: try: # rotate reversely, from computational grid to physical grid central_lat, central_lon, rotation_angle = rotate except (TypeError, ValueError): raise ValueError( "rotate must be a 3-item sequence: [central_lat, central_lon, rotation_angle]" ) # Rotate section endpoints to computational coordinates when needed. if section_coord_type == "phy": new_start_point = np.zeros(2) new_end_point = np.zeros(2) new_start_point[1], new_start_point[0] = rtp_rotation( start_point[1], start_point[0], central_lat, central_lon, rotation_angle ) new_end_point[1], new_end_point[0] = rtp_rotation( end_point[1], end_point[0], central_lat, central_lon, rotation_angle ) else: new_start_point = start_point new_end_point = end_point # Initialize a profile if flat_earth: sec_points, sec_range = interpolation_lola_linear(new_start_point, new_end_point, val) else: g = Geod(ellps='WGS84') az, _, dist = g.inv( new_start_point[0],new_start_point[1], new_end_point[0],new_end_point[1], return_back_azimuth=False ) sec_range = np.arange(0, dist/1000, val) r = g.fwd_intermediate( new_start_point[0],new_start_point[1], az, npts=sec_range.size, del_s=val*1000 ) sec_points = np.array([r.lons, r.lats]).T # create points array points = np.zeros([sec_range.size*self.coords['dep'].size, 5]) offset = 0 for i, lola in enumerate(sec_points): for _, rad in enumerate(self.coords['rad'].values): points[offset] = [rad, lola[1], lola[0], sec_range[i], 0.] offset += 1 # Interpolation # points[:, 4] = interpn( # (self.coords['rad'].values, # self.coords['lat'].values, # self.coords['lon'].values), # self.data_vars[field].values, # points[:, 0:3], # bounds_error=False # ) # Angle can not be interpolated directly, 0 degree and 180 degree are the same. But the interpolation will give 90 degree. if (output_coord_type == "phy"): if ((field == "xi") or (field == "eta") or ((field == "phi"))): data_xi = interpn( (self.coords['rad'].values, self.coords['lat'].values, self.coords['lon'].values), self.data_vars["xi"].values, points[:, 0:3], bounds_error=False ) data_eta = interpn( (self.coords['rad'].values, self.coords['lat'].values, self.coords['lon'].values), self.data_vars["eta"].values, points[:, 0:3], bounds_error=False ) data_phi = np.rad2deg(0.5*np.arctan2(data_eta, data_xi)) - rotation_angle # need rotation correction, phi -> phi - rotation_angle data_epsilon = np.sqrt(data_xi**2 + data_eta**2) data_xi = data_epsilon * np.cos(2*np.deg2rad(data_phi)) data_eta = data_epsilon * np.sin(2*np.deg2rad(data_phi)) if field == "xi": points[:, 4] = data_xi elif field == "eta": points[:, 4] = data_eta else: # field == "phi" points[:, 4] = data_phi else: # field = "vel", dlnv, epsilon points[:, 4] = interpn( (self.coords['rad'].values, self.coords['lat'].values, self.coords['lon'].values), self.data_vars[field].values, points[:, 0:3], bounds_error=False ) # when rotate is not None, only "phi" need special treatment else: if (field == "phi"): data_xi = interpn( (self.coords['rad'].values, self.coords['lat'].values, self.coords['lon'].values), self.data_vars["xi"].values, points[:, 0:3], bounds_error=False ) data_eta = interpn( (self.coords['rad'].values, self.coords['lat'].values, self.coords['lon'].values), self.data_vars["eta"].values, points[:, 0:3], bounds_error=False ) data_phi = np.rad2deg(0.5*np.arctan2(data_eta, data_xi)) points[:, 4] = data_phi else: points[:, 4] = interpn( (self.coords['rad'].values, self.coords['lat'].values, self.coords['lon'].values), self.data_vars[field].values, points[:, 0:3], bounds_error=False ) points[:, 0] = _EARTH_RADIUS_KM - points[:, 0] data = points[:, [2, 1, 3, 0, 4]] # rotate reversely, from computational grid to physical grid if output_coord_type == "phy": data[:, 1], data[:, 0] = rtp_rotation_reverse( data[:, 1], data[:, 0], central_lat, central_lon, rotation_angle ) return data