def longitude_slicer(lons, query): """ Returns a slice object that will slice out the smallest chunk of lons that covers the query domain defined by query['domain']['W'] and query['domain']['E']. The resulting slice will result in longitudes which increase from west to east, with a grid delta that is closest to the request grid delta, query['resolution']. """ domain = query['domain'] lons = np.asarray(lons, dtype=np.float32) if 'grid_delta' in query: warnings.warn('grid_delta in queries is obsolete, use resolution') lon_delta = query.get('resolution', None) slicer = angle_slicer(lons, domain['W'], domain['E'], lon_delta) # at least one point west of the eastern edge assert np.any(angles.angle_diff(lons[slicer], domain['E']) <= 0.) # east of the eastern edge assert np.any(angles.angle_diff(lons[slicer], domain['E']) >= 0.) # east of the western edge assert np.any(angles.angle_diff(lons[slicer], domain['W']) >= 0.) # west of the western edge assert np.any(angles.angle_diff(lons[slicer], domain['W']) <= 0.) return slicer
def bounding_box(fcst, pad=0.1, lon_pad=None, lat_pad=None): lons = np.unique(fcst['longitude'].values) lats = np.unique(fcst['latitude'].values) lon_diffs = angles.angle_diff(lons[:, None], lons) lat_diffs = angles.angle_diff(lats[:, None], lats) western_most_ind = np.nonzero(np.all(lon_diffs >= 0., axis=0))[0] western_most = np.unique(lons[western_most_ind]).item() eastern_most_ind = np.nonzero(np.all(lon_diffs <= 0., axis=0))[0] eastern_most = np.unique(lons[eastern_most_ind]).item() northern_most_ind = np.nonzero(np.all(lat_diffs <= 0., axis=0))[0] northern_most = np.unique(lats[northern_most_ind]).item() southern_most_ind = np.nonzero(np.all(lat_diffs >= 0., axis=0))[0] southern_most = np.unique(lats[southern_most_ind]).item() # count the number of lons greater than and less than each lon # and take the difference. The longitude (or pair of lons) that # minimize this help us determine the median. This allows different # definitions of longitude. lon_rel_loc = np.abs(np.sum(lon_diffs >= 0., axis=0) - np.sum(lon_diffs <= 0., axis=0)) central_lons = lons[lon_rel_loc == np.min(lon_rel_loc)] # make sure the central two aren't too far apart. assert np.max(central_lons) - np.min(central_lons) < 90 median_lon = np.median(central_lons) lat_rel_loc = np.abs(np.sum(lat_diffs >= 0., axis=0) - np.sum(lat_diffs <= 0., axis=0)) central_lats = lats[lat_rel_loc == np.min(lat_rel_loc)] median_lat = np.median(central_lats) width = angles.geographic_distance(western_most, median_lat, eastern_most, median_lat) height = angles.geographic_distance(median_lon, northern_most, median_lon, southern_most) if lon_pad is None: lon_pad = pad * np.abs(angles.angle_diff(eastern_most, western_most)) if lat_pad is None: lat_pad = pad * np.abs(angles.angle_diff(northern_most, southern_most)) return {'llcrnrlon': western_most - lon_pad, 'urcrnrlon': eastern_most + lon_pad, 'urcrnrlat': northern_most + lat_pad, 'llcrnrlat': southern_most - lat_pad, 'width': width * (1. + 2 * pad), 'height': height * (1. + 2 * pad), 'lon_0': median_lon, 'lat_0': median_lat}
def angle_slicer(x, low, high, delta=None, tolerance=4): """ A function which returns a slicer that will extract all the values in x that fall between low and high with a prescribed grid delta. """ assert x.ndim == 1 # determine the native grid delta. diffs = angles.angle_diff(x) diffs = np.unique(np.round(diffs, tolerance)) # assume angles are equally spaced. assert diffs.size == 1 native_delta = np.abs(diffs[0]) # round to the nearest native stride but make sure we dont hit zero delta = delta or native_delta stride = max(1, int(np.round(delta / native_delta))) def fully_contained(y): """Returns true if the range low/high is contained in y""" return (np.any(angles.angle_diff(low, y) >= 0) and np.any(angles.angle_diff(high, y) <= 0)) high_diff = angles.angle_diff(x, high) low_diff = angles.angle_diff(x, low) inside_domain = np.logical_and(high_diff <= 0, low_diff >= 0) # if the edges are in the domain, make sure everything is # otherwise we will have to use multiple slicers. if inside_domain[0] and inside_domain[-1]: assert np.all(inside_domain) # returns the index which minimizes x conditional on 'cond' being true def argmin_conditional(x, cond): x = x.copy() x[np.logical_not(cond)] = np.nan return np.nanargmin(x) # find the indices of the values that fall on or just outside the # lower and higher limits. low_ind = argmin_conditional(-low_diff, low_diff <= 0.) high_ind = argmin_conditional(high_diff, high_diff >= 0.) # if the angles in x are descending we flip the indices around if low_ind > high_ind: high_ind, low_ind = low_ind, high_ind possible_slicers = [slice(low_ind - i, high_ind - i + 1, stride) for i in range(stride)] # we strive to find a slicer that yield the fewest number of point # that fully contains the domain. possible_slicers = [s for s in possible_slicers if fully_contained(x[s])] if not len(possible_slicers): raise ValueError("Couldn't find a slice that would provide contain " "the desired domain") return possible_slicers[0]
def test_angle_diff(self): pairs = [((180, 180), 0.), ((181, 181), 0.), ((-1, -1), 0.), ((180, -180), 0.), # note, wrapped values get sent to -180 ((180, 0), -180.), ((0, 180), -180.), ((360, 180), -180.), ((361, 181), -180.), ((179, 0), 179.), ((0, 179), -179.), ((360, 181), 179.), ((1, 182), 179.), ((181, 179), 2.), ((179, 181), -2.), ((361, 359), 2.), ((-1, 1.), -2.), ((np.pi - 0.1, 0., False), np.pi - 0.1), ((np.pi - 0.1, -np.pi + 0.1, False), -0.2)] for args, expected in pairs: actual = angles.angle_diff(*args) self.assertAlmostEqual(expected, actual, places=6, msg=("actual: %f " "expected: %f " "args: %s" % (actual, expected, args)))
def test_maximum_error(self): from slocum.lib import angles ds = self.get_data() scheme = self.get_scheme() actual = roundtrip(scheme, ds) diff = angles.angle_diff(ds[scheme.variable_name].values, actual[scheme.variable_name].values) self.assertTrue(np.all(np.abs(diff) <= np.pi/16))
def __init__(self, fcst, velocity_variable, speed_units='knots', ax=None, fig=None, **kwdargs): self.variable = velocity_variable self.speed_units = speed_units # set the default colormap if needed kwdargs['cmap'] = kwdargs.get('cmap', velocity_cmap) # convert the bins to knots bins = xray.Variable('bins', velocity_variable.speed_bins.copy(), {'units': velocity_variable.units}) _, self.bins, _ = units.convert_units(bins, speed_units) default_norm = plt.cm.colors.BoundaryNorm(self.bins, self.bins.size) kwdargs['norm'] = kwdargs.get('norm', default_norm) self.variable = velocity_variable self.ax, self.fig = utils.axis_figure(ax, fig) # add the color bar self.cax = self.fig.add_axes([0.92, 0.05, 0.03, 0.9]) cbar = mpl.colorbar.ColorbarBase(self.cax, cmap=kwdargs['cmap'], norm=kwdargs['norm']) cbar.set_label("Knots") fcst = self.normalize(fcst) # use the longitude grid to define the radius of the circles sorted_lats = np.sort(fcst['latitude'].values) resol = np.median(angles.angle_diff(sorted_lats[1:], sorted_lats[:-1])) # create the map self.m = utils.get_basemap(fcst, ax=self.ax, lon_pad=0.75 * resol, lat_pad = 0.75 * resol) def create_circle(one_loc): # determine the circle center x, y = self.m(one_loc['longitude'].values, one_loc['latitude'].values) speeds = one_lonlat[self.variable.speed_name].values dirs = one_lonlat[self.variable.direction_name].values orientation = self.variable.direction_orientation return DirectionCircle(x, y, speeds=np.atleast_1d(speeds), directions=np.atleast_1d(dirs), orientation=orientation, radius=0.4 * resol, ax=self.ax, **kwdargs) self.circles = [[create_circle(one_lonlat) for lo, one_lonlat in one_lat.groupby('longitude')] for la, one_lat in fcst.groupby('latitude')] self.set_title(fcst)
def fully_contained(y): """Returns true if the range low/high is contained in y""" return (np.any(angles.angle_diff(low, y) >= 0) and np.any(angles.angle_diff(high, y) <= 0))