Exemplo n.º 1
0
class Interpolator2D():
    """ Class for interpolating 2D (lat,lon) geospatial data.

        For irregular grids, the data values must be passed as a
        1d array and all three arrays (values, lats, lons) must have
        the same length.

        For regular grids, the data values must be passed as a
        2d array with shape (M,N) where M and N are the lengths
        of the latitude and longitude array, respectively.

        Attributes:
            values: 1d or 2d numpy array
                Values to be interpolated
            lats: 1d numpy array
                Latitude values
            lons: 1d numpy array
                Longitude values
            origin: tuple(float,float)
                Reference location (origo of XY coordinate system).
            method_irreg : {‘linear’, ‘nearest’, ‘cubic’, ‘regularize’}, optional
                Interpolation method used for irregular grids.
                Note that 'nearest' is usually significantly faster than
                the 'linear' and 'cubic'.
                If the 'regularize' is selected, the data is first mapped onto
                a regular grid by means of a linear interpolation (for points outside
                the area covered by the data, a nearest-point interpolation is used).
                The bin size of the regular grid is specified via the reg_bin argument.
            bins_irreg_max: int
                Maximum number of bins along either axis of the regular grid onto which
                the irregular data is mapped. Only relevant if method_irreg is set to
                'regularize'. Default is 2000.
    """
    def __init__(self,
                 values,
                 lats,
                 lons,
                 origin=None,
                 method_irreg='regularize',
                 bins_irreg_max=400):

        # compute coordinates of origin, if not provided
        if origin is None: origin = center_point(lats, lons)
        self.origin = origin

        # check if bathymetry data are on a regular or irregular grid
        reggrid = (np.ndim(values) == 2)

        # convert to radians
        lats_rad, lons_rad = torad(lats, lons)

        # necessary to resolve a mismatch between scipy and underlying Fortran code
        # https://github.com/scipy/scipy/issues/6556
        if np.min(lons_rad) < 0: self._lon_corr = np.pi
        else: self._lon_corr = 0
        lons_rad += self._lon_corr

        # initialize lat-lon interpolator
        if reggrid:  # regular grid
            if len(lats) > 2 and len(lons) > 2:
                self.interp_ll = RectSphereBivariateSpline(u=lats_rad,
                                                           v=lons_rad,
                                                           r=values)
            elif len(lats) > 1 and len(lons) > 1:
                z = np.swapaxes(values, 0, 1)
                self.interp_ll = interp2d(x=lats_rad,
                                          y=lons_rad,
                                          z=z,
                                          kind='linear')
            elif len(lats) == 1:
                self.interp_ll = interp1d(x=lons_rad,
                                          y=np.squeeze(values),
                                          kind='linear')
            elif len(lons) == 1:
                self.interp_ll = interp1d(x=lats_rad,
                                          y=np.squeeze(values),
                                          kind='linear')

        else:  # irregular grid
            if len(np.unique(lats)) <= 1 or len(np.unique(lons)) <= 1:
                self.interp_ll = GridData2D(u=lats_rad,
                                            v=lons_rad,
                                            r=values,
                                            method='nearest')

            elif method_irreg == 'regularize':

                # initialize interpolators on irregular grid
                if len(np.unique(lats)) >= 2 and len(np.unique(lons)) >= 2:
                    method = 'linear'
                else:
                    method = 'nearest'
                gd = GridData2D(u=lats_rad,
                                v=lons_rad,
                                r=values,
                                method=method)
                gd_near = GridData2D(u=lats_rad,
                                     v=lons_rad,
                                     r=values,
                                     method='nearest')

                # determine bin size for regular grid
                lat_diffs = np.diff(np.sort(np.unique(lats)))
                lat_diffs = lat_diffs[lat_diffs > 1e-4]
                lon_diffs = np.diff(np.sort(np.unique(lons)))
                lon_diffs = lon_diffs[lon_diffs > 1e-4]
                bin_size = (np.min(lat_diffs), np.min(lon_diffs))

                # regular grid that data will be mapped to
                lats_reg, lons_reg = self._create_grid(lats=lats,
                                                       lons=lons,
                                                       bin_size=bin_size,
                                                       max_bins=bins_irreg_max)

                # map to regular grid
                lats_reg_rad, lons_reg_rad = torad(lats_reg, lons_reg)
                lons_reg_rad += self._lon_corr
                vi = gd(theta=lats_reg_rad, phi=lons_reg_rad, grid=True)
                vi_near = gd_near(theta=lats_reg_rad,
                                  phi=lons_reg_rad,
                                  grid=True)
                indices_nan = np.where(np.isnan(vi))
                vi[indices_nan] = vi_near[indices_nan]

                # initialize interpolator on regular grid
                self.interp_ll = RectSphereBivariateSpline(u=lats_reg_rad,
                                                           v=lons_reg_rad,
                                                           r=vi)

            else:
                self.interp_ll = GridData2D(u=lats_rad,
                                            v=lons_rad,
                                            r=values,
                                            method=method_irreg)

        # store data used for interpolation
        self.lat_nodes = lats
        self.lon_nodes = lons
        self.values = values

    def get_nodes(self):
        return (self.values, self.lat_nodes, self.lon_nodes)

    def interp_xy(self, x, y, grid=False, x_deriv_order=0, y_deriv_order=0):
        """ Interpolate using planar coordinate system (xy).

            x and y can be floats or arrays.

            If grid is set to False, the interpolation will be evaluated at
            the positions (x_i, y_i), where x=(x_1,...,x_N) and
            y=(y_1,...,y_N). Note that in this case, x and y must have
            the same length.

            If grid is set to True, the interpolation will be evaluated at
            all combinations (x_i, y_j), where x=(x_1,...,x_N) and
            y=(y_1,...,y_M). Note that in this case, the lengths of x
            and y do not have to be the same.

            Args:
                x: float or array
                   x-coordinate of the positions(s) where the interpolation is to be evaluated
                y: float or array
                   y-coordinate of the positions(s) where the interpolation is to be evaluated
                grid: bool
                   Specify how to combine elements of x and y.
                x_deriv_order: int
                    Order of x-derivative
                y_deriv_order: int
                    Order of y-derivative

            Returns:
                zi: Interpolated interpolation values
        """
        lat, lon = XYtoLL(x=x,
                          y=y,
                          lat_ref=self.origin[0],
                          lon_ref=self.origin[1],
                          grid=grid)

        if grid:
            M = lat.shape[0]
            N = lat.shape[1]
            lat = np.reshape(lat, newshape=(M * N))
            lon = np.reshape(lon, newshape=(M * N))

        zi = self.interp(lat=lat,
                         lon=lon,
                         squeeze=False,
                         lat_deriv_order=y_deriv_order,
                         lon_deriv_order=x_deriv_order)

        if x_deriv_order + y_deriv_order > 0:
            r = DLDL_over_DXDY(lat=lat,
                               lat_deriv_order=y_deriv_order,
                               lon_deriv_order=x_deriv_order)
            zi *= r

        if grid:
            zi = np.reshape(zi, newshape=(M, N))

        if np.ndim(zi) == 2:
            zi = np.swapaxes(zi, 0, 1)

        zi = np.squeeze(zi)

        if np.ndim(zi) == 0 or (np.ndim(zi) == 1 and len(zi) == 1):
            zi = float(zi)

        return zi

    def interp(self,
               lat,
               lon,
               grid=False,
               squeeze=True,
               lat_deriv_order=0,
               lon_deriv_order=0):
        """ Interpolate using spherical coordinate system (latitude-longitude).

            lat and lot can be floats or arrays.

            If grid is set to False, the interpolation will be evaluated at
            the coordinates (lat_i, lon_i), where lat=(lat_1,...,lat_N)
            and lon=(lon_1,...,lon_N). Note that in this case, lat and
            lon must have the same length.

            If grid is set to True, the interpolation will be evaluated at
            all combinations (lat_i, lon_j), where lat=(lat_1,...,lat_N)
            and lon=(lon_1,...,lon_M). Note that in this case, the lengths
            of lat and lon do not have to be the same.

            Derivates are given per radians^n, where n is the overall
            derivative order.

            Args:
                lat: float or array
                    latitude of the positions(s) where the interpolation is to be evaluated
                lon: float or array
                    longitude of the positions(s) where the interpolation is to be evaluated
                grid: bool
                    Specify how to combine elements of lat and lon. If lat and lon have different
                    lengths, specifying grid has no effect as it is automatically set to True.
                lat_deriv_order: int
                    Order of latitude-derivative
                lon_deriv_order: int
                    Order of longitude-derivative

            Returns:
                zi: Interpolated values (or derivates)
        """
        lat = np.squeeze(np.array(lat))
        lon = np.squeeze(np.array(lon))
        lat_rad, lon_rad = torad(lat, lon)
        lon_rad += self._lon_corr

        if isinstance(self.interp_ll, interp2d):
            zi = self.interp_ll.__call__(x=lat_rad,
                                         y=lon_rad,
                                         dx=lat_deriv_order,
                                         dy=lon_deriv_order)
            if grid: zi = np.swapaxes(zi, 0, 1)
            if not grid and np.ndim(zi) == 2: zi = np.diagonal(zi)

        elif isinstance(self.interp_ll, interp1d):
            if len(self.lat_nodes) > 1:
                zi = self.interp_ll(x=lat_rad)
            elif len(self.lon_nodes) > 1:
                zi = self.interp_ll(x=lon_rad)

        else:
            zi = self.interp_ll.__call__(theta=lat_rad,
                                         phi=lon_rad,
                                         grid=grid,
                                         dtheta=lat_deriv_order,
                                         dphi=lon_deriv_order)

        if squeeze:
            zi = np.squeeze(zi)

        if np.ndim(zi) == 0 or (np.ndim(zi) == 1 and len(zi) == 1):
            zi = float(zi)

        return zi

    def _create_grid(self, lats, lons, bin_size, max_bins):
        """ Created regular lat-lon grid with uniform spacing that covers
            a set of (lat,lon) coordinates.

            Args:
                lats: numpy.array
                    Latitude values in degrees
                lons: numpy.array
                    Longitude values in degrees
                bin_size: float or tuple(float,float)
                    Lat and long bin size
                max_bins: int
                    Maximum number of bins along either axis

            Returns:
                : numpy.array, numpy.array
                    Latitude and longitude values of the regular grid
        """
        if isinstance(bin_size, (int, float)): bin_size = (bin_size, bin_size)
        res = []
        for v, dv in zip([lats, lons], bin_size):
            v_min = np.min(v) - dv
            v_max = np.max(v) + dv
            num = max(3, int((v_max - v_min) / dv) + 1)
            num = min(max_bins, num)
            v_reg = np.linspace(v_min, v_max, num=num)
            res.append(v_reg)

        return tuple(res)