def maximum_intensity_projection(data, r=None, az=None, angle=None, elev=None, autoext=True): """Computes the maximum intensity projection along an arbitrary cut \ through the ppi from polar data. Parameters ---------- data : :class:`numpy:numpy.ndarray` Array containing polar data (azimuth, range) r : :class:`numpy:numpy.ndarray` Array containing range data az : array Array containing azimuth data angle : float angle of slice, Defaults to 0. Should be between 0 and 180. 0. means horizontal slice, 90. means vertical slice elev : float elevation angle of scan, Defaults to 0. autoext : True | False This routine uses numpy.digitize to bin the data. As this function needs bounds, we create one set of coordinates more than would usually be provided by `r` and `az`. Returns ------- xs : :class:`numpy:numpy.ndarray` meshgrid x array ys : :class:`numpy:numpy.ndarray` meshgrid y array mip : :class:`numpy:numpy.ndarray` Array containing the maximum intensity projection (range, range*2) """ # this may seem odd at first, but d1 and d2 are also used in several # plotting functions and thus it may be easier to compare the functions d1 = r d2 = az # providing 'reasonable defaults', based on the data's shape if d1 is None: d1 = np.arange(data.shape[1], dtype=np.float) if d2 is None: d2 = np.arange(data.shape[0], dtype=np.float) if angle is None: angle = 0.0 if elev is None: elev = 0.0 if autoext: # the ranges need to go 'one bin further', assuming some regularity # we extend by the distance between the preceding bins. x = np.append(d1, d1[-1] + (d1[-1] - d1[-2])) # the angular dimension is supposed to be cyclic, so we just add the # first element y = np.append(d2, d2[0]) else: # no autoext basically is only useful, if the user supplied the correct # dimensions himself. x = d1 y = d2 # roll data array to specified azimuth, assuming equidistant azimuth angles ind = (d2 >= angle).nonzero()[0][0] data = np.roll(data, ind, axis=0) # build cartesian range array, add delta to last element to compensate for # open bound (np.digitize) dc = np.linspace(-np.max(d1), np.max(d1) + 0.0001, num=d1.shape[0] * 2 + 1) # get height values from polar data and build cartesian height array # add delta to last element to compensate for open bound (np.digitize) hp = np.zeros((y.shape[0], x.shape[0])) hc = misc.bin_altitude(x, elev, 0, re=6370040.) hp[:] = hc hc[-1] += 0.0001 # create meshgrid for polar data xx, yy = np.meshgrid(x, y) # create meshgrid for cartesian slices xs, ys = np.meshgrid(dc, hc) # xs, ys = np.meshgrid(dc,x) # convert polar coordinates to cartesian xxx = xx * np.cos(np.radians(90. - yy)) # yyy = xx * np.sin(np.radians(90.-yy)) # digitize coordinates according to cartesian range array range_dig1 = np.digitize(xxx.ravel(), dc) range_dig1.shape = xxx.shape # digitize heights according polar height array height_dig1 = np.digitize(hp.ravel(), hc) # reshape accordingly height_dig1.shape = hp.shape # what am I doing here?! range_dig1 = range_dig1[0:-1, 0:-1] height_dig1 = height_dig1[0:-1, 0:-1] # create height and range masks height_mask = [(height_dig1 == i).ravel().nonzero()[0] for i in range(1, len(hc))] range_mask = [(range_dig1 == i).ravel().nonzero()[0] for i in range(1, len(dc))] # create mip output array, set outval to inf mip = np.zeros((d1.shape[0], 2 * d1.shape[0])) mip[:] = np.inf # fill mip array, # in some cases there are no values found in the specified range and height # then we fill in nans and interpolate afterwards for i in range(0, len(range_mask)): mask1 = range_mask[i] found = False for j in range(0, len(height_mask)): mask2 = np.intersect1d(mask1, height_mask[j]) # this is to catch the ValueError from the max() routine when # calculating on empty array try: mip[j, i] = data.ravel()[mask2].max() if not found: found = True except ValueError: if found: mip[j, i] = np.nan # interpolate nans inside image, do not touch outvals good = ~np.isnan(mip) xp = good.ravel().nonzero()[0] fp = mip[~np.isnan(mip)] x = np.isnan(mip).ravel().nonzero()[0] mip[np.isnan(mip)] = np.interp(x, xp, fp) # reset outval to nan mip[mip == np.inf] = np.nan return xs, ys, mip
def spherical_to_xyz(r, phi, theta, sitecoords, re=None, ke=4. / 3., squeeze=None, strict_dims=False): """Transforms spherical coordinates (r, phi, theta) to cartesian coordinates (x, y, z) centered at sitecoords (aeqd). It takes the shortening of the great circle distance with increasing elevation angle as well as the resulting increase in height into account. Parameters ---------- r : :class:`numpy:numpy.ndarray` Contains the radial distances in meters. phi : :class:`numpy:numpy.ndarray` Contains the azimuthal angles in degree. theta: :class:`numpy:numpy.ndarray` Contains the elevation angles in degree. sitecoords : a sequence of three floats the lon / lat coordinates of the radar location and its altitude a.m.s.l. (in meters) if sitecoords is of length two, altitude is assumed to be zero re : float earth's radius [m] ke : float adjustment factor to account for the refractivity gradient that affects radar beam propagation. In principle this is wavelength- dependent. The default of 4/3 is a good approximation for most weather radar wavelengths. squeeze : bool If True, returns squeezed array. strict_dims : bool If True, generates output of (theta, phi, r, 3) in any case. If False, dimensions with same length are "merged". Returns ------- xyz : :class:`numpy:numpy.ndarray` Array of shape (..., 3). Contains cartesian coordinates. rad : osr object Destination Spatial Reference System (Projection). Defaults to wgs84 (epsg 4326). """ # if site altitude is present, use it, else assume it to be zero try: centalt = sitecoords[2] except IndexError: centalt = 0. # if no radius is given, get the approximate radius of the WGS84 # ellipsoid for the site's latitude if re is None: re = projection.get_earth_radius(sitecoords[1]) # Set up aeqd-projection sitecoord-centered, wgs84 datum and ellipsoid # use world azimuthal equidistant projection projstr = ('+proj=aeqd +lon_0={lon:f} +x_0=0 +y_0=0 +lat_0={lat:f} ' + '+ellps=WGS84 +datum=WGS84 +units=m +no_defs' + '').format( lon=sitecoords[0], lat=sitecoords[1]) else: # Set up aeqd-projection sitecoord-centered, assuming spherical earth # use Sphere azimuthal equidistant projection projstr = ('+proj=aeqd +lon_0={lon:f} +lat_0={lat:f} +a={a:f} ' '+b={b:f} +units=m +no_defs').format(lon=sitecoords[0], lat=sitecoords[1], a=re, b=re) rad = projection.proj4_to_osr(projstr) r = np.asanyarray(r) theta = np.asanyarray(theta) phi = np.asanyarray(phi) if r.ndim: r = r.reshape((1, ) * (3 - r.ndim) + r.shape) if phi.ndim: phi = phi.reshape((1, ) + phi.shape + (1, ) * (2 - phi.ndim)) if not theta.ndim: theta = np.broadcast_to(theta, phi.shape) dims = 3 if not strict_dims: if phi.ndim and theta.ndim and (theta.shape[0] == phi.shape[1]): dims -= 1 if r.ndim and theta.ndim and (theta.shape[0] == r.shape[2]): dims -= 1 if theta.ndim and phi.ndim: theta = theta.reshape(theta.shape + (1, ) * (dims - theta.ndim)) z = misc.bin_altitude(r, theta, centalt, re, ke=ke) dist = misc.site_distance(r, theta, z, re, ke=ke) if ((not strict_dims) and phi.ndim and r.ndim and (r.shape[2] == phi.shape[1])): z = np.squeeze(z) dist = np.squeeze(dist) phi = np.squeeze(phi) x = dist * np.cos(np.radians(90 - phi)) y = dist * np.sin(np.radians(90 - phi)) if z.ndim: z = np.broadcast_to(z, x.shape) xyz = np.stack((x, y, z), axis=-1) if xyz.ndim == 1: xyz.shape = (1, ) * 3 + xyz.shape elif xyz.ndim == 2: xyz.shape = (xyz.shape[0], ) + (1, ) * 2 + (xyz.shape[1], ) if squeeze is None: warnings.warn( "Function `spherical_to_xyz` returns an array " "of shape (theta, phi, range, 3). Use `squeeze=True` " "to remove singleton dimensions." "", DeprecationWarning) if squeeze: xyz = np.squeeze(xyz) return xyz, rad