Source code for cartopy.util

# Copyright Crown and Cartopy Contributors
#
# This file is part of Cartopy and is released under the BSD 3-clause license.
# See LICENSE in the root of the repository for full licensing details.
"""
Utilities that are useful in conjunction with cartopy.

"""
import numpy as np
import numpy.ma as ma


[docs] def add_cyclic_point(data, coord=None, axis=-1): """ Add a cyclic point to an array and optionally a corresponding coordinate. Parameters ---------- data An n-dimensional array of data to add a cyclic point to. coord : optional A 1-dimensional array which specifies the coordinate values for the dimension the cyclic point is to be added to. The coordinate values must be regularly spaced. Defaults to None. axis : optional Specifies the axis of the data array to add the cyclic point to. Defaults to the right-most axis. Returns ------- cyclic_data The data array with a cyclic point added. cyclic_coord The coordinate with a cyclic point, only returned if the coord keyword was supplied. Examples -------- Adding a cyclic point to a data array, where the cyclic dimension is the right-most dimension. >>> import numpy as np >>> data = np.ones([5, 6]) * np.arange(6) >>> cyclic_data = add_cyclic_point(data) >>> print(cyclic_data) # doctest: +NORMALIZE_WHITESPACE [[0. 1. 2. 3. 4. 5. 0.] [0. 1. 2. 3. 4. 5. 0.] [0. 1. 2. 3. 4. 5. 0.] [0. 1. 2. 3. 4. 5. 0.] [0. 1. 2. 3. 4. 5. 0.]] Adding a cyclic point to a data array and an associated coordinate >>> lons = np.arange(0, 360, 60) >>> cyclic_data, cyclic_lons = add_cyclic_point(data, coord=lons) >>> print(cyclic_data) # doctest: +NORMALIZE_WHITESPACE [[0. 1. 2. 3. 4. 5. 0.] [0. 1. 2. 3. 4. 5. 0.] [0. 1. 2. 3. 4. 5. 0.] [0. 1. 2. 3. 4. 5. 0.] [0. 1. 2. 3. 4. 5. 0.]] >>> print(cyclic_lons) [ 0 60 120 180 240 300 360] """ if coord is not None: if coord.ndim != 1: raise ValueError('The coordinate must be 1-dimensional.') if len(coord) != data.shape[axis]: raise ValueError(f'The length of the coordinate does not match ' f'the size of the corresponding dimension of ' f'the data array: len(coord) = {len(coord)}, ' f'data.shape[{axis}] = {data.shape[axis]}.') delta_coord = np.diff(coord) if not np.allclose(delta_coord, delta_coord[0]): raise ValueError('The coordinate must be equally spaced.') new_coord = ma.concatenate((coord, coord[-1:] + delta_coord[0])) slicer = [slice(None)] * data.ndim try: slicer[axis] = slice(0, 1) except IndexError: raise ValueError('The specified axis does not correspond to an ' 'array dimension.') new_data = ma.concatenate((data, data[tuple(slicer)]), axis=axis) if coord is None: return_value = new_data else: return_value = new_data, new_coord return return_value
def _add_cyclic_data(data, axis=-1): """ Add a cyclic point to a data array. Parameters ---------- data : ndarray An n-dimensional array of data to add a cyclic point to. axis : int, optional Specifies the axis of the data array to add the cyclic point to. Defaults to the right-most axis. Returns ------- The data array with a cyclic point added. """ slicer = [slice(None)] * data.ndim try: slicer[axis] = slice(0, 1) except IndexError: raise ValueError( 'The specified axis does not correspond to an array dimension.') npc = np.ma if np.ma.is_masked(data) else np return npc.concatenate((data, data[tuple(slicer)]), axis=axis) def _add_cyclic_x(x, axis=-1, cyclic=360): """ Add a cyclic point to a x/longitude coordinate array. Parameters ---------- x : ndarray An array which specifies the x-coordinate values for the dimension the cyclic point is to be added to. axis : int, optional Specifies the axis of the x-coordinate array to add the cyclic point to. Defaults to the right-most axis. cyclic : float, optional Width of periodic domain (default: 360) Returns ------- The coordinate array ``x`` with a cyclic point added. """ npc = np.ma if np.ma.is_masked(x) else np # get cyclic x-coordinates # cx is the code from basemap (addcyclic) # https://github.com/matplotlib/basemap/blob/master/lib/mpl_toolkits/basemap/__init__.py cx = (np.take(x, [0], axis=axis) + cyclic * np.sign(np.diff(np.take(x, [0, -1], axis=axis), axis=axis))) # basemap ensures that the values do not exceed cyclic # (next code line). We do not do this to deal with rotated grids that # might have values not exactly 0. # cx = npc.where(cx <= cyclic, cx, np.mod(cx, cyclic)) return npc.concatenate((x, cx), axis=axis) def has_cyclic(x, axis=-1, cyclic=360, precision=1e-4): """ Check if x/longitude coordinates already have a cyclic point. Checks all differences between the first and last x-coordinates along ``axis`` to be less than ``precision``. Parameters ---------- x : ndarray An array with the x-coordinate values to be checked for cyclic points. axis : int, optional Specifies the axis of the ``x`` array to be checked. Defaults to the right-most axis. cyclic : float, optional Width of periodic domain (default: 360). precision : float, optional Maximal difference between first and last x-coordinate to detect cyclic point (default: 1e-4). Returns ------- True if a cyclic point was detected along the given axis, False otherwise. Examples -------- Check for cyclic x-coordinate in one dimension. >>> import numpy as np >>> lons = np.arange(0, 360, 60) >>> clons = np.arange(0, 361, 60) >>> print(has_cyclic(lons)) False >>> print(has_cyclic(clons)) True Check for cyclic x-coordinate in two dimensions. >>> lats = np.arange(-90, 90, 30) >>> lon2d, lat2d = np.meshgrid(lons, lats) >>> clon2d, clat2d = np.meshgrid(clons, lats) >>> print(has_cyclic(lon2d)) False >>> print(has_cyclic(clon2d)) True """ npc = np.ma if np.ma.is_masked(x) else np # transform to 0-cyclic, assuming e.g. -180 to 180 if any < 0 x1 = np.mod(npc.where(x < 0, x + cyclic, x), cyclic) dd = np.diff(np.take(x1, [0, -1], axis=axis), axis=axis) if npc.all(np.abs(dd) < precision): return True else: return False
[docs] def add_cyclic(data, x=None, y=None, axis=-1, cyclic=360, precision=1e-4): """ Add a cyclic point to an array and optionally corresponding x/longitude and y/latitude coordinates. Checks all differences between the first and last x-coordinates along ``axis`` to be less than ``precision``. Parameters ---------- data : ndarray An n-dimensional array of data to add a cyclic point to. x : ndarray, optional An n-dimensional array which specifies the x-coordinate values for the dimension the cyclic point is to be added to, i.e. normally the longitudes. Defaults to None. If ``x`` is given then *add_cyclic* checks if a cyclic point is already present by checking all differences between the first and last coordinates to be less than ``precision``. No point is added if a cyclic point was detected. If ``x`` is 1- or 2-dimensional, ``x.shape[-1]`` must equal ``data.shape[axis]``, otherwise ``x.shape[axis]`` must equal ``data.shape[axis]``. y : ndarray, optional An n-dimensional array with the values of the y-coordinate, i.e. normally the latitudes. The cyclic point simply copies the last column. Defaults to None. No cyclic point is added if ``y`` is 1-dimensional. If ``y`` is 2-dimensional, ``y.shape[-1]`` must equal ``data.shape[axis]``, otherwise ``y.shape[axis]`` must equal ``data.shape[axis]``. axis : int, optional Specifies the axis of the arrays to add the cyclic point to, i.e. axis with changing x-coordinates. Defaults to the right-most axis. cyclic : int or float, optional Width of periodic domain (default: 360). precision : float, optional Maximal difference between first and last x-coordinate to detect cyclic point (default: 1e-4). Returns ------- cyclic_data The data array with a cyclic point added. cyclic_x The x-coordinate with a cyclic point, only returned if the ``x`` keyword was supplied. cyclic_y The y-coordinate with the last column of the cyclic axis duplicated, only returned if ``x`` was 2- or n-dimensional and the ``y`` keyword was supplied. Examples -------- Adding a cyclic point to a data array, where the cyclic dimension is the right-most dimension. >>> import numpy as np >>> data = np.ones([5, 6]) * np.arange(6) >>> cyclic_data = add_cyclic(data) >>> print(cyclic_data) # doctest: +NORMALIZE_WHITESPACE [[0. 1. 2. 3. 4. 5. 0.] [0. 1. 2. 3. 4. 5. 0.] [0. 1. 2. 3. 4. 5. 0.] [0. 1. 2. 3. 4. 5. 0.] [0. 1. 2. 3. 4. 5. 0.]] Adding a cyclic point to a data array and an associated x-coordinate. >>> lons = np.arange(0, 360, 60) >>> cyclic_data, cyclic_lons = add_cyclic(data, x=lons) >>> print(cyclic_data) # doctest: +NORMALIZE_WHITESPACE [[0. 1. 2. 3. 4. 5. 0.] [0. 1. 2. 3. 4. 5. 0.] [0. 1. 2. 3. 4. 5. 0.] [0. 1. 2. 3. 4. 5. 0.] [0. 1. 2. 3. 4. 5. 0.]] >>> print(cyclic_lons) [ 0 60 120 180 240 300 360] Adding a cyclic point to a data array and an associated 2-dimensional x-coordinate. >>> lons = np.arange(0, 360, 60) >>> lats = np.arange(-90, 90, 180/5) >>> lon2d, lat2d = np.meshgrid(lons, lats) >>> cyclic_data, cyclic_lon2d = add_cyclic(data, x=lon2d) >>> print(cyclic_data) # doctest: +NORMALIZE_WHITESPACE [[0. 1. 2. 3. 4. 5. 0.] [0. 1. 2. 3. 4. 5. 0.] [0. 1. 2. 3. 4. 5. 0.] [0. 1. 2. 3. 4. 5. 0.] [0. 1. 2. 3. 4. 5. 0.]] >>> print(cyclic_lon2d) [[ 0 60 120 180 240 300 360] [ 0 60 120 180 240 300 360] [ 0 60 120 180 240 300 360] [ 0 60 120 180 240 300 360] [ 0 60 120 180 240 300 360]] Adding a cyclic point to a data array and the associated 2-dimensional x- and y-coordinates. >>> lons = np.arange(0, 360, 60) >>> lats = np.arange(-90, 90, 180/5) >>> lon2d, lat2d = np.meshgrid(lons, lats) >>> cyclic_data, cyclic_lon2d, cyclic_lat2d = add_cyclic( ... data, x=lon2d, y=lat2d) >>> print(cyclic_data) # doctest: +NORMALIZE_WHITESPACE [[0. 1. 2. 3. 4. 5. 0.] [0. 1. 2. 3. 4. 5. 0.] [0. 1. 2. 3. 4. 5. 0.] [0. 1. 2. 3. 4. 5. 0.] [0. 1. 2. 3. 4. 5. 0.]] >>> print(cyclic_lon2d) [[ 0 60 120 180 240 300 360] [ 0 60 120 180 240 300 360] [ 0 60 120 180 240 300 360] [ 0 60 120 180 240 300 360] [ 0 60 120 180 240 300 360]] >>> print(cyclic_lat2d) [[-90. -90. -90. -90. -90. -90. -90.] [-54. -54. -54. -54. -54. -54. -54.] [-18. -18. -18. -18. -18. -18. -18.] [ 18. 18. 18. 18. 18. 18. 18.] [ 54. 54. 54. 54. 54. 54. 54.]] Not adding a cyclic point if cyclic point detected in x. >>> lons = np.arange(0, 361, 72) >>> lats = np.arange(-90, 90, 180/5) >>> lon2d, lat2d = np.meshgrid(lons, lats) >>> cyclic_data, cyclic_lon2d, cyclic_lat2d = add_cyclic( ... data, x=lon2d, y=lat2d) >>> print(cyclic_data) # doctest: +NORMALIZE_WHITESPACE [[0. 1. 2. 3. 4. 5.] [0. 1. 2. 3. 4. 5.] [0. 1. 2. 3. 4. 5.] [0. 1. 2. 3. 4. 5.] [0. 1. 2. 3. 4. 5.]] >>> print(cyclic_lon2d) [[ 0 72 144 216 288 360] [ 0 72 144 216 288 360] [ 0 72 144 216 288 360] [ 0 72 144 216 288 360] [ 0 72 144 216 288 360]] >>> print(cyclic_lat2d) [[-90. -90. -90. -90. -90. -90.] [-54. -54. -54. -54. -54. -54.] [-18. -18. -18. -18. -18. -18.] [ 18. 18. 18. 18. 18. 18.] [ 54. 54. 54. 54. 54. 54.]] """ if x is None: return _add_cyclic_data(data, axis=axis) # if x was given if x.ndim > 2: xaxis = axis else: xaxis = -1 if x.shape[xaxis] != data.shape[axis]: estr = (f'x.shape[{xaxis}] does not match the size of the' f' corresponding dimension of the data array:' f' x.shape[{xaxis}] = {x.shape[xaxis]},' f' data.shape[{axis}] = {data.shape[axis]}.') raise ValueError(estr) if has_cyclic(x, axis=xaxis, cyclic=cyclic, precision=precision): if y is None: return data, x # if y was given return data, x, y # if not has_cyclic, add cyclic points to data and x out_data = _add_cyclic_data(data, axis=axis) out_x = _add_cyclic_x(x, axis=xaxis, cyclic=cyclic) if y is None: return out_data, out_x # if y was given if y.ndim == 1: return out_data, out_x, y if y.ndim > 2: yaxis = axis else: yaxis = -1 if y.shape[yaxis] != data.shape[axis]: estr = (f'y.shape[{yaxis}] does not match the size of the' f' corresponding dimension of the data array:' f' y.shape[{yaxis}] = {y.shape[yaxis]},' f' data.shape[{axis}] = {data.shape[axis]}.') raise ValueError(estr) out_y = _add_cyclic_data(y, axis=yaxis) return out_data, out_x, out_y