def as_cartopy_crs(self): """Return this projection as a Cartopy CRS instance.""" import iris.exceptions as iexcept import cartopy.crs as ccrs proj = ccrs.Geostationary(central_longitude=self.sub_lon, satellite_height=self.satellite_height, sweep_axis=self.sweep, globe=ccrs.Globe(semimajor_axis=self.req, semiminor_axis=self.rpol, ellipse=None)) # Calculate extent proj.extent = None # (x0, x1, y0, y1) try: proj.extent = ( self.iris_cube().coord('projection_x_coordinate').points[-1], self.iris_cube().coord('projection_x_coordinate').points[0], self.iris_cube().coord('projection_y_coordinate').points[0], self.iris_cube().coord('projection_y_coordinate').points[-1] ) except iexcept.CoordinateNotFoundError as err: log.warning(err) return proj
def plot(self, element, index=None, fix_unknown=False, show=False, valid_min=None, valid_max=None, **kw): """Plot requested data on a cylindrical map. Parameters ---------- element : str A string specifying the field to show on map. Usually one from the request elements. index : int index of second dimension when retrieving multi-dim array eg, if extracted field is (N, d), where N is number of records and d is number of wavelengths then index=1 will plot the field corresponding to the second wavelength, and so on... show : bool, optional True displays the plot (default is False which return a pyplot object) valid_min : real Valid minimum physical value in the array. Setting this will mask any values < valid_min valid_max : real Valid maximim physical value in the array. Setting this will mask any values > valid_max Keywords: See ypylib.XYZ.mapdata() keyword parameters for adjusting plot style. Returns ------- matplotlib.pyplot : object when ``show=False`` """ # extract data from metdb if self.mdb_extract is False and self.data is not None: data = self.data log.info('Skip MDB_EXTRACT.') else: data = self.extract(fix_unknown=fix_unknown) if data is None: return # show data on map host = ['[{}]'.format(self.hostname), ''][self.hostname is None] if isinstance(element, str) is False: log.warning('ELEMENT should be a string, but given\n%s', element) return if index is None or len(data[element].shape) == 1: plt_arr = data[element] element_str = element else: plt_arr = data[element][:, index] element_str = '{} [{}]'.format(element, str(index)) # vmin = kw.get('vmin', plt_arr.min()) # vmax = kw.get('vmax', plt_arr.max()) vmin = [valid_min, plt_arr.min()][valid_min is None] vmax = [valid_max, plt_arr.max()][valid_max is None] if np.ma.is_masked(vmin) and np.ma.is_masked(vmax): log.warning('No valid data points to plot.') return if len(plt_arr.shape) > 1: log.warning('Cant decide which array to plot,') log.info('Please give me an index value (e.g. index=1).') return w = (plt_arr >= vmin) & (plt_arr <= vmax) if sum(w) == 0: log.warning('Number of constrained data points = 0. Abort plot.') return xyz = XYZ(data['LNGD'][w], data['LTTD'][w], plt_arr[w]) # w = (data[element] >= vmin) & (data[element] <= vmax) # xyz = XYZ(data['LNGD'][w], data['LTTD'][w], data[element][w]) # update plot title title = kw.get( 'title', '{}:{} {}\n{}-{}'.format(self.subtype, element_str, host, self.start, self.stop)) kw.update({'title': title}) # print(self.extent) if self.area: kw.update({'limit': self.limit}) # warnings.simplefilter("ignore", UserWarning) res = xyz.mapdata(**kw) if show: res.show() else: return res
def extract(self, verbose=False, fix_unknown=False): """Extract obs from MetDB. Parameters ---------- verbose : bool increase verbosity fix_unknown : bool, optional Set data types for unknown elements e.g., merged back model fields to 32bit float. Returns ------- numpy ndarray matching request """ try: data = obs(Query.contact, self.subtype, self.keywords, self.elements, hostname=self.hostname) except ValueError as verr: if fix_unknown: elements = self.elements[len(Query.elements):] log.warning("Setting dtypes for %s as 'float'", elements) for e in elements: self.set_element_dtype(e, 'float') data = obs(Query.contact, self.subtype, self.keywords, self.elements, hostname=self.hostname) else: log.error( '%s\n** Hint for unknown elements: use ' 'extract(fix_unknow=True) OR call ' 'set_element_dtype(element, dtype) prior ' 'to extract() to bypass this error **', verr) raise (verr) except IOError as err: if verbose: log.info(' %s:%s\n %s - %s\n %s', self.hostname, self.ddict, self.start, self.stop, sorted(self.elements)) raise (err) if self.constrain: # single field constrain # key = list(self.constrain.keys())[0] # value = list(self.constrain.values())[0] # data = data[data[key] == value] # iterate through all constained fields for key, val in self.constrain.items(): data = data[data[key] == val] if len(data) == 0: log.error('Constrain results with no data.') print(self.constrain) return if self.keep: self.data = data return data
def plotx(filename, ax=None, cb_on=False, cb_bounds=None, cb_nticks=None, cb_unit='', cb_width=0.02, ccolor='w', coastline=True, cres='110m', dataset_path=None, draw_countries=False, dust_rgb=False, dust_quality=None, extent=None, figsize=(6, 6), gl_font=None, glw=0.5, list_all=False, MDI=None, projection=None, quick=False, satellite='MSG', save_fig=None, show_path=False, show_ticks=False, stride=(1, 1), tag_fig=None, tag_col='k', title=None, xglocs=None, yglocs=None, **kw): """Display 2D arrays from geostationary Slotstore file. Args: filename (str): Slotstore filename. An hdf5 file containing full-disc arrays of geostationary satellite or equivalent model data. ax (cartopy.mpl.geoaxes.GeoAxesSubplot, optional): Plot data on a specific geo axis. cb_on (bool, optional): Add colour bar to the plot. cb_bounds (array_like, optional): Specify discrete bounds for colorbar. cb_nticks (int, optional): Number of ticks to show in the colorbar. cb_unit (str, optional): Show data unit string on colorbar. cb_width (float, optional): Colour bar width. ccolor (str, optional): Coastline/boundary colour. Defaults to white. coastline (bool, optional): Show/hide coastlines. Defaults to True. Setting this to False will show the plot as a plain 2D array. cres (str, optional): Resolution of cartopy coastline/country boundary. Defaults to '110m'. Accepted values are '10m', '50m', '110m'. dataset_path (str, optional): Full path to 2D array data in hdf5. draw_countries (bool, optional): Show country boundaries. dust_rgb (bool, optional): Plot Dust RGB, require BT at 3 SEVIRI IR window channels in the file. Note: dataset_path and dust_rgb keywords are mutually exclusive. dust_quality (int, optional): Mask plot based on dust confidence flag (value range: 1-7). extent (sequence, optional): Longitude and Latitude bounds to plot the data (lon0, lon1, lat0, lat1) figsize (tuple, optional): Figure size. Default is (6, 6) gl_font (dict, optional): Font properties for grid labels default is {'family': 'monospace', 'size': 8, 'color': '#333333'} glw (float, optional): Width of grid lines. list_all (bool, optional): Show list of all available dataset in the file (no plot) and exit. MDI (number, optional): Missing Data Indicator. projection (cartopy crs, optional): Cartopy projection to use for mapping - one from the following Geostationary(), PlateCarree(), Mercator(). quick (bool, optional): Quick plot. Equivalent to stride=(10, 10). satellite (str, optional): Satellite ID. Defaults to 'MSG'. Accepted satellite IDs are: 'GOES16', 'GOES-E', 'GOES17', 'GOES-W', 'IODC'|'MSG_IODC', 'HIM8'. save_fig (str, optional): Save plot to a named file. show_path (bool, optional): Show filename and dataset path on plot. show_ticks (bool, optional): plot ticks and tick labels for non-mapped display. stride (tuple, optional): Skip pixels in x,y dimension, Default (1, 1) is to show at full resolution. tag_fig (str, optional): Add optional string to top left corner of figure. Useful for figures for articles. tag_col (str, optional): Colour of tag_fig text. title (str, optional): Figure title. Defaults to variable name from dataset_path (or 'DustRGB'). xglocs (array_like, optional): Locations of x grid-lines. yglocs (array_like, optional): Locations of y grid-lines. **kw: accepted `imshow` keywords (see imshow). Returns: matplotlib.pyplot or None: plot object or None if figure saved to disc. Raises: NotImplementedError: If Projection is not one of the following Geostationary, PlateCarree, Mercator ValueError: If dataset_path is missing or incorrect or ambiguous. """ import matplotlib.pyplot as plt import cartopy.feature as cfeature import cartopy.mpl.gridliner as cgrid from matplotlib import colors, ticker # defaults for MSG 0-deg service idx = 1 # index of datetime string in hdf filename c_lon = 0 # central geostationary longitude def_extent = (-80, 80, -80, 80) # default map extent sat = satellite.upper() if sat in ('IODC', 'MSG_IODC'): # update defaults for IODC service idx = 2 c_lon = 41.5 def_extent = (-40, 120, -80, 80) if sat == 'HIM8': # update defaults for Himawari service c_lon = 140.7 def_extent = (60, 220, -80, 80) if sat in ('GOES16', 'GOES-E'): c_lon = -75 def_extent = (-155, 5, -80, 80) if sat == 'GOES15': c_lon = -135 # def_extent = (-155, 5, -80, 80) if sat in ('GOES17', 'GOES-W'): c_lon = -137 # def_extent = (-155, 5, -80, 80) h5 = h5Parse(filename) if list_all: h5.ls() return if quick: stride = (10, 10) apply_dust_quality = False if dust_quality: dconf = '/Product/GM/DustAOD/DustConfidence' try: dset = h5.get_data(dconf) dust_qual = dset[dconf][::-1, ::-1][::stride[0], ::stride[1]] apply_dust_quality = True except KeyError: log.warn('Dust_quality not available - filter wont be applied') if gl_font is None: gl_font = dict(family='monospace', size=8, color='#333333') # Get dust RGB array if dust_rgb and dataset_path is None: dataset_path = 'DustRGB' cb_on = False plot_array = get_dust_rgb(h5, stride=stride, satellite=satellite) elif dataset_path and dust_rgb is False: dset = h5.get_data(dataset_path) if len(list(dset.keys())) == 0: return plot_array = dset[dataset_path][::-1, ::-1][::stride[0], ::stride[1]] # print plot_array.dtype if plot_array.dtype in ( np.int8, np.int16, np.intc, np.uint8, np.uint16, np.intp): MDI = [MDI, IMDI][MDI is None] plot_array = np.ma.masked_equal(plot_array, MDI) if apply_dust_quality: plot_array = np.ma.masked_less(dust_qual, dust_quality) else: MDI = [MDI, RMDI][MDI is None] plot_array[plot_array <= (MDI + RMDItol)] = np.nan if apply_dust_quality: plot_array = np.ma.where( dust_qual >= dust_quality, plot_array, np.nan) elif dataset_path and dust_rgb: raise ValueError('dust_rgb must be False if dataset_path is given') else: raise ValueError( 'Either dust_rgb or dataset_path must be specified.\n\n' ' msgview(filename, [dataset_path|dust_rgb=True], ..) OR\n' ' msgview(filename, list_all=True) to see all datasets.\n') # Get MSG datetime string from hdf5 filename msg_datetime = os.path.basename(filename).split('_')[idx] title_str = [msg_datetime, os.path.basename(dataset_path)] # Reassign 3-channel DustMask values for readable legend/title if title_str[1] in 'Dust': title_str[1] = 'DustMask' plot_array[plot_array < -0.5] = 0 plot_array[plot_array == 300] = 1 plot_array[plot_array == 400] = 2 plot_array[plot_array == 500] = 3 # Rename DustHeightError to DustPressureError if title_str[1] in 'DustHeightError': title_str[1] = 'DustPressureError' if title is None: title = '{} {}'.format(title_str[0], title_str[1]) # Start plot if ax: projection = ax.projection else: plt.figure(figsize=figsize) projection = [projection, ccrs.Geostationary()][projection is None] # Normalise colour based on discrete colour bounds (cb_bounds)? if cb_bounds: bounds = np.array(cb_bounds) norm = colors.BoundaryNorm(boundaries=bounds, ncolors=len(bounds)) else: norm = None # combine plot_array and imshow keywords plot_array_kw = { 'extent': geo_extent(satellite=satellite), 'origin': 'upper', 'norm': None, 'interpolation': None} plot_array_kw.update(**kw) gridline_kw = dict(alpha=0.5, xlocs=xglocs, ylocs=yglocs, linestyle='-', linewidth=glw) if coastline: if projection in (ccrs.PlateCarree(), ccrs.PlateCarree(central_longitude=0), ccrs.PlateCarree(central_longitude=180), ccrs.Mercator(), ccrs.Mercator(central_longitude=0), ccrs.Mercator(central_longitude=180)): # Plot using rectangular projection if ax is None: ax = plt.axes(projection=projection) extent = [extent, def_extent][extent is None] ax.set_extent(extent, ccrs.PlateCarree()) gl = ax.gridlines(draw_labels=True, **gridline_kw) gl.xlabels_top = False gl.ylabels_right = False gl.xformatter = cgrid.LONGITUDE_FORMATTER gl.yformatter = cgrid.LATITUDE_FORMATTER gl.xlabel_style, gl.ylabel_style = gl_font, gl_font im = ax.imshow(plot_array, transform=ccrs.Geostationary(c_lon), **plot_array_kw) elif projection == ccrs.Geostationary(): # Plot in Geostationary projection if ax is None: ax = plt.axes(projection=ccrs.Geostationary(c_lon)) ax.set_global() gl = ax.gridlines(**gridline_kw) im = ax.imshow(plot_array, **plot_array_kw) if extent: ax.set_extent(extent, ccrs.PlateCarree()) else: raise NotImplementedError( '{} projection not implemented.\n' 'Please use one from the following:\n' '\tGeostationary\n\tPlateCarree or\n\tMercator\n'.format( projection.__class__)) ax.coastlines(resolution=cres, lw=0.5, color=ccolor) else: if extent: # TODO: implement geo-to-pixel for cut outs without map log.warning('Ignored extent. For geographic subsets please set ' 'coastline=True.') ax_pos = [0.05, 0.1, 0.8, 0.8] if show_ticks: ax_pos = [0.1, 0.1, 0.8, 0.8] if ax is None: ax = plt.axes(ax_pos) # Add country borders if draw_countries: ax.add_feature(cfeature.BORDERS, lw=0.4, edgecolor=ccolor) pos = ax.get_position() # Tag a figure with additional string at top-left (useful for figure # panel numbering) if tag_fig: # title = '{} {}'.format(tag_fig, title) ax.text(0.01, 0.95, tag_fig, color=tag_col, fontsize=14, transform=ax.transAxes) # Draw title text ax.set_title(title, fontsize="13", loc='left') # Hide plot ticks and tick labels? if show_ticks is False: for ticx, ticy in list( zip(ax.xaxis.get_major_ticks(), ax.yaxis.get_major_ticks())): ticx.tick1On = ticx.tick2On = False ticx.label1On = ticx.label2On = False ticy.tick1On = ticy.tick2On = False ticy.label1On = ticy.label2On = False # Show image without coastline - simple 2D array imshow() if coastline is False: im = plt.imshow(plot_array, norm=norm, interpolation=None, **kw) ax.grid(lw=0.5, alpha=0.5) # Attach vertical colour bar if cb_on: cax = plt.axes([pos.x1, pos.y0, cb_width, pos.height]) cb = plt.colorbar(mappable=im, cax=cax, format='%g') cb.ax.tick_params(direction='in') cb.ax.set_title('{}'.format(cb_unit)) if cb_nticks: tick_locator = ticker.MaxNLocator(nbins=cb_nticks) cb.locator = tick_locator cb.update_ticks() if title_str[1] in 'DustMask': cb.ax.set_yticklabels( ['Dust', 'Ice cloud', 'Low cloud/\nSurface', 'Uncertain'], rotation='90', va='bottom') if show_path: # Show filename and HDF5 dataset path on plot textstr = '{}\n{}'.format(dataset_path, filename) plt.gcf().text(0.99, 0.01, textstr, fontsize=8, ha='right') if save_fig: # save figure as file to disk plt.savefig(save_fig) plt.close() log.info('%s saved.', save_fig) else: # return pyplot object return plt
def inverse_projection(self, cols_and_lines=None, maxlon=None): """Generate lat, lon coords (4.4.3.2 of [1]). Method copied from Fortran: SpsMod_Coordinates/Sps_GeostationaryProjection.inc Args: cols_and_lines (None, optional): Description maxlon (None, optional): Description Returns: tuple: a two-element tuple containing same-shape 1D arrays for lons and lats (in degrees). """ import warnings warnings.simplefilter('ignore', RuntimeWarning) if cols_and_lines is None: # Do full disk col_start = -self.coff + 1 col_stop = self.nc + col_start cols = np.arange(col_start, col_stop) line_start = -self.loff + 1 line_stop = self.nl + line_start lines = np.arange(line_start, line_stop) colsm, linesm = np.meshgrid(cols, lines) cols = colsm.flatten() lines = linesm.flatten() else: errmsg = ("cols_and_lines should be numpy array, 2 dims, second" " of which should be of length 2") if (type(cols_and_lines) is not np.ndarray or len(cols_and_lines.shape) != 2 or cols_and_lines.shape[1] != 2): log.warning(errmsg) return cols = cols_and_lines[:, 0] - self.coff + 1 lines = cols_and_lines[:, 1] - self.loff + 1 # Generate intermediate coords x = np.deg2rad(cols * self.dx) y = np.deg2rad(lines * self.dy) sin_y = np.sin(y) sin2_y = np.sin(y) * np.sin(y) cos_y = np.cos(y) cos2_y = np.cos(y) * np.cos(y) sin_x = np.sin(x) cos_x = np.cos(x) # Solve quadratic to get the vector length, but check that the # discriminant is positive, else the point is off the earth disk. req = 1e-3 * self.req rpol = 1e-3 * self.rpol latf = req * req / (rpol * rpol) h = 1e-3 * self.satellite_height + req a = cos2_y + latf * sin2_y b = h * cos_x * cos_y sd = b * b - a * (h * h - req * req) space_mask = sd < 0 sn = (b - np.sqrt(sd)) / a # Calculate components of position vector s1 = h - sn * cos_x * cos_y if self.sweep == 'x': s2 = sn * sin_x s3 = sn * sin_y * cos_x else: s2 = sn * sin_x * cos_y s3 = sn * sin_y # Convert to latitude and longitude sxy = np.sqrt(s1 * s1 + s2 * s2) lons = np.rad2deg(np.arctan(s2 / s1) + np.deg2rad(self.sub_lon)) lats = np.rad2deg(np.arctan(latf * s3 / sxy)) # Adjust longitudes if -180 to 180 range needed if maxlon is not None: lons[lons >= maxlon] = (lons[lons >= maxlon] - 360.0) lons[space_mask] = RMDI lats[space_mask] = RMDI lons = np.ma.array(lons, mask=space_mask) lats = np.ma.array(lats, mask=space_mask) return (lons, lats)
def forward_projection(self, lons_and_lats): """Convert lons/lats to intermediate coords and col/line vals (4.4.4 of [1]). Assumes lons/lats are in degrees, and lons in range -180 to 180. Method copied from Fortran: SpsMod_Coordinates/Sps_GeostationaryProjection.inc Columns/lines are 1-based (not 0-based as in Python) Args: lons_and_lats (ndarray): 2D numpy array containing lons and lats in degrees, stacked 1D arrays so size of 2nd array is 2, with lons = lons_and_lats[:, 0], lats = lons_and_lats[:, 1]. Returns: tuple: 4-element tuple of 1D arrays: x, y, columns, lines. """ errmsg = ("lons_and_lats should be numpy array, 2 dims, second" " of which should be of length 2") if (type(lons_and_lats) is not np.ndarray or len(lons_and_lats.shape) != 2 or lons_and_lats.shape[1] != 2): log.warning(errmsg) return lons_r = np.deg2rad(lons_and_lats[:, 0]) lats_r = np.deg2rad(lons_and_lats[:, 1]) # Geocentric latitude clat req = 1.0e-3 * self.req rpol = 1.0e-3 * self.rpol clatf = rpol * rpol / (req * req) h = 1.0e-3 * self.satellite_height + req rf = (req * req - rpol * rpol) / (req * req) clat = np.arctan(clatf * np.tan(lats_r)) cos_clat = np.cos(clat) cos2_clat = cos_clat * cos_clat # Distance rl of point P from the centre of the earth rl = rpol / np.sqrt(1.0 - rf * cos2_clat) # Cartesian components of the vector rs (r1,r2,r3) # which points from the satellite to P. r1 = h - rl * cos_clat * np.cos(lons_r - np.deg2rad(self.sub_lon)) r2 = -rl * cos_clat * np.sin(lons_r - np.deg2rad(self.sub_lon)) r3 = rl * np.sin(clat) # Check to see if P is visible (the maximum viewing extent # is where a line from the satellite is tangent to the earth). rs = r1 * r1 + r2 * r2 + r3 * r3 mask = np.logical_or.reduce((np.abs(lons_r) > np.pi, np.abs(lats_r) > np.pi / 2.0, (rs + rl * rl) > (h * h))) # Convert to planar coordinates if self.sweep == 'x': rn = np.sqrt(r1 * r1 + r3 * r3) x = np.where(mask, RMDI, np.arctan(-r2 / rn)) y = np.where(mask, RMDI, np.arctan(r3 / r1)) else: rn = np.sqrt(rs) x = np.where(mask, RMDI, np.arctan(-r2 / r1)) y = np.where(mask, RMDI, np.arcsin(r3 / rn)) # Deduce column / lines cols = np.where(mask, IMDI, np.round(self.coff + np.rad2deg(x / self.dx))).astype('int64') lines = np.where(mask, IMDI, np.round(self.loff + np.rad2deg(y / self.dy))).astype('int64') return (x, y, cols, lines)