def test_grid_by_width(self): """ Test error-free creation and return of a valid grid, with dimensions provided as cell widths. """ grid = GridDimensions((180, 360), "width") dims_by_width = grid.dims_by_width() dims_by_width_expected = (180, 360) self.assertEqual(dims_by_width, dims_by_width_expected)
def test_grid_count_to_width(self): """ Test error-free creation and return of a valid grid with dimensions provided as cell counts, and the correct conversion of the grid to a representation in cell widths. """ grid = GridDimensions((45, 180), "count") dims_by_width = grid.dims_by_width() dims_by_width_expected = (4, 2) self.assertEqual(dims_by_width, dims_by_width_expected)
def test_tiny_grid_cells(self): """ Test correct conversion between grid representations when grid cell widths are very small. """ grid = GridDimensions((2880, 2880), "count") dims_by_width = grid.dims_by_width() dims_by_width_expected = (0.0625, 0.125) dims_by_count = grid.dims_by_count() dims_by_count_expected = (2880, 2880) self.assertEqual(dims_by_width, dims_by_width_expected) self.assertEqual(dims_by_count, dims_by_count_expected)
def test_max_grid_dimensions(self): """ Test that a grid is successfully created when the maximum valid cell widths are provided. """ grid = GridDimensions((180, 360), "width") dims_by_width = grid.dims_by_width() dims_by_width_expected = (180, 360) dims_by_count = grid.dims_by_count() dims_by_count_expected = (1, 1) self.assertEqual(dims_by_width, dims_by_width_expected) self.assertEqual(dims_by_count, dims_by_count_expected)
def test_float_grid_dims(self): """ Test the error-free creation of a valid grid with floating point grid cell widths, and sensible conversion to a representation in cell counts. """ grid = GridDimensions((0.5, 1.5), "width") dims_by_width = grid.dims_by_width() dims_by_width_expected = (0.5, 1.5) dims_by_count = grid.dims_by_count() dims_by_count_expected = (360, 240) self.assertEqual(dims_by_width, dims_by_width_expected) self.assertEqual(dims_by_count, dims_by_count_expected)
class ModelImageRenderer: """ A converter between gridded climate data and image visualizations of the data. Displays data in the form of nested lists or arrays as an overlay on a world map projection. """ def __init__(self: 'ModelImageRenderer', data: np.ndarray) -> None: """ Instantiate a new ModelImageReader. Data to display is provided through the data parameter. This parameter may either by an array, or an array-like structure such as a nested list. the There must be three dimensions to the data: time first, followed by latitude and longitude. :param data: An array-like structure of numeric values, representing temperature over a globe """ self._data = data self._grid = GridDimensions((len(data), len(data[0])), "count") # Some parameters for the visualization are also left as attributes. self._continent_linewidth = 0.5 self._lat_long_linewidth = 0.1 def save_image( self: 'ModelImageRenderer', out_path: str, min_max_grades: Tuple[int, int] = (-60, 60)) -> None: """ Produces a .PNG formatted image file containing the gridded data overlaid on a map projection. The map is displayed in equirectangular projection, labelled with lines of latitude and longitude at the intersection between neighboring grid cells. The grid cells are filled in with a colour denoting the magnitude of the temperature at that cell. The optional min_max_grades parameter specifies the lower bound and the upper bound of the range of values for which different colours are assigned. Any grid cell with a temperature value below the first element of min_max_grades will be assigned the same colour. The same applies to any cell with a temperature greater than min_max_grades[1]. The default boundaries are -60 and 40. The image is saved at the specified absolute or relative filepath. :param out_path: The location where the image file is created :param min_max_grades: A tuple containing the boundary values for the colorbar shown in the image file """ if len(min_max_grades) != 2: raise ValueError("Color grade boundaries must be given in a tuple" "of length 2 (is length {})".format( len(min_max_grades))) # Create an empty world map in equirectangular projection. map = Basemap(llcrnrlat=-90, llcrnrlon=-180, urcrnrlat=90, urcrnrlon=180) map.drawcoastlines(linewidth=self._continent_linewidth) # Construct a grid from the horizontal and vertical sizes of the cells. grid_by_width = self._grid.dims_by_width() lat_val = -90.0 lats = [] while lat_val <= 90: lats.append(lat_val) lat_val += grid_by_width[0] lon_val = -180.0 lons = [] while lon_val <= 180: lons.append(lon_val) lon_val += grid_by_width[1] map.drawparallels(lats, linewidth=self._lat_long_linewidth) map.drawmeridians(lons, linewidth=self._lat_long_linewidth) x, y = map(lons, lats) # Overlap the gridded data on top of the map, and display a colour # legend with the appropriate boundaries. img = map.pcolormesh(x, y, self._data, cmap=plt.cm.get_cmap("jet")) map.colorbar(img) plt.clim(min_max_grades[0], min_max_grades[1]) # Save the image and clear added components from memory plt.savefig(out_path) plt.close()
class ModelImageRenderer: """ A converter between gridded climate data and image visualizations of the data. Displays data in the form of nested lists or arrays as an overlay on a world map projection. """ def __init__(self: 'ModelImageRenderer', data: np.ndarray) -> None: """ Instantiate a new ModelImageReader. Data to display is provided through the data parameter. This parameter may either by an array, or an array-like structure such as a nested list. the There must be three dimensions to the data: time first, followed by latitude and longitude. :param data: An array-like structure of numeric values, representing temperature over a globe """ self._data = data self._grid = GridDimensions((len(data), len(data[0])), "count") # Some parameters for the visualization are also left as attributes. self._continent_linewidth = 0.5 self._lat_long_linewidth = 0.1 def save_image(self: 'ModelImageRenderer', out_path: str, min_max_grades: Tuple[float, float] = (-8, 8)) -> None: """ Produces a .PNG formatted image file containing the gridded data overlaid on a map projection. The map is displayed in equirectangular projection, labelled with lines of latitude and longitude at the intersection between neighboring grid cells. The grid cells are filled in with a colour denoting the magnitude of the temperature at that cell. The optional min_max_grades parameter specifies the lower bound and the upper bound of the range of values for which different colours are assigned. Any grid cell with a temperature value below the first element of min_max_grades will be assigned the same colour. The same applies to any cell with a temperature greater than min_max_grades[1]. The default boundaries are -60 and 40. The image is saved at the specified absolute or relative filepath. :param out_path: The location where the image file is created :param min_max_grades: A tuple containing the boundary values for the colorbar shown in the image file """ if min_max_grades is not None and len(min_max_grades) != 2: raise ValueError("Color grade boundaries must be given in a tuple" "of length 2 (is length {})" .format(len(min_max_grades))) # Create an empty world map in equirectangular projection. map = Basemap(llcrnrlat=-90, llcrnrlon=-180, urcrnrlat=90, urcrnrlon=180) map.drawcoastlines(linewidth=self._continent_linewidth) # Construct a grid from the horizontal and vertical sizes of the cells. grid_by_width = self._grid.dims_by_width() lat_val = -90.0 lats = [] while lat_val <= 90: lats.append(lat_val) lat_val += grid_by_width[0] lon_val = -180.0 lons = [] while lon_val <= 180: lons.append(lon_val) lon_val += grid_by_width[1] map.drawparallels(lats, linewidth=self._lat_long_linewidth) map.drawmeridians(lons, linewidth=self._lat_long_linewidth) x, y = map(lons, lats) # Overlap the gridded data on top of the map, and display a colour # legend with the appropriate boundaries. img_bin = map.pcolormesh(x, y, self._data, cmap=plt.cm.get_cmap("jet")) map.colorbar(img_bin) plt.clim(min_max_grades[0], min_max_grades[1]) fig = plt.gcf() fig.canvas.draw() pixels = fig.canvas.tostring_rgb() img = np.fromstring(pixels, dtype=np.uint8, sep='') img = img.reshape(fig.canvas.get_width_height()[::-1] + (3,)) img = img[118:-113, 80:-30, :] alphas = np.ones(img.shape[:2], dtype=np.uint8) * 255 alphas[:, -65:-57] = 0 alphas[:9, :-43] = 0 alphas[-8:, :-43] = 0 for i in range(img.shape[0]): for j in range(img.shape[1] - 43, img.shape[1]): pixel = tuple(img[i, j, :]) # if sum(pixel) == 765: if sum(pixel) < 450 and j >= img.shape[1] - 33: img[i, j, :] = [147, 149, 152] elif j >= img.shape[1] - 33 or i < 9 or i > img.shape[0] - 9: alphas[i, j] = 0 img = np.dstack((img, alphas)) plt.imsave(fname=out_path, arr=img) plt.close()