def test_geostationary_fixed_angle(): """Test handling geostationary information that gives fixed angle instead of sweep.""" attrs = {'grid_mapping_name': 'geostationary', 'fixed_angle_axis': 'y'} crs = CFProjection(attrs).to_cartopy() assert isinstance(crs, ccrs.Geostationary) assert crs.proj4_params['sweep'] == 'x'
def test_bad_projection_raises(): """Test behavior when given an unknown projection.""" attrs = {'grid_mapping_name': 'unknown'} with pytest.raises(ValueError) as exc: CFProjection(attrs).to_cartopy() assert 'Unhandled projection' in str(exc.value)
def generate_rectangular_grid(nx, ny, dx, dy, cf_attrs, x0=None, y0=None): """Generate an xarray Dataset representing a regular horizontal grid in projected space. Parameters ---------- nx : int Number of grid points in x direction ny : int Numver of grid points in y direction dx : float Grid spacing in projected space in x direction dy : float Grid spacing in projected space in y direction cf_attrs : dict Dictionary of attributes describing the projection following the CF Conventions x0 : float, optional x coordinate of lower-left corner of grid. If None, defaults to centering the grid on the projection origin. y0 : float, optional y coordinate of lower-left corner of grid. If None, defaults to centering the grid on the projection origin. Returns ------- xarray.Dataset Dataset describing grid, following CF Conventions """ crs = CFProjection(cf_attrs).to_cartopy() if x0 is None or y0 is None: x0 = -(nx - 1) / 2 * dx y0 = -(ny - 1) / 2 * dy # Generate grid x = np.arange(nx) * dx + x0 y = np.arange(ny) * dy + y0 xx, yy = np.meshgrid(x, y) lonlats = ccrs.PlateCarree().transform_points(crs, xx, yy) lon = lonlats[..., 0] lat = lonlats[..., 1] # Create Dataset ds = xr.Dataset(coords={ 'lon': (['y', 'x'], lon), 'lat': (['y', 'x'], lat), 'y': y, 'x': x }) ds['lon'].attrs = {'standard_name': 'longitude', 'units': 'degrees_east'} ds['lat'].attrs = {'standard_name': 'latitude', 'units': 'degrees_north'} ds['y'].attrs = { 'standard_name': 'projection_y_coordinate', 'units': 'meter' } ds['x'].attrs = { 'standard_name': 'projection_x_coordinate', 'units': 'meter' } ds['projection'] = xr.DataArray(np.array(0), attrs=cf_attrs) return ds
def test_coord_helper_da_latlon(): """Provide a DataArray with lat/lon coords for coord helpers.""" return xr.DataArray( np.arange(9).reshape((3, 3)), dims=('y', 'x'), coords={ 'latitude': xr.DataArray( np.array( [[34.99501239, 34.99875307, 35.], [35.44643155, 35.45019292, 35.45144675], [35.89782579, 35.90160784, 35.90286857]] ), dims=('y', 'x') ), 'longitude': xr.DataArray( np.array( [[-101.10219213, -100.55111288, -100.], [-101.10831414, -100.55417417, -100.], [-101.11450453, -100.55726965, -100.]] ), dims=('y', 'x') ), 'crs': CFProjection(sample_cf_attrs) } )
def test_coord_helper_da_yx(): """Provide a DataArray with y/x coords for coord helpers.""" return xr.DataArray(np.arange(9).reshape((3, 3)), dims=('y', 'x'), coords={'y': np.linspace(0, 1e5, 3), 'x': np.linspace(-1e5, 0, 3), 'crs': CFProjection(sample_cf_attrs)})
def test_cfprojection_api(): """Test the basic API of the projection interface.""" attrs = {'grid_mapping_name': 'lambert_conformal_conic', 'earth_radius': 6367000} proj = CFProjection(attrs) assert proj['earth_radius'] == 6367000 assert proj.to_dict() == attrs assert str(proj) == 'Projection: lambert_conformal_conic'
def test_aea_single_std_parallel(): """Test albers equal area with one standard parallel.""" attrs = { 'grid_mapping_name': 'albers_conical_equal_area', 'standard_parallel': 20 } crs = CFProjection(attrs).to_cartopy() assert isinstance(crs, ccrs.AlbersEqualArea) assert crs.proj4_params['lat_1'] == 20
def test_lcc_single_std_parallel(): """Test lambert conformal projection with one standard parallel.""" attrs = { 'grid_mapping_name': 'lambert_conformal_conic', 'standard_parallel': 25 } crs = CFProjection(attrs).to_cartopy() assert isinstance(crs, ccrs.LambertConformal) assert crs.proj4_params['lat_1'] == 25
def test_polar_stereographic_std_parallel(): """Test handling a polar stereographic projection that gives a standard parallel.""" attrs = {'grid_mapping_name': 'polar_stereographic', 'latitude_of_projection_origin': -90, 'standard_parallel': 60} crs = CFProjection(attrs).to_cartopy() assert isinstance(crs, ccrs.Stereographic) assert crs.proj4_params['lat_0'] == -90 assert crs.proj4_params['lat_ts'] == 60
def test_mercator(): """Test handling a mercator projection.""" attrs = {'grid_mapping_name': 'mercator', 'standard_parallel': 25, 'longitude_of_projection_origin': -100, 'false_easting': 0, 'false_westing': 0, 'central_latitude': 0} crs = CFProjection(attrs).to_cartopy() assert isinstance(crs, ccrs.Mercator) assert crs.proj4_params['lat_ts'] == 25 assert crs.proj4_params['lon_0'] == -100
def test_stereographic(): """Test handling a stereographic projection.""" attrs = {'grid_mapping_name': 'stereographic', 'scale_factor_at_projection_origin': 0.9, 'longitude_of_projection_origin': -100, 'latitude_of_projection_origin': 60} crs = CFProjection(attrs).to_cartopy() assert isinstance(crs, ccrs.Stereographic) assert crs.proj4_params['lon_0'] == -100 assert crs.proj4_params['lat_0'] == 60 assert crs.proj4_params['k_0'] == 0.9
def test_mercator_scale_factor(): """Test handling a mercator projection with a scale factor.""" attrs = { 'grid_mapping_name': 'mercator', 'scale_factor_at_projection_origin': 0.9 } crs = CFProjection(attrs).to_cartopy() assert isinstance(crs, ccrs.Mercator) assert crs.proj4_params['k_0'] == 0.9
def test_geostationary(): """Test handling a geostationary projection.""" attrs = {'grid_mapping_name': 'geostationary', 'perspective_point_height': 35000000, 'longitude_of_projection_origin': -100, 'sweep_angle_axis': 'x', 'latitude_of_projection_origin': 0} crs = CFProjection(attrs).to_cartopy() assert isinstance(crs, ccrs.Geostationary) assert crs.proj4_params['h'] == 35000000 assert crs.proj4_params['lon_0'] == -100 assert crs.proj4_params['sweep'] == 'x'
def test_lcc(): """Test handling lambert conformal conic projection.""" attrs = {'grid_mapping_name': 'lambert_conformal_conic', 'earth_radius': 6367000, 'standard_parallel': [25, 30]} proj = CFProjection(attrs) crs = proj.to_cartopy() assert isinstance(crs, ccrs.LambertConformal) assert crs.proj4_params['lat_1'] == 25 assert crs.proj4_params['lat_2'] == 30 assert crs.globe.to_proj4_params()['ellps'] == 'sphere'
def test_globe(): """Test handling building a cartopy globe.""" attrs = {'grid_mapping_name': 'lambert_conformal_conic', 'earth_radius': 6367000, 'standard_parallel': 25} proj = CFProjection(attrs) crs = proj.to_cartopy() globe_params = crs.globe.to_proj4_params() assert globe_params['ellps'] == 'sphere' assert globe_params['a'] == 6367000 assert globe_params['b'] == 6367000
def test_globe_spheroid(): """Test handling building a cartopy globe that is not spherical.""" attrs = {'grid_mapping_name': 'lambert_conformal_conic', 'semi_major_axis': 6367000, 'semi_minor_axis': 6360000} proj = CFProjection(attrs) crs = proj.to_cartopy() globe_params = crs.globe.to_proj4_params() assert 'ellps' not in globe_params assert globe_params['a'] == 6367000 assert globe_params['b'] == 6360000
def test_aea(): """Test handling albers equal area projection.""" attrs = { 'grid_mapping_name': 'albers_conical_equal_area', 'earth_radius': 6367000, 'standard_parallel': [20, 50] } proj = CFProjection(attrs) crs = proj.to_cartopy() assert isinstance(crs, ccrs.AlbersEqualArea) assert crs.proj4_params['lat_1'] == 20 assert crs.proj4_params['lat_2'] == 50 assert crs.globe.to_proj4_params()['ellps'] == 'sphere'
def test_inverse_flattening_0(): """Test new code for dealing the case where inverse_flattening = 0.""" attrs = { 'grid_mapping_name': 'lambert_conformal_conic', 'semi_major_axis': 6367000, 'semi_minor_axis': 6367000, 'inverse_flattening': 0 } proj = CFProjection(attrs) crs = proj.to_cartopy() globe_params = crs.globe.to_proj4_params() assert globe_params['ellps'] == 'sphere' assert globe_params['a'] == 6367000 assert globe_params['b'] == 6367000
def test_ne(): """Test that two CFProjection instances are not equal when attrs differs.""" cf_proj_1 = CFProjection({'grid_mapping_name': 'latitude_longitude'}) cf_proj_2 = CFProjection({'grid_mapping_name': 'lambert_conformal_conic'}) assert cf_proj_1 != cf_proj_2
def test_eq(): """Test that two CFProjection instances are equal given that they have the same attrs.""" attrs = {'grid_mapping_name': 'latitude_longitude'} cf_proj_1 = CFProjection(attrs) cf_proj_2 = CFProjection(attrs.copy()) assert cf_proj_1 == cf_proj_2
def test_lat_lon(): """Test handling basic lat/lon projection.""" attrs = {'grid_mapping_name': 'latitude_longitude'} crs = CFProjection(attrs).to_cartopy() assert isinstance(crs, ccrs.PlateCarree)
def test_assign_crs_dataarray_by_argument(test_ds_generic): """Test assigning CRS to DataArray by projection dict.""" da = test_ds_generic['test'] new_da = da.metpy.assign_crs(sample_cf_attrs) assert isinstance(new_da.metpy.cartopy_crs, ccrs.LambertConformal) assert new_da['crs'] == CFProjection(sample_cf_attrs)
def test_assign_crs_dataset_by_kwargs(test_ds_generic): """Test assigning CRS to Dataset by projection kwargs.""" new_ds = test_ds_generic.metpy.assign_crs(**sample_cf_attrs) assert isinstance(new_ds['test'].metpy.cartopy_crs, ccrs.LambertConformal) assert new_ds['crs'] == CFProjection(sample_cf_attrs)
def to_stations(da: Var, *, stations: pd.DataFrame, datastore: DataStore = None, chunks: dict = None, **kwargs) -> xr.DataArray: if isinstance(da, camps.Variable): da = da(datastore=datastore, chunks=chunks, **kwargs) elif isinstance(da, xr.DataArray): if chunks: da = da.camps.chunk(chunks) # Determine x and y # Use Projected crs as common crs for grid and station try: x = da.camps.projx.name y = da.camps.projy.name except KeyError: # projected coordinates do not exist if da.camps.grid_mapping: # try to make them from grid_mapping and lat/lons da = da.metpy.assign_crs(da.camps.grid_mapping) da = da.metpy.assign_y_x() da = da.drop('metpy_crs') x = da.camps.projx.name y = da.camps.projy.name else: # exception for mercator data without grid_mapping lat = da.camps.latitude if lat.ndim == 1: y = lat.name lon = da.camps.longitude if lon.ndim == 1: x = lon.name # ensure longitude expressed as degrees east from prime meridian with -179 and 180 bounds lon_attrs = lon.attrs da[lon.name] = xr.where(lon > 180, lon - 360, lon) da[lon.name].attrs = lon_attrs # Add the lat lon grid mapping since it didn't have one # Latitude and longitude on the WGS 1984 datum lat_lon_wgs84 = { 'grid_mapping_name': "latitude_longitude", 'longitude_of_prime_meridian': 0.0, 'semi_major_axis': 6378137.0, 'inverse_flattening': 298.257223563 } gm = xr.DataArray() gm.attrs.update(lat_lon_wgs84) da = da.assign_coords({'lat_lon_wgs84': gm}) da.attrs['grid_mapping'] = 'lat_lon_wgs84' pyproj_crs = CFProjection(da.camps.grid_mapping).to_pyproj() stations['x'], stations['y'] = Proj(pyproj_crs)(stations.lon.values, stations.lat.values) # rechunk so that multiple chunks don't span x and y dims if da.chunks is not None: da = da.chunk({x: -1, y: -1}) da = da.unify_chunks() # load x,y data da[x].load() da[y].load() # make horizonontal space 1-D by stacking x and y stacked = da.stack(xy=(x, y)) gridxy = np.column_stack((stacked[x].data, stacked[y].data)) stationxy = np.column_stack((stations.x, stations.y)) tree = cKDTree(gridxy) # fast nearest neighbor search algorith dist_ix = tree.query( stationxy) # find distance to nearest and index of nearest # make a grid polygon from shapely.geometry import Polygon, MultiPoint, Point unit_y = xr.DataArray(np.ones(da[y].shape), dims=da[y].dims) unit_x = xr.DataArray(np.ones(da[x].shape), dims=da[x].dims) edge_x = edge(da[x] * unit_y) # broadcast constant x along the y dim edge_y = edge(unit_x * da[y]) # broadcast constant y along the x dim xy = zip(edge_x, edge_y) grid_polygon = Polygon(xy) # make station points xy = zip(stations.x.values, stations.y.values) station_points = MultiPoint(list(xy)) # determine which stations lie outside grid domain (polygon) stations['point_str'] = pd.Series([str(p) for p in station_points]) stations['ix'] = stations.index points_outside_grid = station_points - station_points.intersection( grid_polygon) if not points_outside_grid.is_empty: if isinstance(points_outside_grid, Point): points_outside_grid = [points_outside_grid] ix_stations_outside_grid = stations.set_index('point_str').loc[[ str(p) for p in points_outside_grid ]].ix else: ix_stations_outside_grid = list() # let be empty list print(len(ix_stations_outside_grid)) def nearest_worker(da: xr.DataArray, *, x, y, ix, ix_nan) -> xr.DataArray: da = da.stack(station=(x, y)) # squash the horizontal space dims into one da = da.isel(station=ix) da = da.drop_vars('station') # remove station coord da.loc[{ 'station': ix_nan }] = np.nan # use integer index location to set stations outside grid to missing return da # make template for expressing the change in shape in map_blocks template = da.copy() # reshape action template = template.stack( station=(x, y)) # combine the lat/lon dims into one dim called station template = template.isel( station=[0] * len(stations)) # select only the first; this removes the dim station template = template.drop( 'station' ) # drop the multiindex lat/lon coord associated with 'station' from the 0th grid point mb_kwargs = dict(x=x, y=y, ix=dist_ix[1], ix_nan=ix_stations_outside_grid) da = xr.map_blocks(nearest_worker, da, kwargs=mb_kwargs, template=template) # remove any metadata that may be leftover from the grid da = da.drop_vars([x, y], errors='ignore') # configure station metadata # prep station coord with numeric index called 'station' stations = stations.reset_index() stations.index.set_names('station', inplace=True) # assign the new coords da = da.assign_coords({'platform_id': stations.call}) da.platform_id.attrs['standard_name'] = 'platform_id' # assign the new coords with numeric index da = da.assign_coords({'lat': stations.lat}) da.lat.attrs['standard_name'] = 'latitude' da.lat.attrs['units'] = 'degrees_north' da = da.assign_coords({'lon': stations.lon}) da.lon.attrs['standard_name'] = 'longitude' da.lon.attrs['units'] = 'degrees_east' # drop the numeric index; da = da.reset_index('station', drop=True) return da
def test_lcc_minimal(): """Test handling lambert conformal conic projection with minimal attributes.""" attrs = {'grid_mapping_name': 'lambert_conformal_conic'} crs = CFProjection(attrs).to_cartopy() assert isinstance(crs, ccrs.LambertConformal)
def test_aea_minimal(): """Test handling albers equal area projection with minimal attributes.""" attrs = {'grid_mapping_name': 'albers_conical_equal_area'} crs = CFProjection(attrs).to_cartopy() assert isinstance(crs, ccrs.AlbersEqualArea)