Esempio n. 1
0
    def geocentric_cartesian(
        cube: Cube, x_coords: ndarray, y_coords: ndarray
    ) -> ndarray:
        """
        A function to convert a global (lat/lon) coordinate system into a
        geocentric (3D trignonometric) system. This function ignores orographic
        height differences between coordinates, giving a 2D projected
        neighbourhood akin to selecting a neighbourhood of grid points about a
        point without considering their vertical displacement.

        Args:
            cube:
                A cube from which is taken the globe for which the geocentric
                coordinates are being calculated.
            x_coords:
                An array of x coordinates that will represent one axis of the
                mesh of coordinates to be transformed.
            y_coords:
                An array of y coordinates that will represent one axis of the
                mesh of coordinates to be transformed.

        Returns:
            An array of all the xyz combinations that describe the nodes of
            the grid, now in 3D geocentric cartesian coordinates. The shape
            of the array is (n_nodes, 3), order x[:, 0], y[:, 1], z[:, 2].
        """
        coordinate_system = cube.coord_system().as_cartopy_crs()
        cartesian_calculator = coordinate_system.as_geocentric()
        z_coords = np.zeros_like(x_coords)
        cartesian_nodes = cartesian_calculator.transform_points(
            coordinate_system, x_coords, y_coords, z_coords
        )
        return cartesian_nodes
Esempio n. 2
0
def transform_grid_to_lat_lon(cube: Cube) -> Tuple[ndarray, ndarray]:
    """
    Calculate the latitudes and longitudes of each points in the cube.

    Args:
        cube:
            Cube with points to transform

    Returns
        lats:
            Array of cube.data.shape of Latitude values
        lons:
            Array of cube.data.shape of Longitude values
    """
    trg_latlon = ccrs.PlateCarree()
    trg_crs = cube.coord_system().as_cartopy_crs()
    x_points = cube.coord(axis="x").points
    y_points = cube.coord(axis="y").points
    x_zeros = np.zeros_like(x_points)
    y_zeros = np.zeros_like(y_points)

    # Broadcast x points and y points onto grid
    all_x_points = y_zeros.reshape(len(y_zeros), 1) + x_points
    all_y_points = y_points.reshape(len(y_points), 1) + x_zeros

    # Transform points
    points = trg_latlon.transform_points(trg_crs, all_x_points, all_y_points)
    lons = points[..., 0]
    lats = points[..., 1]

    return lats, lons
    def _load_data(self, array_path, group_dims, grid_mapping,
                   attr_name=None, separator='__', handle_nan=None):
        """
        Create an Iris cube from a TileDB array describing a data variable and
        pre-loaded dimension-describing coordinates.

        TODO not handled here: aux coords and dims, cell measures, aux factories.

        """
        single_attr_name = 'dataset'
        if attr_name is None:
            attr_metadata, lazy_data = self._from_tdb_array(array_path, single_attr_name,
                                                            to_dask=True, handle_nan=handle_nan)
            metadata = attr_metadata
            attr_name = metadata.pop(single_attr_name)
        else:
            attr_metadata, lazy_data = self._from_tdb_array(array_path,
                                                            single_attr_name,
                                                            array_name=attr_name,
                                                            to_dask=True,
                                                            handle_nan=handle_nan)
            metadata = {}
            for key, value in attr_metadata.items():
                # Varname-specific keys are of form `keyname__attrname`; we only want `keyname`.
                # TODO pass the separator character to the method.
                try:
                    key_name, key_attr = key.split(separator)
                    if key_attr == attr_name:
                        metadata[key_name] = value
                except ValueError:
                    # Not all keys are varname-specific; we want all of these.
                    metadata[key] = value

        cell_methods = parse_cell_methods(metadata.pop('cell_methods', None))
        dim_names = metadata.pop('dimensions').split(',')
        # Dim Coords And Dims (mapping of coords to cube axes).
        dcad = [(group_dims[name], i) for i, name in enumerate(dim_names)]
        safe_attrs = self._handle_attributes(metadata,
                                             exclude_keys=['dataset', 'multiattr', 'grid_mapping'])
        std_name = metadata.pop('standard_name', None)
        long_name = metadata.pop('long_name', None)
        var_name = metadata.pop('var_name', None)
        if all(itm is None for itm in [std_name, long_name, var_name]):
            long_name = attr_name

        cube = Cube(lazy_data,
                    standard_name=std_name,
                    long_name=long_name,
                    var_name=var_name,
                    units=metadata.pop('units', '1'),
                    dim_coords_and_dims=dcad,
                    cell_methods=cell_methods,
                    attributes=safe_attrs)
        cube.coord_system = grid_mapping
        return cube
Esempio n. 4
0
def transform_grid_to_lat_lon(cube: Cube) -> Tuple[ndarray, ndarray]:
    """
    Calculate the latitudes and longitudes of each points in the cube.

    Args:
        cube:
            Cube with points to transform

    Returns
        lats:
            Array of cube.data.shape of Latitude values
        lons:
            Array of cube.data.shape of Longitude values
    """
    trg_latlon = ccrs.PlateCarree()
    trg_crs = cube.coord_system().as_cartopy_crs()
    cube = cube.copy()
    # TODO use the proj units that are accesible with later versions of proj
    # to determine the default units to convert to for a given projection.

    # Assuming proj units of metre for all projections not in degrees.
    for axis in ["x", "y"]:
        try:
            cube.coord(axis=axis).convert_units("m")
        except ValueError as err:
            msg = (
                "Cube passed to transform_grid_to_lat_lon does not have an "
                f"{axis} coordinate with units that can be converted to metres. "
            )
            raise ValueError(msg + str(err))

    x_points = cube.coord(axis="x").points
    y_points = cube.coord(axis="y").points
    x_zeros = np.zeros_like(x_points)
    y_zeros = np.zeros_like(y_points)

    # Broadcast x points and y points onto grid
    all_x_points = y_zeros.reshape(len(y_zeros), 1) + x_points
    all_y_points = y_points.reshape(len(y_points), 1) + x_zeros

    # Transform points
    points = trg_latlon.transform_points(trg_crs, all_x_points, all_y_points)
    lons = points[..., 0]
    lats = points[..., 1]

    return lats, lons
Esempio n. 5
0
    def different_projection(self, method, ancillary_data, additional_data,
                             expected, **kwargs):
        """Test that the plugin copes with non-lat/lon grids."""

        trg_crs = None
        src_crs = ccrs.PlateCarree()
        trg_crs = ccrs.LambertConformal(central_longitude=50,
                                        central_latitude=10)
        trg_crs_iris = coord_systems.LambertConformal(central_lon=50,
                                                      central_lat=10)
        lons = self.cube.coord('longitude').points
        lats = self.cube.coord('latitude').points
        x, y = [], []
        for lon, lat in zip(lons, lats):
            x_trg, y_trg = trg_crs.transform_point(lon, lat, src_crs)
            x.append(x_trg)
            y.append(y_trg)

        new_x = AuxCoord(x,
                         standard_name='projection_x_coordinate',
                         units='m',
                         coord_system=trg_crs_iris)
        new_y = AuxCoord(y,
                         standard_name='projection_y_coordinate',
                         units='m',
                         coord_system=trg_crs_iris)

        cube = Cube(self.cube.data,
                    long_name="air_temperature",
                    dim_coords_and_dims=[(self.cube.coord('time'), 0)],
                    aux_coords_and_dims=[(new_y, 1), (new_x, 2)],
                    units="K")

        plugin = Plugin(method)
        with iris.FUTURE.context(cell_datetime_objects=True):
            cube = cube.extract(self.time_extract)
        result = plugin.process(cube, self.sites, self.neighbour_list,
                                ancillary_data, additional_data, **kwargs)

        self.assertEqual(cube.coord_system(), trg_crs_iris)
        self.assertAlmostEqual(result.data, expected)
        self.assertEqual(result.coord(axis='y').name(), 'latitude')
        self.assertEqual(result.coord(axis='x').name(), 'longitude')
        self.assertAlmostEqual(result.coord(axis='y').points, 4.74)
        self.assertAlmostEqual(result.coord(axis='x').points, 9.47)
Esempio n. 6
0
def lat_lon_determine(cube: Cube) -> Optional[CRS]:
    """
    Test whether a diagnostic cube is on a latitude/longitude grid or uses an
    alternative projection.

    Args:
        cube:
            A diagnostic cube to examine for coordinate system.

    Returns:
        Coordinate system of the diagnostic cube in a cartopy format unless
        it is already a latitude/longitude grid, in which case None is
        returned.
    """
    trg_crs = None
    if (not cube.coord(axis="x").name() == "longitude"
            or not cube.coord(axis="y").name() == "latitude"):
        trg_crs = cube.coord_system().as_cartopy_crs()
    return trg_crs
Esempio n. 7
0
def transform_grid_to_lat_lon(cube: Cube) -> Tuple[ndarray, ndarray]:
    """Calculate the latitudes and longitudes of each points in the cube.

    The result is defined over the spatial grid, of shape (ny, nx) where
    ny is the length of the y-axis coordinate and nx the length of the
    x-axis coordinate.

    Args:
        cube:
            Cube with points to transform

    Returns
        - Array of shape (ny, nx) containing grid latitude values
        - Array of shape (ny, nx) containing grid longitude values
    """
    trg_latlon = ccrs.PlateCarree()
    trg_crs = cube.coord_system().as_cartopy_crs()
    cube = cube.copy()
    # TODO use the proj units that are accesible with later versions of proj
    # to determine the default units to convert to for a given projection.

    # Assuming proj units of metre for all projections not in degrees.
    for axis in ["x", "y"]:
        try:
            cube.coord(axis=axis).convert_units("m")
        except ValueError as err:
            msg = (
                "Cube passed to transform_grid_to_lat_lon does not have an "
                f"{axis} coordinate with units that can be converted to metres. "
            )
            raise ValueError(msg + str(err))

    all_y_points, all_x_points = get_grid_y_x_values(cube)

    # Transform points
    points = trg_latlon.transform_points(trg_crs, all_x_points, all_y_points)
    lons = points[..., 0]
    lats = points[..., 1]

    return lats, lons
Esempio n. 8
0
    def calc_true_north_offset(reference_cube: Cube) -> ndarray:
        """
        Calculate the angles between grid North and true North, as a
        matrix of values on the grid of the input reference cube.

        Args:
            reference_cube:
                2D cube on grid for which "north" is required.  Provides both
                coordinate system (reference_cube.coord_system()) and template
                spatial grid on which the angle adjustments should be provided.

        Returns:
            Angle in radians by which wind direction wrt true North at
            each point must be rotated to be relative to grid North.
        """
        reference_x_coord = reference_cube.coord(axis="x")
        reference_y_coord = reference_cube.coord(axis="y")

        # find corners of reference_cube grid in lat / lon coordinates
        latlon = [
            GLOBAL_CRS.as_cartopy_crs().transform_point(
                reference_x_coord.points[i],
                reference_y_coord.points[j],
                reference_cube.coord_system().as_cartopy_crs(),
            )
            for i in [0, -1]
            for j in [0, -1]
        ]
        latlon = np.array(latlon).T.tolist()

        # define lat / lon coordinates to cover the reference_cube grid at an
        # equivalent resolution
        lat_points = np.linspace(
            np.floor(min(latlon[1])),
            np.ceil(max(latlon[1])),
            len(reference_y_coord.points),
        )
        lon_points = np.linspace(
            np.floor(min(latlon[0])),
            np.ceil(max(latlon[0])),
            len(reference_x_coord.points),
        )

        lat_coord = DimCoord(
            lat_points, "latitude", units="degrees", coord_system=GLOBAL_CRS
        )
        lon_coord = DimCoord(
            lon_points, "longitude", units="degrees", coord_system=GLOBAL_CRS
        )

        # define a unit vector wind towards true North over the lat / lon grid
        udata = np.zeros(reference_cube.shape, dtype=np.float32)
        vdata = np.ones(reference_cube.shape, dtype=np.float32)

        ucube_truenorth = Cube(
            udata,
            "grid_eastward_wind",
            dim_coords_and_dims=[(lat_coord, 0), (lon_coord, 1)],
        )
        vcube_truenorth = Cube(
            vdata,
            "grid_northward_wind",
            dim_coords_and_dims=[(lat_coord, 0), (lon_coord, 1)],
        )

        # rotate unit vector onto reference_cube coordinate system
        ucube, vcube = rotate_winds(
            ucube_truenorth, vcube_truenorth, reference_cube.coord_system()
        )

        # unmask and regrid rotated winds onto reference_cube grid
        ucube.data = ucube.data.data
        ucube = ucube.regrid(reference_cube, Linear())
        vcube.data = vcube.data.data
        vcube = vcube.regrid(reference_cube, Linear())

        # ratio of u to v winds is the tangent of the angle which is the
        # true North to grid North rotation
        angle_adjustment = np.arctan2(ucube.data, vcube.data)

        return angle_adjustment
Esempio n. 9
0
    def process(
        self, sites: List[Dict[str, Any]], orography: Cube, land_mask: Cube
    ) -> Cube:
        """
        Using the constraints provided, find the nearest grid point neighbours
        to the given spot sites for the model/grid given by the input cubes.
        Returned is a cube that contains the defining characteristics of the
        spot sites (e.g. x coordinate, y coordinate, altitude) and the indices
        of the selected grid point neighbour.

        Args:
            sites:
                A list of dictionaries defining the spot sites for which
                neighbours are to be found. e.g.:

                   [{'altitude': 11.0, 'latitude': 57.867000579833984,
                    'longitude': -5.632999897003174, 'wmo_id': 3034}]

            orography:
                A cube of orography, used to obtain the grid point altitudes.
            land_mask:
                A land mask cube for the model/grid from which grid point
                neighbours are being selected, with land points set to one and
                sea points set to zero.

        Returns:
            A cube containing both the spot site information and for each
            the grid point indices of its nearest neighbour as per the
            imposed constraints.
        """
        # Check if we are dealing with a global grid.
        self.global_coordinate_system = orography.coord(axis="x").circular

        # Exclude regional grids with spatial dimensions other than metres.
        if not self.global_coordinate_system:
            if not orography.coord(axis="x").units == "metres":
                msg = (
                    "Cube spatial coordinates for regional grids must be"
                    "in metres to match the defined search_radius."
                )
                raise ValueError(msg)

        # Ensure land_mask and orography are on the same grid.
        if not orography.dim_coords == land_mask.dim_coords:
            msg = "Orography and land_mask cubes are not on the same " "grid."
            raise ValueError(msg)

        # Enforce x-y coordinate order for input cubes.
        enforce_coordinate_ordering(
            orography,
            [orography.coord(axis="x").name(), orography.coord(axis="y").name()],
        )
        enforce_coordinate_ordering(
            land_mask,
            [land_mask.coord(axis="x").name(), land_mask.coord(axis="y").name()],
        )

        # Remap site coordinates on to coordinate system of the model grid.
        site_x_coords = np.array([site[self.site_x_coordinate] for site in sites])
        site_y_coords = np.array([site[self.site_y_coordinate] for site in sites])
        site_coords = self._transform_sites_coordinate_system(
            site_x_coords, site_y_coords, orography.coord_system().as_cartopy_crs()
        )

        # Exclude any sites falling outside the domain given by the cube and
        # notify the user.
        (
            sites,
            site_coords,
            site_x_coords,
            site_y_coords,
        ) = self.check_sites_are_within_domain(
            sites, site_coords, site_x_coords, site_y_coords, orography
        )

        # Find nearest neighbour point using quick iris method.
        nearest_indices = self.get_nearest_indices(site_coords, orography)

        # Create an array containing site altitudes, using the nearest point
        # orography height for any that are unset.
        site_altitudes = np.array(
            [site.get(self.site_altitude, None) for site in sites]
        )
        site_altitudes = np.where(
            np.isnan(site_altitudes.astype(float)),
            orography.data[tuple(nearest_indices.T)],
            site_altitudes,
        )

        # If further constraints are being applied, build a KD Tree which
        # includes points filtered by constraint.
        if self.land_constraint or self.minimum_dz:
            # Build the KDTree, an internal test for the land_constraint checks
            # whether to exclude sea points from the tree.
            tree, index_nodes = self.build_KDTree(land_mask)

            # Site coordinates made cartesian for global coordinate system
            if self.global_coordinate_system:
                site_coords = self.geocentric_cartesian(
                    orography, site_coords[:, 0], site_coords[:, 1]
                )

            if not self.minimum_dz:
                # Query the tree for the nearest neighbour, in this case a land
                # neighbour is returned along with the distance to it.
                distances, node_indices = tree.query([site_coords])
                # Look up the grid coordinates that correspond to the tree node
                (land_neighbour_indices,) = index_nodes[node_indices]
                # Use the found land neighbour if it is within the
                # search_radius, otherwise use the nearest neighbour.
                distances = np.array([distances[0], distances[0]]).T
                nearest_indices = np.where(
                    distances < self.search_radius,
                    land_neighbour_indices,
                    nearest_indices,
                )
            else:
                # Query the tree for self.node_limit nearby neighbours.
                distances, node_indices = tree.query(
                    [site_coords],
                    distance_upper_bound=self.search_radius,
                    k=self.node_limit,
                )
                # Loop over the sites and for each choose the returned
                # neighbour with the minimum vertical displacement.
                for index, (distance, indices) in enumerate(
                    zip(distances[0], node_indices[0])
                ):
                    grid_point = self.select_minimum_dz(
                        orography, site_altitudes[index], index_nodes, distance, indices
                    )
                    # None is returned if the tree query returned no neighbours
                    # within the search radius.
                    if grid_point is not None:
                        nearest_indices[index] = grid_point

        # Calculate the vertical displacements between the chosen grid point
        # and the spot site.
        vertical_displacements = (
            site_altitudes - orography.data[tuple(nearest_indices.T)]
        )

        # Create a list of WMO IDs if available. These are stored as strings
        # to accommodate the use of 'None' for unset IDs.
        wmo_ids = [str(site.get("wmo_id", None)) for site in sites]

        # Construct a name to describe the neighbour finding method employed
        method_name = self.neighbour_finding_method_name()

        # Create an array of indices and displacements to return
        data = np.stack(
            (nearest_indices[:, 0], nearest_indices[:, 1], vertical_displacements),
            axis=0,
        )
        data = np.expand_dims(data, 0).astype(np.float32)

        # Regardless of input sitelist coordinate system, the site coordinates
        # are stored as latitudes and longitudes in the neighbour cube.
        if self.site_coordinate_system != ccrs.PlateCarree():
            lon_lats = self._transform_sites_coordinate_system(
                site_x_coords, site_y_coords, ccrs.PlateCarree()
            )
            longitudes = lon_lats[:, 0]
            latitudes = lon_lats[:, 1]
        else:
            longitudes = site_x_coords
            latitudes = site_y_coords

        # Create a cube of neighbours
        neighbour_cube = build_spotdata_cube(
            data,
            "grid_neighbours",
            1,
            site_altitudes.astype(np.float32),
            latitudes.astype(np.float32),
            longitudes.astype(np.float32),
            wmo_ids,
            neighbour_methods=[method_name],
            grid_attributes=["x_index", "y_index", "vertical_displacement"],
        )

        # Add a hash attribute based on the model grid to ensure the neighbour
        # cube is only used with a compatible grid.
        grid_hash = create_coordinate_hash(orography)
        neighbour_cube.attributes["model_grid_hash"] = grid_hash

        return neighbour_cube
Esempio n. 10
0
    def _regrid_and_populate(
        self,
        temperature: Cube,
        humidity: Cube,
        pressure: Cube,
        uwind: Cube,
        vwind: Cube,
        topography: Cube,
    ) -> None:
        """
        Regrids input variables onto the high resolution orography field, then
        populates the class instance with regridded variables before converting
        to SI units.  Also calculates V.gradZ as a class member.

        Args:
            temperature:
                Temperature at top of boundary layer
            humidity:
                Relative humidity at top of boundary layer
            pressure:
                Pressure at top of boundary layer
            uwind:
                Positive eastward wind vector component at top of boundary
                layer
            vwind:
                Positive northward wind vector component at top of boundary
                layer
            topography:
                Height of topography above sea level on 1 km UKPP domain grid
        """
        # convert topography grid, datatype and units
        for axis in ["x", "y"]:
            topography = sort_coord_in_cube(topography,
                                            topography.coord(axis=axis))
        enforce_coordinate_ordering(
            topography,
            [
                topography.coord(axis="y").name(),
                topography.coord(axis="x").name()
            ],
        )
        self.topography = topography.copy(
            data=topography.data.astype(np.float32))
        self.topography.convert_units("m")

        # rotate winds
        try:
            uwind, vwind = rotate_winds(uwind, vwind,
                                        topography.coord_system())
        except ValueError as err:
            if "Duplicate coordinates are not permitted" in str(err):
                # ignore error raised if uwind and vwind do not need rotating
                pass
            else:
                raise ValueError(str(err))
        else:
            # remove auxiliary spatial coordinates from rotated winds
            for cube in [uwind, vwind]:
                for axis in ["x", "y"]:
                    cube.remove_coord(cube.coord(axis=axis, dim_coords=False))

        # regrid and convert input variables
        self.temperature = self._regrid_variable(temperature, "kelvin")
        self.humidity = self._regrid_variable(humidity, "1")
        self.pressure = self._regrid_variable(pressure, "Pa")
        self.uwind = self._regrid_variable(uwind, "m s-1")
        self.vwind = self._regrid_variable(vwind, "m s-1")

        # calculate orography gradients
        gradx, grady = self._orography_gradients()

        # calculate v.gradZ
        self.vgradz = np.multiply(gradx.data, self.uwind.data) + np.multiply(
            grady.data, self.vwind.data)