def get_range(standard_name, level="total", typ=None): """ Returns valid range of values for target at given level Args: standard_name: string of CF standard_name level (optional): horizontal level of data type (optional): type of data (pl, ml, tl, ...) Returns: Tuple of lowest and highest valid value """ if standard_name in Targets.RANGES: if level == "total" and "total" in Targets.RANGES[standard_name]: unit, values = Targets.RANGES[standard_name]["total"] return convert_to(values, unit, Targets.get_unit(standard_name)) if typ in Targets.RANGES[standard_name]: if level in Targets.RANGES[standard_name][typ]: unit, values = Targets.RANGES[standard_name][typ][level] return convert_to(values, unit, Targets.get_unit(standard_name)) elif level is None: return 0, 0 if standard_name.startswith("surface_origin_tracer_from_"): return 0, 100 return None, None
def _prepare_datafields(self): """ Computes vertical velocity in cm/s. """ self.data["upward_wind"] = convert_to( thermolib.omega_to_w(self.data["lagrangian_tendency_of_air_pressure"], self.data['air_pressure'], self.data["air_temperature"]), "m/s", "cm/s") self.variable = "upward_wind" self.y_values = self.data[self.variable] self.unit = "cm/s"
def verticalunitsclicked(self, index): new_unit = self._suffixes[index] old_unit = self.sbPbot.suffix().strip() if new_unit == old_unit: return self.setBotTopLimits("maximum") for sb in (self.sbPbot, self.sbPtop): sb.setSuffix(" " + new_unit) if new_unit == "hPa": sb.setValue( thermolib.flightlevel2pressure( convert_to(sb.value(), old_unit, "hft", 1) * units.hft).to(units.hPa).magnitude) elif old_unit == "hPa": sb.setValue( convert_to( thermolib.pressure2flightlevel( sb.value() * units.hPa).magnitude, "hft", new_unit)) else: sb.setValue(convert_to(sb.value(), old_unit, new_unit, 1)) self.setBotTopLimits(self.cbVerticalAxis.currentText())
def get_thresholds(standard_name): """ Returns a list of meaningful values for a BoundaryNorm for plotting. Args: standard_name: string of CF standard_name level (optional): horizontal level of data type (optional): type of data (pl, ml, tl, ...) Returns: Tuple of threshold values to be supplied to a BoundaryNorm. """ try: threshold_unit, thresholds = Targets.THRESHOLDS[standard_name] return convert_to(thresholds, threshold_unit, Targets.get_unit(standard_name)) except KeyError: return None
def test_convert_to(): assert convert_to(10, "km", "m", None) == 10000 assert convert_to(1000, "m", "km", None) == 1 assert convert_to(1000, "Pa", "hPa", None) == 10 assert convert_to(1000, "hPa", "Pa", None) == 100000 assert convert_to(10, "degC", "K", None) == 283.15 assert convert_to(10 * 9.81, "m^2s^-2", "m", None) == pytest.approx(10) assert convert_to(10 * 9.81, "m**2s**-2", "m", None) == pytest.approx(10) with pytest.raises(TypeError): assert convert_to(10, "m", "m**2s**-2", None) == 10 assert convert_to(10, "m", "m**2s**-2", 1) == 10 assert convert_to(10, "m", "m**2s**-2", 9.81) == pytest.approx(98.1) assert convert_to(1000, "", "Pa", 999) == 999000 assert convert_to(1000, "Pa", "", 999) == 999000 assert convert_to(1000, "whattheheck", "Pa", 999) == 999000 assert convert_to(1000, "hPa", "whattheheck", 999) == 999000 assert convert_to(1000, "whattheheck", "whatthehock", 999) == 999000 assert convert_to(10, "percent", "dimensionless", None) == 0.1 assert convert_to(10, "permille", "dimensionless", None) == 0.01 assert convert_to(10, "ppm", "dimensionless", None) == pytest.approx(10e-6) assert convert_to(10, "ppb", "dimensionless", None) == pytest.approx(10e-9) assert convert_to(10, "ppt", "dimensionless", None) == pytest.approx(10e-12) assert convert_to(10, "ppm", "ppt", None) == pytest.approx(10e6) assert convert_to(10, "ppb", "ppm", None) == pytest.approx(10e-3)
def plot_hsection(self, data, lats, lons, bbox=(-180, -90, 180, 90), level=None, figsize=(960, 640), crs=None, proj_params=None, valid_time=None, init_time=None, style=None, resolution=-1, noframe=False, show=False, transparent=False): """ EPSG overrides proj_params! """ if proj_params is None: proj_params = {"projection": "cyl"} bbox_units = "latlon" # Projection parameters from EPSG code. if crs is not None: proj_params, bbox_units = [ get_projection_params(crs)[_x] for _x in ("basemap", "bbox") ] logging.debug("plotting data..") # Check if required data is available. self.data_units = self.driver.data_units.copy() for datatype, dataitem, dataunit in self.required_datafields: if dataitem not in data: raise KeyError(f"required data field '{dataitem}' not found") origunit = self.driver.data_units[dataitem] if dataunit is not None: data[dataitem] = convert_to(data[dataitem], origunit, dataunit) self.data_units[dataitem] = dataunit else: logging.debug("Please add units to plot variables") # Copy parameters to properties. self.data = data self.lats = lats self.lons = lons self.level = level self.valid_time = valid_time self.init_time = init_time self.style = style self.resolution = resolution self.noframe = noframe self.crs = crs # Derive additional data fields and make the plot. logging.debug("preparing additional data fields..") self._prepare_datafields() logging.debug("creating figure..") dpi = 80 figsize = (figsize[0] / dpi), (figsize[1] / dpi) facecolor = "white" fig = mpl.figure.Figure(figsize=figsize, dpi=dpi, facecolor=facecolor) logging.debug( "\twith frame and legends" if not noframe else "\twithout frame") if noframe: ax = fig.add_axes([0.0, 0.0, 1.0, 1.0]) else: ax = fig.add_axes([0.05, 0.05, 0.9, 0.88]) # The basemap instance is created with a fixed aspect ratio for framed # plots; the aspect ratio is not fixed for frameless plots (standard # WMS). This means that for WMS plots, the map will always fill the # entire image area, no matter of whether this stretches or shears the # map. This is the behaviour specified by the WMS standard (WMS Spec # 1.1.1, Section 7.2.3.8): # "The returned picture, regardless of its return format, shall have # exactly the specified width and height in pixels. In the case where # the aspect ratio of the BBOX and the ratio width/height are different, # the WMS shall stretch the returned map so that the resulting pixels # could themselves be rendered in the aspect ratio of the BBOX. In # other words, it should be possible using this definition to request a # map for a device whose output pixels are themselves non-square, or to # stretch a map into an image area of a different aspect ratio." # NOTE: While the MSUI always requests image sizes that match the aspect # ratio, for instance the Metview 4 client does not (mr, 2011Dec16). # Some additional code to store the last 20 coastlines in memory for quicker # access. key = repr((proj_params, bbox, bbox_units)) basemap_use_cache = getattr(mswms_settings, "basemap_use_cache", False) basemap_request_size = getattr(mswms_settings, "basemap_request_size ", 200) basemap_cache_size = getattr(mswms_settings, "basemap_cache_size", 20) bm_params = { "area_thresh": 1000., "ax": ax, "fix_aspect": (not noframe) } bm_params.update(proj_params) if bbox_units == "degree": bm_params.update({ "llcrnrlon": bbox[0], "llcrnrlat": bbox[1], "urcrnrlon": bbox[2], "urcrnrlat": bbox[3] }) elif bbox_units.startswith("meter"): # convert meters to degrees try: bm_p = basemap.Basemap(resolution=None, **bm_params) except ValueError: # projection requires some extent bm_p = basemap.Basemap(resolution=None, width=1e7, height=1e7, **bm_params) bm_center = [float(_x) for _x in bbox_units[6:-1].split(",")] center_x, center_y = bm_p(*bm_center) bbox_0, bbox_1 = bm_p(bbox[0] + center_x, bbox[1] + center_y, inverse=True) bbox_2, bbox_3 = bm_p(bbox[2] + center_x, bbox[3] + center_y, inverse=True) bm_params.update({ "llcrnrlon": bbox_0, "llcrnrlat": bbox_1, "urcrnrlon": bbox_2, "urcrnrlat": bbox_3 }) elif bbox_units == "no": pass else: raise ValueError(f"bbox_units '{bbox_units}' not known.") if basemap_use_cache and key in BASEMAP_CACHE: bm = basemap.Basemap(resolution=None, **bm_params) (bm.resolution, bm.coastsegs, bm.coastpolygontypes, bm.coastpolygons, bm.coastsegs, bm.landpolygons, bm.lakepolygons, bm.cntrysegs) = BASEMAP_CACHE[key] logging.debug("Loaded '%s' from basemap cache", key) else: bm = basemap.Basemap(resolution='l', **bm_params) # read in countries manually, as those are laoded only on demand bm.cntrysegs, _ = bm._readboundarydata("countries") if basemap_use_cache: BASEMAP_CACHE[key] = (bm.resolution, bm.coastsegs, bm.coastpolygontypes, bm.coastpolygons, bm.coastsegs, bm.landpolygons, bm.lakepolygons, bm.cntrysegs) if basemap_use_cache: BASEMAP_REQUESTS.append(key) BASEMAP_REQUESTS[:] = BASEMAP_REQUESTS[-basemap_request_size:] if len(BASEMAP_CACHE) > basemap_cache_size: useful = {} for idx, key in enumerate(BASEMAP_REQUESTS): useful[key] = useful.get(key, 0) + idx least_useful = sorted([ (value, key) for key, value in useful.items() ])[:-basemap_cache_size] for _, key in least_useful: del BASEMAP_CACHE[key] BASEMAP_REQUESTS[:] = [ _x for _x in BASEMAP_REQUESTS if key != _x ] if self._plot_countries: # Set up the map appearance. try: bm.drawcoastlines(color='0.25') except ValueError as ex: logging.error( "Error in basemap/matplotlib call of drawcoastlines: %s", ex) bm.drawcountries(color='0.5') bm.drawmapboundary(fill_color='white') # zorder = 0 is necessary to paint over the filled continents with # scatter() for drawing the flight tracks and trajectories. # Curiously, plot() works fine without this setting, but scatter() # doesn't. bm.fillcontinents(color='0.98', lake_color='white', zorder=0) self._draw_auto_graticule(bm) if noframe: ax.axis('off') self.bm = bm # !! BETTER PASS EVERYTHING AS PARAMETERS? self.fig = fig self.shift_data() self.mask_data() self.lonmesh, self.latmesh = bm(*np.meshgrid(self.lons, self.lats)) self._plot_style() # Set transparency for the output image. if transparent: fig.patch.set_alpha(0.) # Return the image as png embedded in a StringIO stream. canvas = FigureCanvas(fig) output = io.BytesIO() canvas.print_png(output) if show: logging.debug("saving figure to mpl_hsec.png ..") canvas.print_png("mpl_hsec.png") # Convert the image to an 8bit palette image with a significantly # smaller file size (~factor 4, from RGBA to one 8bit value, plus the # space to store the palette colours). # NOTE: PIL at the current time can only create an adaptive palette for # RGB images, hence alpha values are lost here. If transparency is # requested, the figure face colour is stored as the "transparent" # colour in the image. This works in most cases, but might lead to # visible artefacts in some cases. logging.debug("converting image to indexed palette.") # Read the above stored png into a PIL image and create an adaptive # colour palette. output.seek(0) # necessary for PIL.Image.open() palette_img = PIL.Image.open(output).convert(mode="RGB").convert( "P", palette=PIL.Image.ADAPTIVE) output = io.BytesIO() if not transparent: logging.debug("saving figure as non-transparent PNG.") palette_img.save( output, format="PNG") # using optimize=True doesn't change much else: # If the image has a transparent background, we need to find the # index of the background colour in the palette. See the # documentation for PIL's ImagePalette module # (http://www.pythonware.com/library/pil/handbook/imagepalette.htm). The # idea is to create a 256 pixel image with the same colour palette # as the original image and use it as a lookup-table. Converting the # lut image back to RGB gives us a list of all colours in the # palette. (Why doesn't PIL provide a method to directly access the # colours in a palette??) lut = palette_img.resize((256, 1)) lut.putdata(list(range(256))) lut = [c[1] for c in lut.convert("RGB").getcolors()] facecolor_rgb = list( mpl.colors.hex2color(mpl.colors.cnames[facecolor])) for i in [0, 1, 2]: facecolor_rgb[i] = int(facecolor_rgb[i] * 255) try: facecolor_index = lut.index(tuple(facecolor_rgb)) logging.debug( "saving figure as transparent PNG with transparency index %s.", facecolor_index) palette_img.save(output, format="PNG", transparency=facecolor_index) except ValueError: logging.debug( "transparency requested but not possible, saving non-transparent instead" ) palette_img.save(output, format="PNG") logging.debug("returning figure..") return output.getvalue()
def plot_lsection(self, data, lats, lons, valid_time, init_time): """ """ # Check if required data is available. self.data_units = self.driver.data_units.copy() for datatype, dataitem, dataunit in self.required_datafields: if dataitem not in data: raise KeyError(f"required data field '{dataitem}' not found") origunit = self.driver.data_units[dataitem] if dataunit is not None: data[dataitem] = convert_to(data[dataitem], origunit, dataunit) self.data_units[dataitem] = dataunit else: logging.debug("Please add units to plot variables") # Copy parameters to properties. self.data = data self.lats = lats self.lons = lons self.valid_time = valid_time self.init_time = init_time # Derive additional data fields and make the plot. self._prepare_datafields() impl = getDOMImplementation() xmldoc = impl.createDocument(None, "MSS_LinearSection_Data", None) # Title of this section. node = xmldoc.createElement("Title") node.appendChild(xmldoc.createTextNode(self.title)) xmldoc.documentElement.appendChild(node) # Time information of this section. node = xmldoc.createElement("ValidTime") node.appendChild( xmldoc.createTextNode( self.valid_time.strftime("%Y-%m-%dT%H:%M:%SZ"))) xmldoc.documentElement.appendChild(node) node = xmldoc.createElement("InitTime") node.appendChild( xmldoc.createTextNode( self.init_time.strftime("%Y-%m-%dT%H:%M:%SZ"))) xmldoc.documentElement.appendChild(node) # Longitude data. node = xmldoc.createElement("Longitude") node.setAttribute("num_waypoints", f"{len(self.lons)}") data_str = ",".join([str(lon) for lon in self.lons]) node.appendChild(xmldoc.createTextNode(data_str)) xmldoc.documentElement.appendChild(node) # Latitude data. node = xmldoc.createElement("Latitude") node.setAttribute("num_waypoints", f"{len(self.lats)}") data_str = ",".join([str(lat) for lat in self.lats]) node.appendChild(xmldoc.createTextNode(data_str)) xmldoc.documentElement.appendChild(node) # Variable data. node = xmldoc.createElement("Data") node.setAttribute("num_waypoints", f"{len(self.y_values)}") node.setAttribute("unit", self.unit) if isinstance(self.y_values[0], Quantity): data_str = ",".join([str(val.magnitude) for val in self.y_values]) else: data_str = ",".join([str(val) for val in self.y_values]) node.appendChild(xmldoc.createTextNode(data_str)) xmldoc.documentElement.appendChild(node) # Return the XML document as formatted string. return xmldoc.toprettyxml(indent=" ")
def plot_vsection(self, data, lats, lons, valid_time, init_time, resolution=(-1, -1), bbox=(-1, 1050, -1, 200), style=None, show=False, highlight=None, noframe=False, figsize=(960, 480), draw_verticals=False, numlabels=10, orography_color='k', transparent=False, return_format="image/png"): """ """ # Check if required data is available. self.data_units = self.driver.data_units.copy() for datatype, dataitem, dataunit in self.required_datafields: if dataitem not in data: raise KeyError(f"required data field '{dataitem}' not found") origunit = self.driver.data_units[dataitem] if dataunit is not None: data[dataitem] = convert_to(data[dataitem], origunit, dataunit) self.data_units[dataitem] = dataunit else: logging.debug("Please add units to plot variables") # Copy parameters to properties. self.data = data self.lats = lats self.lat_inds = np.arange(len(lats)) self.lons = lons self.valid_time = valid_time self.init_time = init_time self.resolution = resolution self.style = style self.highlight = highlight self.noframe = noframe self.draw_verticals = draw_verticals self.p_bot = bbox[1] * 100 self.p_top = bbox[3] * 100 self.numlabels = numlabels self.orography_color = orography_color # Provide an air_pressured 2-D field in 'Pa' from vertical axis if (("air_pressure" not in self.data) and units(self.driver.vert_units).check("[pressure]")): self.data_units["air_pressure"] = "Pa" self.data["air_pressure"] = convert_to( self.driver.vert_data[::-self.driver.vert_order, np.newaxis], self.driver.vert_units, self.data_units["air_pressure"]).repeat(len(self.lats), axis=1) if (("air_potential_temperature" not in self.data) and units(self.driver.vert_units).check("[temperature]")): self.data_units["air_potential_temperature"] = "K" self.data["air_potential_temperature"] = convert_to( self.driver.vert_data[::-self.driver.vert_order, np.newaxis], self.driver.vert_units, self.data_units["air_potential_temperature"]).repeat(len( self.lats), axis=1) # Derive additional data fields and make the plot. self._prepare_datafields() if "air_pressure" not in self.data: raise KeyError( "'air_pressure' need to be available for VSEC plots." "Either provide as data or compute in _prepare_datafields") # Code for producing a png image with Matplotlib. # =============================================== if return_format == "image/png": logging.debug("creating figure..") dpi = 80 figsize = (figsize[0] / dpi), (figsize[1] / dpi) facecolor = "white" self.fig = mpl.figure.Figure(figsize=figsize, dpi=dpi, facecolor=facecolor) logging.debug("\twith frame and legends" if not noframe else "\twithout frame") if noframe: self.ax = self.fig.add_axes([0.0, 0.0, 1.0, 1.0]) else: self.ax = self.fig.add_axes([0.07, 0.17, 0.9, 0.72]) # prepare horizontal axis self.horizontal_coordinate = self.lat_inds[np.newaxis, :].repeat( self.data["air_pressure"].shape[0], axis=0) self._plot_style() # Set transparency for the output image. if transparent: self.fig.patch.set_alpha(0.) # Return the image as png embedded in a StringIO stream. canvas = FigureCanvas(self.fig) output = io.BytesIO() canvas.print_png(output) if show: logging.debug("saving figure to mpl_vsec.png ..") canvas.print_png("mpl_vsec.png") # Convert the image to an 8bit palette image with a significantly # smaller file size (~factor 4, from RGBA to one 8bit value, plus the # space to store the palette colours). # NOTE: PIL at the current time can only create an adaptive palette for # RGB images, hence alpha values are lost here. If transparency is # requested, the figure face colour is stored as the "transparent" # colour in the image. This works in most cases, but might lead to # visible artefacts in some cases. logging.debug("converting image to indexed palette.") # Read the above stored png into a PIL image and create an adaptive # colour palette. output.seek(0) # necessary for PIL.Image.open() palette_img = PIL.Image.open(output).convert(mode="RGB").convert( "P", palette=PIL.Image.ADAPTIVE) output = io.BytesIO() if not transparent: logging.debug("saving figure as non-transparent PNG.") palette_img.save( output, format="PNG") # using optimize=True doesn't change much else: # If the image has a transparent background, we need to find the # index of the background colour in the palette. See the # documentation for PIL's ImagePalette module # (http://www.pythonware.com/library/pil/handbook/imagepalette.htm). The # idea is to create a 256 pixel image with the same colour palette # as the original image and use it as a lookup-table. Converting the # lut image back to RGB gives us a list of all colours in the # palette. (Why doesn't PIL provide a method to directly access the # colours in a palette??) lut = palette_img.resize((256, 1)) lut.putdata(list(range(256))) lut = [c[1] for c in lut.convert("RGB").getcolors()] facecolor_rgb = list( mpl.colors.hex2color(mpl.colors.cnames[facecolor])) for i in [0, 1, 2]: facecolor_rgb[i] = int(facecolor_rgb[i] * 255) facecolor_index = lut.index(tuple(facecolor_rgb)) logging.debug( "saving figure as transparent PNG with transparency index %i.", facecolor_index) palette_img.save(output, format="PNG", transparency=facecolor_index) logging.debug("returning figure..") return output.getvalue() # Code for generating an XML document with the data values in ASCII format. # ========================================================================= elif return_format == "text/xml": impl = getDOMImplementation() xmldoc = impl.createDocument(None, "MSS_VerticalSection_Data", None) # Title of this section. node = xmldoc.createElement("Title") node.appendChild(xmldoc.createTextNode(self.title)) xmldoc.documentElement.appendChild(node) # Time information of this section. node = xmldoc.createElement("ValidTime") node.appendChild( xmldoc.createTextNode( self.valid_time.strftime("%Y-%m-%dT%H:%M:%SZ"))) xmldoc.documentElement.appendChild(node) node = xmldoc.createElement("InitTime") node.appendChild( xmldoc.createTextNode( self.init_time.strftime("%Y-%m-%dT%H:%M:%SZ"))) xmldoc.documentElement.appendChild(node) # Longitude data. node = xmldoc.createElement("Longitude") node.setAttribute("num_waypoints", f"{len(self.lons)}") data_str = "" for value in self.lons: data_str += str(value) + "," data_str = data_str[:-1] node.appendChild(xmldoc.createTextNode(data_str)) xmldoc.documentElement.appendChild(node) # Latitude data. node = xmldoc.createElement("Latitude") node.setAttribute("num_waypoints", f"{len(self.lats)}") data_str = "" for value in self.lats: data_str += str(value) + "," data_str = data_str[:-1] node.appendChild(xmldoc.createTextNode(data_str)) xmldoc.documentElement.appendChild(node) # Variable data. data_node = xmldoc.createElement("Data") for var in self.data: node = xmldoc.createElement(var) data_shape = self.data[var].shape node.setAttribute("num_levels", f"{data_shape[0]}") node.setAttribute("num_waypoints", f"{data_shape[1]}") data_str = "" for data_row in self.data[var]: for value in data_row: data_str += str(value) + "," data_str = data_str[:-1] + "\n" data_str = data_str[:-1] node.appendChild(xmldoc.createTextNode(data_str)) data_node.appendChild(node) xmldoc.documentElement.appendChild(data_node) # Return the XML document as formatted string. return xmldoc.toprettyxml(indent=" ")