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
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
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
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)
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
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
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
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
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)