Module maven_iuvs.graphics
Expand source code
import os
import cartopy.crs as ccrs
import matplotlib.colors as colors
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.axes_divider import make_axes_locatable
import numpy as np
import spiceypy as spice
from astropy.io import fits
from matplotlib.image import imread
from shapely.geometry import box, Polygon
from shapely.geometry.polygon import LinearRing
from tempfile import NamedTemporaryFile
import pkg_resources
from maven_iuvs.instrument import calculate_calibration_curve
from maven_iuvs.geometry import beta_flip, haversine, rotation_matrix
from maven_iuvs.statistics import multiple_linear_regression, integrate_intensity
from maven_iuvs.instrument import slit_width_deg
from maven_iuvs.constants import R_Mars_km
# color dictionary
color_dict = {'red': '#D62728', 'orange': '#FF7F0E', 'yellow': '#FDB813',
'green': '#2CA02C', 'blue': '#0079C1', 'violet': '#9467BD',
'cyan': '#17BECF', 'magenta': '#D64ECF', 'brown': '#8C564B',
'darkgrey': '#3F3F3F', 'grey': '#7F7F7F', 'lightgrey': '#BFBFBF'}
def JGR_format(dpi=300, display_widths=False, return_blue=False):
"""
Sets matplotlib.pyplot parameters to match fonts and sizes to those of AGU's JGR and GRL journals.
Parameters
----------
dpi : int
DPI (resolution) of output plots. JGR specifies raster images should be between 300 and 600.
Defaults to 300.
display_widths : bool
Whether or not to print out the widths of the various types of JGR figures. Reference for figure creation.
return_blue : bool
If True, returns the hexadecimal color string for the dark blue color used in JGR publications.
Returns
-------
JGR_blue : str
The hexadecimal color string for the JGR blue color used in text and lines in JGR journals.
"""
# match JGR fonts
plt.rc('mathtext', fontset='stix')
plt.rc('font', **{'family': 'STIXGeneral'})
# match JGR font sizes
s = 8
plt.rc('font', size=s)
plt.rc('axes', titlesize=s)
plt.rc('axes', labelsize=s)
plt.rc('xtick', labelsize=s)
plt.rc('ytick', labelsize=s)
plt.rc('legend', fontsize=s)
plt.rc('figure', titlesize=s)
# make sure output text isn't converted to outlines if vector graphics chosen
plt.rc('pdf', fonttype=42)
plt.rc('ps', fonttype=42)
# set thickness of plot borders and lines to 0.5 points (the minimum line width prescribed by AGU)
plthick = 0.5
plt.rc('lines', linewidth=plthick)
plt.rc('axes', linewidth=plthick)
plt.rc('xtick.major', width=plthick)
plt.rc('xtick.minor', width=plthick)
plt.rc('ytick.major', width=plthick)
plt.rc('ytick.minor', width=plthick)
# set DPI for saving figure
plt.rc('savefig', dpi=dpi)
# store JGR blue color
JGR_blue = '#004174'
# JGR figure widths
fullfigure = 7.5 # width of a full-page-wide figure
halffigure = 3.5 # width of a half-page-wide figure with wrapped text
textfigure = 5.6 # width of full-column-width (text-width) figure
if display_widths:
print('JGR journal figure widths:')
print(' Full-page width: %.1f inches' % fullfigure)
print(' Half-page width: %.1f inches' % halffigure)
print(' Text width: %.1f inches' % textfigure)
# give back the JGR blue color if requested
if return_blue:
return JGR_blue
def colorbar(mappable, axis, ticks=None, ticklabels=None, boundaries=None, minor=True, unit='kR'):
"""
Produces a better colorbar than default, making sure that the height of the colorbar matches the height
of the axis to which it's attached.
Parameters
----------
mappable : object
The imshow/meshgrid object with the colored data.
axis : Axis
The axis to which to attach the colorbar.
ticks : int or float list or array
Locations of colorbar ticks.
ticklabels : str list or array
Labels for colorbar ticks.
boundaries : array-like
The boundaries of discrete ticks (analogous to bin edges).
minor : bool
Whether or not to display minor ticks on the colorbar.
unit : str
The unit to display with the highest value on the colorbar. To suppress set to None.
Returns
-------
cbar : Colorbar
The colorbar object.
"""
# create divider for axis
divider = make_axes_locatable(axis)
# place a new axis to the right of the existing axis
cax = divider.append_axes('right', size='2.5%', pad=0.15)
# otherwise, place the colorbar using provided ticks and ticklabels
if ticks is not None:
cbar = plt.colorbar(mappable, cax=cax, ticks=ticks, boundaries=boundaries)
ticklabels = np.array(ticklabels).astype(str)
if unit is not None:
ticklabels[-1] += ' ' + unit
cbar.ax.set_yticklabels(ticklabels)
# if no ticks provided, just place the colorbar with built-in tick marks
else:
cbar = plt.colorbar(mappable, cax=cax, boundaries=boundaries)
if minor:
cbar.ax.minorticks_on()
# return the colorbar
return cbar
def NO_colormap(bad=None, n=256):
"""
Generates the NO nightglow black/green/yellow-green/white colormap (IDL #8).
Parameters
----------
bad : (3,) tuple
Normalized color tuple (R,G,B) for missing data (NaN) display. Defaults to None (bad values are masked).
n : int
Number of colors to generate. Defaults to 256.
Returns
-------
cmap : object
Special NO nightglow colormap.
"""
# color sequence from black -> green -> yellow-green -> white
cmap_colors = [(0, 0, 0), (0, 0.5, 0), (0.61, 0.8, 0.2), (1, 1, 1)]
# set colormap name
cmap_name = 'NO'
# make a colormap using the color sequence and chosen name
cmap = colors.LinearSegmentedColormap.from_list(cmap_name, cmap_colors, N=n)
# set the nan color
if bad is not None:
try:
cmap.set_bad(bad)
except:
raise Exception('Invalid choice for bad data color. Try a color tuple, e.g., (0,0,0).')
# return the colormap
return cmap
def CO2p_colormap(bad=None, n=256):
"""
Generates the custom CO2p black/pink/white colormap.
Parameters
----------
bad : (3,) tuple
Normalized color tuple (R,G,B) for missing data (NaN) display. Defaults to None (bad values are masked).
n : int
Number of colors to generate. Defaults to 256.
Returns
-------
cmap : object
Special aurora colormap.
"""
# color sequence from black -> purple -> white
cmap_colors = [(0, 0, 0), (0.7255, 0.0588, 0.7255), (1, 1, 1)]
# set colormap name
cmap_name = 'CO2p'
# make a colormap using the color sequence and chosen name
cmap = colors.LinearSegmentedColormap.from_list(cmap_name, cmap_colors, N=n)
# set the nan color
if bad is not None:
try:
cmap.set_bad(bad)
except:
raise Exception('Invalid choice for bad data color. Try a color tuple, e.g., (0,0,0).')
# return the colormap
return cmap
def CO_colormap(bad=None, n=256):
"""
Generates the custom CO Cameron band black/red/white colormap (IDL #3).
Parameters
----------
bad : (3,) tuple
Normalized color tuple (R,G,B) for missing data (NaN) display. Defaults to None (bad values are masked).
n : int
Number of colors to generate. Defaults to 256.
Returns
-------
cmap : object
Special aurora colormap.
"""
# color sequence from black -> red -> white
cmap_colors = [(0, 0, 0), (0.722, 0.051, 0), (1, 1, 1)]
# set colormap name
cmap_name = 'CO'
# make a colormap using the color sequence and chosen name
cmap = colors.LinearSegmentedColormap.from_list(cmap_name, cmap_colors, N=n)
# set the nan color
if bad is not None:
try:
cmap.set_bad(bad)
except:
raise Exception('Invalid choice for bad data color. Try a color tuple, e.g., (0,0,0).')
# return the colormap
return cmap
def H_colormap(bad=None, n=256):
"""
Generates the hydrogen Lyman-alpha black/blue/white colormap (IDL #1).
Parameters
----------
bad : (3,) tuple
Normalized color tuple (R,G,B) for missing data (NaN) display. Defaults to None (bad values are masked).
n : int
Number of colors to generate. Defaults to 256.
Returns
-------
cmap : object
Special aurora colormap.
"""
# color sequence from black -> blue -> white
cmap_colors = [(0, 0, 0), (0, 0.204, 0.678), (1, 1, 1)]
# set colormap name
cmap_name = 'H'
# make a colormap using the color sequence and chosen name
cmap = colors.LinearSegmentedColormap.from_list(cmap_name, cmap_colors, N=n)
# set the nan color
if bad is not None:
try:
cmap.set_bad(bad)
except:
raise Exception('Invalid choice for bad data color. Try a color tuple, e.g., (0,0,0).')
# return the colormap
return cmap
def rainbow_colormap(bad=None, n=256):
"""
Generates a custom rainbow colormap based on my custom color dictionary.
Parameters
----------
bad : (3,) tuple
Normalized color tuple (R,G,B) for missing data (NaN) display. Defaults to None (bad values are masked).
n : int
Number of colors to generate. Defaults to 256.
Returns
-------
cmap : object
Special rainbow colormap.
"""
# color sequence from red -> orange -> yellow -> green -> blue -> violet using my custom color dict
rainbow_colors = [(0.839, 0.153, 0.157), (1, 0.498, 0.055), (0.992, 0.722, 0.075),
(0.173, 0.627, 0.173), (0, 0.475, 0.757), (0.58, 0.404, 0.741)]
# set colormap name
cmap_name = 'rainbow'
# make a colormap using the color sequence and chosen name
cmap = colors.LinearSegmentedColormap.from_list(cmap_name, rainbow_colors, N=n)
# set the nan color
if bad is not None:
try:
cmap.set_bad(bad)
except:
raise Exception('Invalid choice for bad data color. Try a color tuple, e.g., (0,0,0).')
# return the colormap
return cmap
def get_flatfield(n_integrations, n_spatial):
"""
Loads the detector flatfield and stacks it by the number of integrations.
Parameters
----------
n_integrations : int
The number of integrations in a sub-swath (FITS file).
n_spatial : int
The number of spatial bins along the slit.
Returns
-------
flatfield : array
The flatfield stacked by number of integrations (n_integrations, n_spatial, 18).
"""
# load the flatfield, interpolate if required using the 133-bin flatfield
if n_spatial == 133:
detector_flat = np.load(os.path.join(pkg_resources.resource_filename('maven_iuvs', 'ancillary/'),
'mvn_iuv_flatfield-133spa-muv.npy'))[:, :18]
elif n_spatial == 50:
detector_flat = np.load(os.path.join(pkg_resources.resource_filename('maven_iuvs', 'ancillary/'),
'mvn_iuv_flatfield-50spa-muv.npy'))[:, :18]
else:
detector_full = np.load(os.path.join(pkg_resources.resource_filename('maven_iuvs', 'ancillary/'),
'mvn_iuv_flatfield-133spa-muv.npy'))[:, :18]
detector_flat = np.zeros((n_spatial, 18))
for i in range(18):
detector_flat[:, i] = np.interp(np.linspace(0, 132, n_spatial), np.arange(133), detector_full[:, i])
# create a flatfield for the given number of integrations
flatfield = np.repeat(detector_flat[None, :], n_integrations, axis=0)
# return the stacked flatfield
return flatfield
def sharpen_image(image):
"""
Take an image and sharpen it using a high-pass filter matrix:
|-----------|
| 0 -1 0 |
| -1 5 -1 |
| 0 -1 0 |
|-----------|
Parameters
----------
image : array-like
An (m,n,3) array of RGB tuples (the image).
Returns
-------
sharpened_image : ndarray
The original imaged sharpened by convolution with a high-pass filter.
"""
# the array I'll need to determine the sharpened image will need to be the size of the image + a 1 pixel border
sharpening_array = np.zeros((image.shape[0] + 2, image.shape[1] + 2, 3))
# fill the array: the interior is the same as the image, the sides are the same as the first/last row/column,
# the corners can be whatever (here they are just 0) (this is only necessary to sharpen the edges of the image)
sharpening_array[1:-1, 1:-1, :] = image
sharpening_array[0, 1:-1, :] = image[0, :, :]
sharpening_array[-1, 1:-1, :] = image[-1, :, :]
sharpening_array[1:-1, 0, :] = image[:, 0, :]
sharpening_array[1:-1, -1, :] = image[:, -1, :]
# make a copy of the image, which will be modified as it gets sharpened
sharpened_image = np.copy(image)
# multiply each pixel by the sharpening matrix
for integration in range(image.shape[0]):
for position in range(image.shape[1]):
for rgb in range(3):
# if the pixel is not a border pixel in sharpening_array, this will execute
try:
sharpened_image[integration, position, rgb] = \
5 * sharpening_array[integration + 1, position + 1, rgb] - \
sharpening_array[integration, position + 1, rgb] - \
sharpening_array[integration + 2, position + 1, rgb] - \
sharpening_array[integration + 1, position, rgb] - \
sharpening_array[integration + 1, position + 2, rgb]
# if the pixel is a border pixel, no sharpening necessary
except IndexError:
continue
# make sure new pixel rgb values aren't outside the range [0, 1]
sharpened_image = np.where(sharpened_image > 1, 1, sharpened_image)
sharpened_image = np.where(sharpened_image < 0, 0, sharpened_image)
# return the new sharpened image
return sharpened_image
def altitude_mask(altitude, disk=True):
"""
Creates a mask for an (m,n) data array which selects only on-disk or off-disk pixels.
Parameters
----------
altitude : array-like, shape (m,n,5) or (m,n,4)
Pixel corner altitudes from an IUVS FITS file.
disk : bool, optional
Choose whether you want to mask limb pixels (default True) or disk pixels (False).
Returns
-------
mask : ndarray
An (m,n) array of ones and NaNs which you can multiply against an (m,n) array of data values.
"""
# get the pixel corner vectors
altitude = altitude[:, :, :4]
# make an array for the mask
mask = np.ones((altitude.shape[0], altitude.shape[1]))
# loop through altitudes, check to see if the pixel is either completely on the disk (all altitudes are 0)
# or off the disk (all altitudes > 0), and mask as specified.
for i in range(altitude.shape[0]):
for j in range(altitude.shape[1]):
if disk:
if np.size(np.where(altitude[i, j] == 0)) != 4:
mask[i, j] = np.nan
elif not disk:
if np.size(np.where(altitude[i, j] == 0)) == 4:
mask[i, j] = np.nan
# return the mask
return mask
def bin_centers_2d(x, y, z, xmin, xmax, ymin, ymax, dx=1, dy=1, return_grid=False):
"""
Takes IUVS pixels as defined by their centers and rebins the data into a rectangular grid. For latitude and
longitude this will work well at lower latitudes, but near the poles adjacent pixel centers will probably skip
several bins in longitude. Other coordinate systems may not have this issue. The function bin_pixels_2d will help
for this case but is far more computationally intensive.
Parameters
----------
x : array
Horizontal axis coordinates of input data, e.g., longitude.
y : array
Vertical axis coordinates of in put data, e.g., latitude.
z : array
Input data, e.g., radiance.
xmin : int, float
Minimum of horizontal axis bins (left edge of first bin).
xmax : int, float
Maximum of horizontal axis bins (right edge of last bin).
ymin : int, float
Minimum of vertical axis bins (bottom edge of first bin).
ymax : int, float
Maximum of vertical axis bins (top edge of last bin).
dx : int, float
Width of horizontal axis bins.
dy : int, float
Height of vertical axis bins.
return_grid : bool
Set to true if you want to also return meshgrids for plotting the binned data. Defaults to False.
Returns
-------
plot_x : array
Meshgrid of horizontal axis coordinates for plotting with pyplot.pcolormesh.
plot_y : array
Meshgrid of vertical axis coordinates for plotting with pyplot.pcolormesh.
binned_data : array
Binned data for display with pyplot.pcolormesh (or pyplot.imshow).
"""
# ensure input data arrays are one-dimensional
x = np.array(x).flatten()
y = np.array(y).flatten()
z = np.array(z).flatten()
# histogram bins
bins = [np.linspace(xmin, xmax, (xmax - xmin) / dx + 1), np.linspace(ymin, ymax, (ymax - ymin) / dy + 1)]
# produce histogram of data
binned_data, plot_x, plot_y = np.histogram2d(x, y, weights=z, bins=bins)
# produce histogram of counts
count, _, _ = np.histogram2d(x, y, bins=bins)
# divide by counts to get average, putting NaNs where no values fell
ind = np.where(count != 0)
binned_data[ind] /= count[ind]
binned_data[np.where(count == 0.)] = np.nan
# return the binned data and the meshgrids to plot it with if requested
if return_grid:
return plot_x, plot_y, binned_data.T
else:
return binned_data.T
def bin_pixels_2d(x, y, z, xmin, xmax, ymin, ymax, xthreshold, xthresh_tolerance, dx=1, dy=1, return_grid=False):
"""
Takes IUVS pixels as defined by their corners and rebins the data into a rectangular grid. This avoids the issue of
near-polar data pixels covering more than one bin, but the pixel center falling into just one bin. This will
essentially "draw" the observation pixel over a binning grid and place its data value into any bin it intersects.
Parameters
----------
x : array
Horizontal axis coordinates of input data, e.g., longitude. Must have four pixel corners in the IUVS FITS file
arrangement:
---------
|1 3|
| 4 |
|0 2|
---------
and all pixels must be defined (no NaNs) otherwise bad things.
y : array
Vertical axis coordinates of in put data, e.g., latitude. Same arrangement and criteria as above.
z : array
Input data, e.g., radiance. Data you want to ignore when plotting can be set to NaNs in this array so long as
the pixel bounds are still defined in x and y.
xmin : int, float
Minimum of horizontal axis bins (left edge of first bin).
xmax : int, float
Maximum of horizontal axis bins (right edge of last bin).
ymin : int, float
Minimum of vertical axis bins (bottom edge of first bin).
ymax : int, float
Maximum of vertical axis bins (top edge of last bin).
xthreshold : int, float
The value at which x values go back to 0, e.g., 360 for longitude or 24 for local time.
xthresh_tolerance : int, float
How far away from the threshold to check for threshold crossing, e.g., could be 15 degrees for longitude which
would say if a pixel has longitudes > 345 and < 15, then it probably crosses the 360/0 boundary.
dx : int, float
Width of horizontal axis bins.
dy : int, float
Height of vertical axis bins.
return_grid : bool
Set to true if you want to also return meshgrids for plotting the binned data. Defaults to False.
Returns
-------
plot_x : array (opt)
Meshgrid of horizontal axis coordinates for plotting with pyplot.pcolormesh.
plot_y : array (opt)
Meshgrid of vertical axis coordinates for plotting with pyplot.pcolormesh.
binned_data : array
Binned data for display with pyplot.pcolormesh (or pyplot.imshow).
"""
# reshape input arrays from IUVS-format to polygon vertices
xr = np.zeros_like(x)
xr[:, :, [0, 1, 2, 3]] = x[:, :, [0, 1, 3, 2]]
yr = np.zeros_like(y)
yr[:, :, [0, 1, 2, 3]] = y[:, :, [0, 1, 3, 2]]
# reshape arrays by collapsing the spatial and integration dimensions
xr = xr.reshape(xr.shape[0] * xr.shape[1], 4)
yr = yr.reshape(yr.shape[0] * yr.shape[1], 4)
z = z.reshape(z.shape[0] * z.shape[1])
# generate array of observation pixel polygons
data_pixels = np.array([Polygon(zip(xr[i], yr[i])) for i in range(len(z))])
# pixels that cross the x threshold (like longitude going from 359 to 0) do weird stuff, so split any pixels that
# do that into two, one for each side of the boundary, store them, then remove the original pixel and data point
# and add the two new pixels and data points to the original lists
# lists to hold new pixels, new data values for those pixels, and indices of pixels which don't cross the boundary
new_pixels = []
new_z = []
good_ind = []
# loop through the pixels
for i in range(len(data_pixels)):
# get pixel exterior coordinates
x, y = data_pixels[i].exterior.coords.xy
# if x has both large and small values indicating it crosses the boundary...
if (np.min(x) < xthresh_tolerance) & (np.max(x) > xthreshold - xthresh_tolerance):
# get the pixel's coordinates and convert longitude to numpy array for math operations
x, y = data_pixels[i].exterior.coords.xy
x = np.array(x)
# copy x values, set small values to the boundary instead and make a new polygon
x1 = x
x1[np.where(x1 < xthresh_tolerance)] = xthreshold
pix1 = Polygon(zip(x1, y))
# copy x values again, but now set large values to 0 instead and make a new polygon
x2 = x
x2[np.where(x2 > xthreshold - xthresh_tolerance)] = 0
pix2 = Polygon(zip(x, y))
# store the two new pixels and their data value
new_pixels.append(pix1)
new_pixels.append(pix2)
new_z.append(z[i])
new_z.append(z[i])
# if it isn't a boundary-crossing pixel, store its index
else:
good_ind.append(i)
# if there were any pixels in the set crossing the boundary...
if len(new_pixels) != 0:
# convert the indices of good pixels to a numpy array
good_ind = np.array(good_ind)
# remove the bad pixels from the pixel and data lists, then append the new pixels to the end
data_pixels = data_pixels[good_ind]
data_pixels = np.append(data_pixels, new_pixels)
z = z[good_ind]
z = np.append(z, new_z)
# calculate the binning dimensions and make empty arrays to hold the binned data totals and count
xdim = int((xmax - xmin) / dx)
ydim = int((ymax - ymin) / dy)
binned_data = np.zeros((ydim, xdim))
count = np.zeros((ydim, xdim))
# determine number of decimal places
decimalx = str(dx)[::-1].find('.')
if decimalx < 0:
decimalx = 0
decimaly = str(dy)[::-1].find('.')
if decimaly < 0:
decimaly = 0
# loop through the data pixels
for k in range(len(data_pixels)):
# make sure the data aren't NaNs and the pixel is actually good (e.g., doesn't intersect itself)
if (not np.isfinite(z[k])) | (not data_pixels[k].is_valid):
continue
# extract the pixel's bounds
bounds = data_pixels[k].bounds
# find the possible pixels limits it can intersect with so you don't have to compare to the entire bin grid,
# but do it to the decimal precision of your bin spacing and make sure they aren't out of bounds
x0 = np.around(bounds[0], decimalx) - dx
if x0 < xmin:
x0 = xmin
y0 = np.around(bounds[1], decimaly) - dy
if y0 < ymin:
y0 = ymin
x1 = np.around(bounds[2], decimalx) + dx
if x1 > xmax:
x1 = xmax
y1 = np.around(bounds[3], decimaly) + dy
if y1 > ymax:
y1 = ymax
# make an array of the potential pixel coordinates it intersects with
lons = np.arange(x0, x1, dx)
lats = np.arange(y0, y1, dy)
# loop through the potential intersections
for i in lons:
# calculate x index
xind = int(i / dx)
for j in lats:
# calculate y index (after converting latitude to colatitude)
yind = int((j + (ymax - ymin) / 2) / dy)
# make a geometric bin pixel
calc_bin = box(i, j, i + dx, j + dy)
# if the data pixel has any interaction with the bin, then record it, exception handling for near-
# boundary pixels
try:
if data_pixels[k].contains(calc_bin) | data_pixels[k].crosses(calc_bin) | \
data_pixels[k].intersects(calc_bin) | data_pixels[k].overlaps(calc_bin) | \
data_pixels[k].touches(calc_bin) | data_pixels[k].within(calc_bin):
binned_data[yind, xind] += z[k]
count[yind, xind] += 1
except IndexError:
continue
# calculate the average in each bin and set empty bins to NaNs
ind = np.where(count != 0)
binned_data[ind] /= count[ind]
binned_data[np.where(count == 0)] = np.nan
# make meshgrid for data display
plot_x, plot_y = np.meshgrid(np.linspace(xmin, xmax, xdim + 1), np.linspace(ymin, ymax, ydim + 1))
# return the binned data and the meshgrids to plot it with if requested
if return_grid:
return plot_x, plot_y, binned_data
else:
return binned_data
def latlon_grid(cx, cy, latitude, longitude, axis):
"""
Places latitude/longitude grid lines and labels on an apoapse swath image.
Parameters
----------
cx : array
Horizontal coordinate centers in angular space.
cy : array
Vertical coordinate centers in angular space.
latitude : array
Pixel latitude values (same shape as cx and vy).
longitude : array
Pixel longitude values (same shape as cx and vy).
axis : Artist
Axis in which you want the latitude/longitude lines drawn.
"""
# set line and label styles
grid_style = dict(colors='white', linestyles='-', linewidths=0.5)
label_style = dict(fmt=r'$%i\degree$', inline=True, fontsize=8)
dlat = 30
dlon = 30
# set longitude to -180 to 180
longitude[np.where(longitude >= 180)] -= 360
# draw latitude contours, place labels, and remove label rotation
latc = axis.contour(cx, cy, latitude, levels=np.arange(-90, 90, dlat), **grid_style)
latl = axis.clabel(latc, **label_style)
[l.set_rotation(0) for l in latl]
# longitude contours are complicated... first up setting the hard threshold at -180 to 180
tlon = np.copy(longitude)
tlon[np.where((tlon <= -170) | (tlon >= 170))] = np.nan
lonc1 = axis.contour(cx, cy, tlon, levels=np.arange(-180, 180, dlon), **grid_style)
lonl1 = axis.clabel(lonc1, **label_style)
[l.set_rotation(0) for l in lonl1]
# then the hard threshold at 360 to 0 using -180 as the label
tlon = np.copy(longitude)
tlon[np.where(tlon >= 0)] -= 360
tlon[np.where((tlon <= -190) | (tlon >= -170))] = np.nan
lonc2 = axis.contour(cx, cy, tlon, levels=[-180], **grid_style)
lonl2 = axis.clabel(lonc2, **label_style)
[l.set_rotation(0) for l in lonl2]
def latlon_meshgrid(hdul):
"""
Returns a latitude/longitude meshgrid suitable for display with matplotlib.pyplot.pcolormesh.
Parameters
----------
hdul : HDUList
Opened level 1B FITS file.
Returns
-------
X : array
An (n+1,m+1) array of pixel longitudes with "n" = number of slit elements and "m" = number of integrations.
Y : array
An (n+1,m+1) array of pixel latitudes with "n" = number of slit elements and "m" = number of integrations.
mask : array
A mask for eliminating pixels with incomplete geometry information.
"""
# get the latitude and longitude arrays
latitude = hdul['pixelgeometry'].data['pixel_corner_lat']
longitude = hdul['pixelgeometry'].data['pixel_corner_lon']
altitude = hdul['pixelgeometry'].data['pixel_corner_mrh_alt'][:, :, 4]
# make meshgrids to hold latitude and longitude grids for pcolormesh display
X = np.zeros((latitude.shape[0] + 1, latitude.shape[1] + 1))
Y = np.zeros((longitude.shape[0] + 1, longitude.shape[1] + 1))
mask = np.ones((latitude.shape[0], latitude.shape[1]))
# loop through pixel geometry arrays
for i in range(int(latitude.shape[0])):
for j in range(int(latitude.shape[1])):
# there are some pixels where some of the pixel corner longitudes are undefined
# if we encounter one of those, set the data value to missing so it isn't displayed
# with pcolormesh
if np.size(np.where(np.isfinite(longitude[i, j]))) != 5:
mask[i, j] = np.nan
# also mask out non-disk pixels
if altitude[i, j] != 0:
mask[i, j] = np.nan
# place the longitude and latitude values in the meshgrids
X[i, j] = longitude[i, j, 1]
X[i + 1, j] = longitude[i, j, 0]
X[i, j + 1] = longitude[i, j, 3]
X[i + 1, j + 1] = longitude[i, j, 2]
Y[i, j] = latitude[i, j, 1]
Y[i + 1, j] = latitude[i, j, 0]
Y[i, j + 1] = latitude[i, j, 3]
Y[i + 1, j + 1] = latitude[i, j, 2]
# set any of the NaN values to zero (otherwise pcolormesh will break even if it isn't displaying the pixel).
X[np.where(~np.isfinite(X))] = 0
Y[np.where(~np.isfinite(Y))] = 0
# set to domain [-180,180)
X[np.where(X > 180)] -= 360
# return the coordinate arrays and the mask
return X, Y, mask
def angle_meshgrid(hdul):
"""
Returns a meshgrid of observations in angular space.
Parameters
----------
hdul : HDUList
Opened level 1B FITS file.
Returns
-------
X : array
An (n+1,m+1) array of pixel longitudes with "n" = number of slit elements and "m" = number of integrations.
Y : array
An (n+1,m+1) array of pixel latitudes with "n" = number of slit elements and "m" = number of integrations.
"""
# get angles of observation and convert from mirror angles to FOV angles
angles = hdul['integration'].data['mirror_deg'] * 2
# get day and night binning
n_integrations = hdul['integration'].data.shape[0]
n_spatial = len(hdul['binning'].data['spapixlo'][0])
# calculate change in angle between integrations, if it fails, base it off of the slit width (square pixels)
try:
dang = np.diff(angles)[0]
except (IndexError, ValueError):
dang = slit_width_deg / n_spatial
# calculate meshgrids
X, Y = np.meshgrid(np.linspace(0, slit_width_deg, n_spatial + 1),
np.linspace(angles[0] - dang / 2, angles[-1] + dang / 2, n_integrations + 1))
# determine beta-flipping
flipped = beta_flip(hdul)
# rotate if beta-flipped
if flipped:
X = np.fliplr(X)
Y = (np.fliplr(Y) - 90) / (-1) + 90
# return meshgrids
return X, Y
def resize_data(data, xdim, ydim):
"""
Takes a data array of shape (n,m) and reshapes it to (ydim, xdim) using interpolation.
Parameters
----------
data : array-like
The data values.
xdim : int
New number of columns.
ydim : int
New number of rows.
Returns
-------
new_data : array-like
The reshaped data values.
"""
# get data dimensions
dims = np.shape(data)
xdata = dims[1]
ydata = dims[0]
# determine if anti-aliasing is necessary
if (xdata > xdim) | (ydata > ydim):
anti_aliasing = True
else:
anti_aliasing = False
# resize the image
new_data = resize(data, [ydim, xdim], order=0, mode='edge', anti_aliasing=anti_aliasing)
# return the resized data
return new_data
def highres_NearsidePerspective(projection, altitude, r=R_Mars_km * 1e3):
"""
Increases the resolution of the circular outline in cartopy.crs.NearsidePerspective projection.
Parameters
----------
projection : obj
A cartopy.crs.NearsidePerspective() projection.
altitude : int, float
Apoapse altitude in meters.
r : float
The radius of the globe in meters (e.g., for Mars this is the radius of Mars in meters).
Returns
-------
None. Changes the resolution of an existing projection.
"""
# re-implement the cartopy code to figure out the new boundary shape
a = np.float(projection.globe.semimajor_axis or r)
h = np.float(altitude)
max_x = a * np.sqrt(h / (2 * a + h))
t = np.linspace(0, 2 * np.pi, 3601)
coords = np.vstack([max_x * np.cos(t), max_x * np.sin(t)])[:, ::-1]
# update the projection boundary
projection._boundary = LinearRing(coords.T)
def highres_Orthographic(projection, r=R_Mars_km * 1e3):
"""
Increases the resolution of the circular outline in cartopy.crs.Orthographic projection.
Parameters
----------
projection : obj
A cartopy.crs.Orthographic() projection.
r : float
The radius of the globe in meters (e.g., for Mars this is the radius of Mars in meters).
Returns
-------
None. Changes the resolution of an existing projection.
"""
# re-implement the cartopy code to figure out the new boundary shape
a = np.float(projection.globe.semimajor_axis or r)
b = np.float(projection.globe.semiminor_axis or a)
t = np.linspace(0, 2 * np.pi, 3601)
coords = np.vstack([a * 0.99999 * np.cos(t), b * 0.99999 * np.sin(t)])[:, ::-1]
# update the projection boundary
projection._boundary = LinearRing(coords.T)
def rotated_transform(et):
"""
Calculate the rotated pole transform for a particular orbit to replicate the viewing geometry at MAVEN apoapse.
Parameters
----------
et : obj
MAVEN apoapsis ephemeris time.
Returns
-------
transform : ???
A Cartopy rotated pole transform.
"""
# calculate various parameters using SPICE
target = 'Mars'
frame = 'MAVEN_MME_2000'
abcorr = 'LT+S'
observer = 'MAVEN'
state, ltime = spice.spkezr(target, et, frame, abcorr, observer)
spoint, trgepc, srfvec = spice.subpnt('Intercept: ellipsoid', target, et, 'IAU_MARS', abcorr, observer)
rpoint, colatpoint, lonpoint = spice.recsph(spoint)
if lonpoint < 0.:
lonpoint += 2 * np.pi
G = 6.673e-11 * 6.4273e23
r = 1e3 * state[0:3]
v = 1e3 * state[3:6]
h = np.cross(r, v)
n = h / np.linalg.norm(h)
ev = np.cross(v, h) / G - r / np.linalg.norm(r)
evn = ev / np.linalg.norm(ev)
b = np.cross(evn, n)
# get the sub-spacecraft latitude and longitude, and altitude (converted to meters)
sublat = 90 - np.degrees(colatpoint)
sublon = np.degrees(lonpoint)
if sublon > 180:
sublon -= 360
alt = np.sqrt(np.sum(srfvec ** 2)) * 1e3
# north pole unit vector in the IAU Mars basis
polar_vector = [0, 0, 1]
# when hovering over the sub-spacecraft point unrotated (the meridian of the point is a straight vertical line,
# this is the exact view when using cartopy's NearsidePerspective or Orthographic with central_longitude and
# central latitude set to the sub-spacecraft point), calculate the angle by which the planet must be rotated
# about the sub-spacecraft point
angle = np.arctan2(np.dot(polar_vector, -b), np.dot(polar_vector, n))
# first, rotate the pole to a different latitude given the subspacecraft latitude
# cartopy's RotatedPole uses the location of the dateline (-180) as the lon_0 coordinate of the north pole
pole_lat = 90 + sublat
pole_lon = -180
# convert pole latitude to colatitude (for spherical coordinates)
# also convert to radians for use with numpy trigonometric functions
phi = pole_lon * np.pi / 180
theta = (90 - pole_lat) * np.pi / 180
# calculate the Cartesian vector pointing to the pole
polar_vector = [np.cos(phi) * np.sin(theta), np.sin(phi) * np.sin(theta), np.cos(theta)]
# by rotating the pole, the observer's sub-point in cartopy's un-transformed coordinates is (0,0)
# the rotation axis is therefore the x-axis
rotation_axis = [1, 0, 0]
# rotate the polar vector by the calculated angle
rotated_polar_vector = np.dot(rotation_matrix(rotation_axis, -angle), polar_vector)
# get the new polar latitude and longitude after the rotation, with longitude offset to dateline
rotated_polar_lon = np.arctan(rotated_polar_vector[1] / rotated_polar_vector[0]) * 180 / np.pi - 180
if sublat < 0:
rotated_polar_lat = 90 - np.arccos(rotated_polar_vector[2] / np.linalg.norm(rotated_polar_vector)) * 180 / np.pi
else:
rotated_polar_lat = 90 + np.arccos(rotated_polar_vector[2] / np.linalg.norm(rotated_polar_vector)) * 180 / np.pi
# calculate a RotatedPole transform for the rotated pole position
transform = ccrs.RotatedPole(pole_latitude=rotated_polar_lat, pole_longitude=rotated_polar_lon,
central_rotated_longitude=0)
# transform the viewer (0,0) point
tcoords = transform.transform_point(0, 0, ccrs.PlateCarree())
# find the angle by which the planet needs to be rotated about it's rotated polar axis and calculate a new
# RotatedPole transform including this angle rotation
rot_ang = sublon - tcoords[0]
transform = ccrs.RotatedPole(pole_latitude=rotated_polar_lat, pole_longitude=rotated_polar_lon,
central_rotated_longitude=rot_ang)
return transform, alt
def mars_orbit_path(a, e, theta):
"""
Generates Mars's orbital path around Sun with angles based on solar longitude (0 degrees points straight right).
Parameters
----------
a : float
Semimajor axis in any units.
e : float
Orbital eccentricity.
theta : array
Angles in radians.
Returns
-------
xr : array
Horizontal rectangular coordinates of rotated orbit.
yr : array
Vertical rectangular coordinates of rotated orbit.
"""
# rotation of periapsis in degrees relative to unrotated ellipse
theta_periapsis = 251
# calculate un-rotated orbit path
x = a * (1 - e ** 2) / (1 + e * np.cos(theta)) * np.cos(theta)
y = a * (1 - e ** 2) / (1 + e * np.cos(theta)) * np.sin(theta)
# rotate base orbit path
xr = x * np.cos(np.radians(theta_periapsis)) - y * np.sin(np.radians(theta_periapsis))
yr = x * np.sin(np.radians(theta_periapsis)) + y * np.cos(np.radians(theta_periapsis))
return xr, yr
def mars_orbit_path_position(a, e, solar_longitude):
"""
Generates orbital path from Ls=0 to given solar longitude position.
Parameters
----------
a : float
Semimajor axis in any units.
e : float
Orbital eccentricity.
solar_longitude : float
Solar longitude in degrees.
Returns
-------
xr : array
Horizontal rectangular coordinates of the partial orbit.
yr : array
Vertical rectangular coordinates of the partial orbit.
"""
# calculate relative starting and stopping positions in rotated ellipse
start = np.radians(90 + 19)
stop = np.radians(solar_longitude + 90 + 19)
# calculate the number of steps to maintain resolution
n = int(1000 * solar_longitude / 360) + 1
# generate array of angles between starting and stopping position
theta = np.linspace(start, stop, n)
# calculate the rotated orbit path from start to stop
x, y = mars_orbit_path(a, e, theta)
# return the orbit path
return x, y
def plot_solar_longitude(ax, solar_longitude):
"""
Plots a Mars orbital path and position of Mars relative to Ls=0 with annotations showing periapsis, apoapsis,
90-degree solar longitude increments, the Sun, and Mars.
Parameters
----------
ax : Artist
Axis in which to plot the path and place the annotations.
solar_longitude : float
Mars's solar longitude in degrees.
Returns
-------
None.
"""
# constants
e = 0.0935 # eccentricity
a = 1.524 # semi-major axis [AU]
theta = np.linspace(0, np.radians(360), 1000)
# plot orbital path
x, y = mars_orbit_path(a, e, theta)
ax.plot(x, y, color='k', linestyle='--', zorder=2)
# plot 90-degree spokes
x0, y0 = mars_orbit_path_position(a, e, 0)
ax.plot([0, x0], [0, y0], color='k', linestyle='--', zorder=2)
ax.scatter([x0], [y0], color='k', s=4, zorder=4)
ax.text(x0 + 0.1, y0, r'$\mathrm{L_s = 0\degree}$', ha='left', va='center', fontsize=8)
for i in [90, 180, 270]:
x, y = mars_orbit_path_position(a, e, i)
ax.scatter([x[-1]], [y[-1]], color='k', s=4, zorder=5)
ax.plot([0, x[-1]], [0, y[-1]], color='k', linestyle='--', zorder=2)
# plot semimajor axis
xp, yp = mars_orbit_path_position(a, e, 251)
xa, ya = mars_orbit_path_position(a, e, 71)
ax.plot([xp[-1], xa[-1]], [yp[-1], ya[-1]], color='k', linestyle='--', zorder=2)
ax.scatter([xp[-1]], [yp[-1]], color='k', s=4, zorder=5)
ax.text(xp[-1] - 0.05, yp[-1] - 0.05, r'Perihelion ($\mathrm{L_s} = 251\degree$)', ha='right', va='top', fontsize=8)
ax.scatter([xa[-1]], [ya[-1]], color='k', s=4, zorder=5)
ax.text(xa[-1] + 0.05, ya[-1] + 0.05, r'Aphelion ($\mathrm{L_s} = 71\degree$)', ha='left', va='bottom', fontsize=8)
# place Sun
ax.scatter([0], [0], color=color_dict['yellow'], s=200, edgecolors='none', zorder=4)
ax.text(0.25, 0.125, 'Sun', fontsize=8)
# plot Mars position
x0, y0 = mars_orbit_path_position(a, e, solar_longitude)
ax.scatter([x0[-1]], [y0[-1]], color=color_dict['red'], edgecolors='none', s=50, zorder=4)
# label Mars
xl, yl = mars_orbit_path_position(a * 0.87, e, solar_longitude)
ax.text(xl[-1], yl[-1], '$\u2642$', ha='center', va='center', fontsize=8, zorder=3,
bbox=dict(facecolor='white', linewidth=0, boxstyle='circle,pad=0.2'))
# set plot aspect
ax.set_aspect('equal')
def terminator(et):
"""
Calculates a terminator image for display over a surface image.
Parameters
----------
et : float
Ephemeris time at which to calculate Mars subsolar position.
Returns
-------
longitudes : array
Meshgrid of longitudes in degrees.
latitudes : array
Meshgrid of latitudes in degrees.
terminator_array : array
Masking array which, when multiplied with a cylindrical map, changes the colors to represent twilight
and nighttime.
"""
# set SPICE inputs
target = 'Mars'
abcorr = 'LT+S'
observer = 'MAVEN'
# calculate subsolar position
sspoint, strgepc, ssrfvec = spice.subslr('Intercept: ellipsoid', target, et, 'IAU_MARS', abcorr, observer)
srpoint, scolatpoint, slonpoint = spice.recsph(sspoint)
if slonpoint > np.pi:
slonpoint -= 2 * np.pi
subsolar_latitude = 90 - np.degrees(scolatpoint)
subsolar_longitude = np.degrees(slonpoint)
# calculate solar zenith angles
longitudes, latitudes, solar_zenith_angles = haversine(subsolar_latitude, subsolar_longitude)
# make a mask and set the values
terminator_mask = np.zeros_like(solar_zenith_angles)
terminator_mask[np.where(solar_zenith_angles > 102)] = 0.4
terminator_mask[np.where(solar_zenith_angles < 90)] = 1
terminator_mask[np.where((solar_zenith_angles >= 90) & (solar_zenith_angles <= 102))] = 0.7
# make the mask 3-dimensional for RGB tuples
terminator_array = np.repeat(terminator_mask[:, :, None], 3, axis=2)
# return the terminator array with plotting meshgrids
return longitudes, latitudes, terminator_array
def reset_symlog_labels(fig, axes):
"""
Changes 10^0 to 1 in the axis labels in a symmetric-logarithmic axis.
Parameters
----------
fig : object
The figure in which the axis resides.
axes : object, array-like
The axes in need of a good reset.
Returns
-------
None.
"""
# draw canvas to place the labels
fig.canvas.draw()
# loop through axes
for ax in axes:
# get the horizontal axis tick labels
labels = ax.get_xticklabels()
# loop through the labels
for label in labels:
# if it's the label for -1, reset it
if label.get_text() == r'$\mathdefault{-10^{0}}$':
label.set_text(r'$\mathdefault{-1}$')
# if it's the label for +1, reset it
elif label.get_text() == r'$\mathdefault{10^{0}}$':
label.set_text(r'$\mathdefault{1}$')
# reset alignment to bottom instead of top
ax.set_xticklabels(labels, va='bottom')
# set tick padding above the label
for tick in ax.get_xaxis().get_major_ticks():
tick.set_pad(11)
Functions
def CO2p_colormap(bad=None, n=256)
-
Generates the custom CO2p black/pink/white colormap.
Parameters
bad
:(3,) tuple
- Normalized color tuple (R,G,B) for missing data (NaN) display. Defaults to None (bad values are masked).
n
:int
- Number of colors to generate. Defaults to 256.
Returns
cmap
:object
- Special aurora colormap.
Expand source code
def CO2p_colormap(bad=None, n=256): """ Generates the custom CO2p black/pink/white colormap. Parameters ---------- bad : (3,) tuple Normalized color tuple (R,G,B) for missing data (NaN) display. Defaults to None (bad values are masked). n : int Number of colors to generate. Defaults to 256. Returns ------- cmap : object Special aurora colormap. """ # color sequence from black -> purple -> white cmap_colors = [(0, 0, 0), (0.7255, 0.0588, 0.7255), (1, 1, 1)] # set colormap name cmap_name = 'CO2p' # make a colormap using the color sequence and chosen name cmap = colors.LinearSegmentedColormap.from_list(cmap_name, cmap_colors, N=n) # set the nan color if bad is not None: try: cmap.set_bad(bad) except: raise Exception('Invalid choice for bad data color. Try a color tuple, e.g., (0,0,0).') # return the colormap return cmap
def CO_colormap(bad=None, n=256)
-
Generates the custom CO Cameron band black/red/white colormap (IDL #3).
Parameters
bad
:(3,) tuple
- Normalized color tuple (R,G,B) for missing data (NaN) display. Defaults to None (bad values are masked).
n
:int
- Number of colors to generate. Defaults to 256.
Returns
cmap
:object
- Special aurora colormap.
Expand source code
def CO_colormap(bad=None, n=256): """ Generates the custom CO Cameron band black/red/white colormap (IDL #3). Parameters ---------- bad : (3,) tuple Normalized color tuple (R,G,B) for missing data (NaN) display. Defaults to None (bad values are masked). n : int Number of colors to generate. Defaults to 256. Returns ------- cmap : object Special aurora colormap. """ # color sequence from black -> red -> white cmap_colors = [(0, 0, 0), (0.722, 0.051, 0), (1, 1, 1)] # set colormap name cmap_name = 'CO' # make a colormap using the color sequence and chosen name cmap = colors.LinearSegmentedColormap.from_list(cmap_name, cmap_colors, N=n) # set the nan color if bad is not None: try: cmap.set_bad(bad) except: raise Exception('Invalid choice for bad data color. Try a color tuple, e.g., (0,0,0).') # return the colormap return cmap
def H_colormap(bad=None, n=256)
-
Generates the hydrogen Lyman-alpha black/blue/white colormap (IDL #1).
Parameters
bad
:(3,) tuple
- Normalized color tuple (R,G,B) for missing data (NaN) display. Defaults to None (bad values are masked).
n
:int
- Number of colors to generate. Defaults to 256.
Returns
cmap
:object
- Special aurora colormap.
Expand source code
def H_colormap(bad=None, n=256): """ Generates the hydrogen Lyman-alpha black/blue/white colormap (IDL #1). Parameters ---------- bad : (3,) tuple Normalized color tuple (R,G,B) for missing data (NaN) display. Defaults to None (bad values are masked). n : int Number of colors to generate. Defaults to 256. Returns ------- cmap : object Special aurora colormap. """ # color sequence from black -> blue -> white cmap_colors = [(0, 0, 0), (0, 0.204, 0.678), (1, 1, 1)] # set colormap name cmap_name = 'H' # make a colormap using the color sequence and chosen name cmap = colors.LinearSegmentedColormap.from_list(cmap_name, cmap_colors, N=n) # set the nan color if bad is not None: try: cmap.set_bad(bad) except: raise Exception('Invalid choice for bad data color. Try a color tuple, e.g., (0,0,0).') # return the colormap return cmap
def JGR_format(dpi=300, display_widths=False, return_blue=False)
-
Sets matplotlib.pyplot parameters to match fonts and sizes to those of AGU's JGR and GRL journals.
Parameters
dpi
:int
- DPI (resolution) of output plots. JGR specifies raster images should be between 300 and 600. Defaults to 300.
display_widths
:bool
- Whether or not to print out the widths of the various types of JGR figures. Reference for figure creation.
return_blue
:bool
- If True, returns the hexadecimal color string for the dark blue color used in JGR publications.
Returns
JGR_blue
:str
- The hexadecimal color string for the JGR blue color used in text and lines in JGR journals.
Expand source code
def JGR_format(dpi=300, display_widths=False, return_blue=False): """ Sets matplotlib.pyplot parameters to match fonts and sizes to those of AGU's JGR and GRL journals. Parameters ---------- dpi : int DPI (resolution) of output plots. JGR specifies raster images should be between 300 and 600. Defaults to 300. display_widths : bool Whether or not to print out the widths of the various types of JGR figures. Reference for figure creation. return_blue : bool If True, returns the hexadecimal color string for the dark blue color used in JGR publications. Returns ------- JGR_blue : str The hexadecimal color string for the JGR blue color used in text and lines in JGR journals. """ # match JGR fonts plt.rc('mathtext', fontset='stix') plt.rc('font', **{'family': 'STIXGeneral'}) # match JGR font sizes s = 8 plt.rc('font', size=s) plt.rc('axes', titlesize=s) plt.rc('axes', labelsize=s) plt.rc('xtick', labelsize=s) plt.rc('ytick', labelsize=s) plt.rc('legend', fontsize=s) plt.rc('figure', titlesize=s) # make sure output text isn't converted to outlines if vector graphics chosen plt.rc('pdf', fonttype=42) plt.rc('ps', fonttype=42) # set thickness of plot borders and lines to 0.5 points (the minimum line width prescribed by AGU) plthick = 0.5 plt.rc('lines', linewidth=plthick) plt.rc('axes', linewidth=plthick) plt.rc('xtick.major', width=plthick) plt.rc('xtick.minor', width=plthick) plt.rc('ytick.major', width=plthick) plt.rc('ytick.minor', width=plthick) # set DPI for saving figure plt.rc('savefig', dpi=dpi) # store JGR blue color JGR_blue = '#004174' # JGR figure widths fullfigure = 7.5 # width of a full-page-wide figure halffigure = 3.5 # width of a half-page-wide figure with wrapped text textfigure = 5.6 # width of full-column-width (text-width) figure if display_widths: print('JGR journal figure widths:') print(' Full-page width: %.1f inches' % fullfigure) print(' Half-page width: %.1f inches' % halffigure) print(' Text width: %.1f inches' % textfigure) # give back the JGR blue color if requested if return_blue: return JGR_blue
def NO_colormap(bad=None, n=256)
-
Generates the NO nightglow black/green/yellow-green/white colormap (IDL #8).
Parameters
bad
:(3,) tuple
- Normalized color tuple (R,G,B) for missing data (NaN) display. Defaults to None (bad values are masked).
n
:int
- Number of colors to generate. Defaults to 256.
Returns
cmap
:object
- Special NO nightglow colormap.
Expand source code
def NO_colormap(bad=None, n=256): """ Generates the NO nightglow black/green/yellow-green/white colormap (IDL #8). Parameters ---------- bad : (3,) tuple Normalized color tuple (R,G,B) for missing data (NaN) display. Defaults to None (bad values are masked). n : int Number of colors to generate. Defaults to 256. Returns ------- cmap : object Special NO nightglow colormap. """ # color sequence from black -> green -> yellow-green -> white cmap_colors = [(0, 0, 0), (0, 0.5, 0), (0.61, 0.8, 0.2), (1, 1, 1)] # set colormap name cmap_name = 'NO' # make a colormap using the color sequence and chosen name cmap = colors.LinearSegmentedColormap.from_list(cmap_name, cmap_colors, N=n) # set the nan color if bad is not None: try: cmap.set_bad(bad) except: raise Exception('Invalid choice for bad data color. Try a color tuple, e.g., (0,0,0).') # return the colormap return cmap
def altitude_mask(altitude, disk=True)
-
Creates a mask for an (m,n) data array which selects only on-disk or off-disk pixels.
Parameters
altitude
:array-like, shape (m,n,5)
or(m,n,4)
- Pixel corner altitudes from an IUVS FITS file.
disk
:bool
, optional- Choose whether you want to mask limb pixels (default True) or disk pixels (False).
Returns
mask
:ndarray
- An (m,n) array of ones and NaNs which you can multiply against an (m,n) array of data values.
Expand source code
def altitude_mask(altitude, disk=True): """ Creates a mask for an (m,n) data array which selects only on-disk or off-disk pixels. Parameters ---------- altitude : array-like, shape (m,n,5) or (m,n,4) Pixel corner altitudes from an IUVS FITS file. disk : bool, optional Choose whether you want to mask limb pixels (default True) or disk pixels (False). Returns ------- mask : ndarray An (m,n) array of ones and NaNs which you can multiply against an (m,n) array of data values. """ # get the pixel corner vectors altitude = altitude[:, :, :4] # make an array for the mask mask = np.ones((altitude.shape[0], altitude.shape[1])) # loop through altitudes, check to see if the pixel is either completely on the disk (all altitudes are 0) # or off the disk (all altitudes > 0), and mask as specified. for i in range(altitude.shape[0]): for j in range(altitude.shape[1]): if disk: if np.size(np.where(altitude[i, j] == 0)) != 4: mask[i, j] = np.nan elif not disk: if np.size(np.where(altitude[i, j] == 0)) == 4: mask[i, j] = np.nan # return the mask return mask
def angle_meshgrid(hdul)
-
Returns a meshgrid of observations in angular space.
Parameters
hdul
:HDUList
- Opened level 1B FITS file.
Returns
X
:array
- An (n+1,m+1) array of pixel longitudes with "n" = number of slit elements and "m" = number of integrations.
Y
:array
- An (n+1,m+1) array of pixel latitudes with "n" = number of slit elements and "m" = number of integrations.
Expand source code
def angle_meshgrid(hdul): """ Returns a meshgrid of observations in angular space. Parameters ---------- hdul : HDUList Opened level 1B FITS file. Returns ------- X : array An (n+1,m+1) array of pixel longitudes with "n" = number of slit elements and "m" = number of integrations. Y : array An (n+1,m+1) array of pixel latitudes with "n" = number of slit elements and "m" = number of integrations. """ # get angles of observation and convert from mirror angles to FOV angles angles = hdul['integration'].data['mirror_deg'] * 2 # get day and night binning n_integrations = hdul['integration'].data.shape[0] n_spatial = len(hdul['binning'].data['spapixlo'][0]) # calculate change in angle between integrations, if it fails, base it off of the slit width (square pixels) try: dang = np.diff(angles)[0] except (IndexError, ValueError): dang = slit_width_deg / n_spatial # calculate meshgrids X, Y = np.meshgrid(np.linspace(0, slit_width_deg, n_spatial + 1), np.linspace(angles[0] - dang / 2, angles[-1] + dang / 2, n_integrations + 1)) # determine beta-flipping flipped = beta_flip(hdul) # rotate if beta-flipped if flipped: X = np.fliplr(X) Y = (np.fliplr(Y) - 90) / (-1) + 90 # return meshgrids return X, Y
def bin_centers_2d(x, y, z, xmin, xmax, ymin, ymax, dx=1, dy=1, return_grid=False)
-
Takes IUVS pixels as defined by their centers and rebins the data into a rectangular grid. For latitude and longitude this will work well at lower latitudes, but near the poles adjacent pixel centers will probably skip several bins in longitude. Other coordinate systems may not have this issue. The function bin_pixels_2d will help for this case but is far more computationally intensive.
Parameters
x
:array
- Horizontal axis coordinates of input data, e.g., longitude.
y
:array
- Vertical axis coordinates of in put data, e.g., latitude.
z
:array
- Input data, e.g., radiance.
xmin
:int, float
- Minimum of horizontal axis bins (left edge of first bin).
xmax
:int, float
- Maximum of horizontal axis bins (right edge of last bin).
ymin
:int, float
- Minimum of vertical axis bins (bottom edge of first bin).
ymax
:int, float
- Maximum of vertical axis bins (top edge of last bin).
dx
:int, float
- Width of horizontal axis bins.
dy
:int, float
- Height of vertical axis bins.
return_grid
:bool
- Set to true if you want to also return meshgrids for plotting the binned data. Defaults to False.
Returns
plot_x
:array
- Meshgrid of horizontal axis coordinates for plotting with pyplot.pcolormesh.
plot_y
:array
- Meshgrid of vertical axis coordinates for plotting with pyplot.pcolormesh.
binned_data
:array
- Binned data for display with pyplot.pcolormesh (or pyplot.imshow).
Expand source code
def bin_centers_2d(x, y, z, xmin, xmax, ymin, ymax, dx=1, dy=1, return_grid=False): """ Takes IUVS pixels as defined by their centers and rebins the data into a rectangular grid. For latitude and longitude this will work well at lower latitudes, but near the poles adjacent pixel centers will probably skip several bins in longitude. Other coordinate systems may not have this issue. The function bin_pixels_2d will help for this case but is far more computationally intensive. Parameters ---------- x : array Horizontal axis coordinates of input data, e.g., longitude. y : array Vertical axis coordinates of in put data, e.g., latitude. z : array Input data, e.g., radiance. xmin : int, float Minimum of horizontal axis bins (left edge of first bin). xmax : int, float Maximum of horizontal axis bins (right edge of last bin). ymin : int, float Minimum of vertical axis bins (bottom edge of first bin). ymax : int, float Maximum of vertical axis bins (top edge of last bin). dx : int, float Width of horizontal axis bins. dy : int, float Height of vertical axis bins. return_grid : bool Set to true if you want to also return meshgrids for plotting the binned data. Defaults to False. Returns ------- plot_x : array Meshgrid of horizontal axis coordinates for plotting with pyplot.pcolormesh. plot_y : array Meshgrid of vertical axis coordinates for plotting with pyplot.pcolormesh. binned_data : array Binned data for display with pyplot.pcolormesh (or pyplot.imshow). """ # ensure input data arrays are one-dimensional x = np.array(x).flatten() y = np.array(y).flatten() z = np.array(z).flatten() # histogram bins bins = [np.linspace(xmin, xmax, (xmax - xmin) / dx + 1), np.linspace(ymin, ymax, (ymax - ymin) / dy + 1)] # produce histogram of data binned_data, plot_x, plot_y = np.histogram2d(x, y, weights=z, bins=bins) # produce histogram of counts count, _, _ = np.histogram2d(x, y, bins=bins) # divide by counts to get average, putting NaNs where no values fell ind = np.where(count != 0) binned_data[ind] /= count[ind] binned_data[np.where(count == 0.)] = np.nan # return the binned data and the meshgrids to plot it with if requested if return_grid: return plot_x, plot_y, binned_data.T else: return binned_data.T
def bin_pixels_2d(x, y, z, xmin, xmax, ymin, ymax, xthreshold, xthresh_tolerance, dx=1, dy=1, return_grid=False)
-
Takes IUVS pixels as defined by their corners and rebins the data into a rectangular grid. This avoids the issue of near-polar data pixels covering more than one bin, but the pixel center falling into just one bin. This will essentially "draw" the observation pixel over a binning grid and place its data value into any bin it intersects.
Parameters
x
:array
- Horizontal axis coordinates of input data, e.g., longitude. Must have four pixel corners in the IUVS FITS file
arrangement:
|1 3| | 4 | |0 2|
and all pixels must be defined (no NaNs) otherwise bad things. y
:array
- Vertical axis coordinates of in put data, e.g., latitude. Same arrangement and criteria as above.
z
:array
- Input data, e.g., radiance. Data you want to ignore when plotting can be set to NaNs in this array so long as the pixel bounds are still defined in x and y.
xmin
:int, float
- Minimum of horizontal axis bins (left edge of first bin).
xmax
:int, float
- Maximum of horizontal axis bins (right edge of last bin).
ymin
:int, float
- Minimum of vertical axis bins (bottom edge of first bin).
ymax
:int, float
- Maximum of vertical axis bins (top edge of last bin).
xthreshold
:int, float
- The value at which x values go back to 0, e.g., 360 for longitude or 24 for local time.
xthresh_tolerance
:int, float
- How far away from the threshold to check for threshold crossing, e.g., could be 15 degrees for longitude which would say if a pixel has longitudes > 345 and < 15, then it probably crosses the 360/0 boundary.
dx
:int, float
- Width of horizontal axis bins.
dy
:int, float
- Height of vertical axis bins.
return_grid
:bool
- Set to true if you want to also return meshgrids for plotting the binned data. Defaults to False.
Returns
plot_x
:array (opt)
- Meshgrid of horizontal axis coordinates for plotting with pyplot.pcolormesh.
plot_y
:array (opt)
- Meshgrid of vertical axis coordinates for plotting with pyplot.pcolormesh.
binned_data
:array
- Binned data for display with pyplot.pcolormesh (or pyplot.imshow).
Expand source code
def bin_pixels_2d(x, y, z, xmin, xmax, ymin, ymax, xthreshold, xthresh_tolerance, dx=1, dy=1, return_grid=False): """ Takes IUVS pixels as defined by their corners and rebins the data into a rectangular grid. This avoids the issue of near-polar data pixels covering more than one bin, but the pixel center falling into just one bin. This will essentially "draw" the observation pixel over a binning grid and place its data value into any bin it intersects. Parameters ---------- x : array Horizontal axis coordinates of input data, e.g., longitude. Must have four pixel corners in the IUVS FITS file arrangement: --------- |1 3| | 4 | |0 2| --------- and all pixels must be defined (no NaNs) otherwise bad things. y : array Vertical axis coordinates of in put data, e.g., latitude. Same arrangement and criteria as above. z : array Input data, e.g., radiance. Data you want to ignore when plotting can be set to NaNs in this array so long as the pixel bounds are still defined in x and y. xmin : int, float Minimum of horizontal axis bins (left edge of first bin). xmax : int, float Maximum of horizontal axis bins (right edge of last bin). ymin : int, float Minimum of vertical axis bins (bottom edge of first bin). ymax : int, float Maximum of vertical axis bins (top edge of last bin). xthreshold : int, float The value at which x values go back to 0, e.g., 360 for longitude or 24 for local time. xthresh_tolerance : int, float How far away from the threshold to check for threshold crossing, e.g., could be 15 degrees for longitude which would say if a pixel has longitudes > 345 and < 15, then it probably crosses the 360/0 boundary. dx : int, float Width of horizontal axis bins. dy : int, float Height of vertical axis bins. return_grid : bool Set to true if you want to also return meshgrids for plotting the binned data. Defaults to False. Returns ------- plot_x : array (opt) Meshgrid of horizontal axis coordinates for plotting with pyplot.pcolormesh. plot_y : array (opt) Meshgrid of vertical axis coordinates for plotting with pyplot.pcolormesh. binned_data : array Binned data for display with pyplot.pcolormesh (or pyplot.imshow). """ # reshape input arrays from IUVS-format to polygon vertices xr = np.zeros_like(x) xr[:, :, [0, 1, 2, 3]] = x[:, :, [0, 1, 3, 2]] yr = np.zeros_like(y) yr[:, :, [0, 1, 2, 3]] = y[:, :, [0, 1, 3, 2]] # reshape arrays by collapsing the spatial and integration dimensions xr = xr.reshape(xr.shape[0] * xr.shape[1], 4) yr = yr.reshape(yr.shape[0] * yr.shape[1], 4) z = z.reshape(z.shape[0] * z.shape[1]) # generate array of observation pixel polygons data_pixels = np.array([Polygon(zip(xr[i], yr[i])) for i in range(len(z))]) # pixels that cross the x threshold (like longitude going from 359 to 0) do weird stuff, so split any pixels that # do that into two, one for each side of the boundary, store them, then remove the original pixel and data point # and add the two new pixels and data points to the original lists # lists to hold new pixels, new data values for those pixels, and indices of pixels which don't cross the boundary new_pixels = [] new_z = [] good_ind = [] # loop through the pixels for i in range(len(data_pixels)): # get pixel exterior coordinates x, y = data_pixels[i].exterior.coords.xy # if x has both large and small values indicating it crosses the boundary... if (np.min(x) < xthresh_tolerance) & (np.max(x) > xthreshold - xthresh_tolerance): # get the pixel's coordinates and convert longitude to numpy array for math operations x, y = data_pixels[i].exterior.coords.xy x = np.array(x) # copy x values, set small values to the boundary instead and make a new polygon x1 = x x1[np.where(x1 < xthresh_tolerance)] = xthreshold pix1 = Polygon(zip(x1, y)) # copy x values again, but now set large values to 0 instead and make a new polygon x2 = x x2[np.where(x2 > xthreshold - xthresh_tolerance)] = 0 pix2 = Polygon(zip(x, y)) # store the two new pixels and their data value new_pixels.append(pix1) new_pixels.append(pix2) new_z.append(z[i]) new_z.append(z[i]) # if it isn't a boundary-crossing pixel, store its index else: good_ind.append(i) # if there were any pixels in the set crossing the boundary... if len(new_pixels) != 0: # convert the indices of good pixels to a numpy array good_ind = np.array(good_ind) # remove the bad pixels from the pixel and data lists, then append the new pixels to the end data_pixels = data_pixels[good_ind] data_pixels = np.append(data_pixels, new_pixels) z = z[good_ind] z = np.append(z, new_z) # calculate the binning dimensions and make empty arrays to hold the binned data totals and count xdim = int((xmax - xmin) / dx) ydim = int((ymax - ymin) / dy) binned_data = np.zeros((ydim, xdim)) count = np.zeros((ydim, xdim)) # determine number of decimal places decimalx = str(dx)[::-1].find('.') if decimalx < 0: decimalx = 0 decimaly = str(dy)[::-1].find('.') if decimaly < 0: decimaly = 0 # loop through the data pixels for k in range(len(data_pixels)): # make sure the data aren't NaNs and the pixel is actually good (e.g., doesn't intersect itself) if (not np.isfinite(z[k])) | (not data_pixels[k].is_valid): continue # extract the pixel's bounds bounds = data_pixels[k].bounds # find the possible pixels limits it can intersect with so you don't have to compare to the entire bin grid, # but do it to the decimal precision of your bin spacing and make sure they aren't out of bounds x0 = np.around(bounds[0], decimalx) - dx if x0 < xmin: x0 = xmin y0 = np.around(bounds[1], decimaly) - dy if y0 < ymin: y0 = ymin x1 = np.around(bounds[2], decimalx) + dx if x1 > xmax: x1 = xmax y1 = np.around(bounds[3], decimaly) + dy if y1 > ymax: y1 = ymax # make an array of the potential pixel coordinates it intersects with lons = np.arange(x0, x1, dx) lats = np.arange(y0, y1, dy) # loop through the potential intersections for i in lons: # calculate x index xind = int(i / dx) for j in lats: # calculate y index (after converting latitude to colatitude) yind = int((j + (ymax - ymin) / 2) / dy) # make a geometric bin pixel calc_bin = box(i, j, i + dx, j + dy) # if the data pixel has any interaction with the bin, then record it, exception handling for near- # boundary pixels try: if data_pixels[k].contains(calc_bin) | data_pixels[k].crosses(calc_bin) | \ data_pixels[k].intersects(calc_bin) | data_pixels[k].overlaps(calc_bin) | \ data_pixels[k].touches(calc_bin) | data_pixels[k].within(calc_bin): binned_data[yind, xind] += z[k] count[yind, xind] += 1 except IndexError: continue # calculate the average in each bin and set empty bins to NaNs ind = np.where(count != 0) binned_data[ind] /= count[ind] binned_data[np.where(count == 0)] = np.nan # make meshgrid for data display plot_x, plot_y = np.meshgrid(np.linspace(xmin, xmax, xdim + 1), np.linspace(ymin, ymax, ydim + 1)) # return the binned data and the meshgrids to plot it with if requested if return_grid: return plot_x, plot_y, binned_data else: return binned_data
def colorbar(mappable, axis, ticks=None, ticklabels=None, boundaries=None, minor=True, unit='kR')
-
Produces a better colorbar than default, making sure that the height of the colorbar matches the height of the axis to which it's attached.
Parameters
mappable
:object
- The imshow/meshgrid object with the colored data.
axis
:Axis
- The axis to which to attach the colorbar.
ticks
:int
orfloat list
orarray
- Locations of colorbar ticks.
ticklabels
:str list
orarray
- Labels for colorbar ticks.
boundaries
:array-like
- The boundaries of discrete ticks (analogous to bin edges).
minor
:bool
- Whether or not to display minor ticks on the colorbar.
unit
:str
- The unit to display with the highest value on the colorbar. To suppress set to None.
Returns
cbar
:Colorbar
- The colorbar object.
Expand source code
def colorbar(mappable, axis, ticks=None, ticklabels=None, boundaries=None, minor=True, unit='kR'): """ Produces a better colorbar than default, making sure that the height of the colorbar matches the height of the axis to which it's attached. Parameters ---------- mappable : object The imshow/meshgrid object with the colored data. axis : Axis The axis to which to attach the colorbar. ticks : int or float list or array Locations of colorbar ticks. ticklabels : str list or array Labels for colorbar ticks. boundaries : array-like The boundaries of discrete ticks (analogous to bin edges). minor : bool Whether or not to display minor ticks on the colorbar. unit : str The unit to display with the highest value on the colorbar. To suppress set to None. Returns ------- cbar : Colorbar The colorbar object. """ # create divider for axis divider = make_axes_locatable(axis) # place a new axis to the right of the existing axis cax = divider.append_axes('right', size='2.5%', pad=0.15) # otherwise, place the colorbar using provided ticks and ticklabels if ticks is not None: cbar = plt.colorbar(mappable, cax=cax, ticks=ticks, boundaries=boundaries) ticklabels = np.array(ticklabels).astype(str) if unit is not None: ticklabels[-1] += ' ' + unit cbar.ax.set_yticklabels(ticklabels) # if no ticks provided, just place the colorbar with built-in tick marks else: cbar = plt.colorbar(mappable, cax=cax, boundaries=boundaries) if minor: cbar.ax.minorticks_on() # return the colorbar return cbar
def get_flatfield(n_integrations, n_spatial)
-
Loads the detector flatfield and stacks it by the number of integrations.
Parameters
n_integrations
:int
- The number of integrations in a sub-swath (FITS file).
n_spatial
:int
- The number of spatial bins along the slit.
Returns
flatfield
:array
- The flatfield stacked by number of integrations (n_integrations, n_spatial, 18).
Expand source code
def get_flatfield(n_integrations, n_spatial): """ Loads the detector flatfield and stacks it by the number of integrations. Parameters ---------- n_integrations : int The number of integrations in a sub-swath (FITS file). n_spatial : int The number of spatial bins along the slit. Returns ------- flatfield : array The flatfield stacked by number of integrations (n_integrations, n_spatial, 18). """ # load the flatfield, interpolate if required using the 133-bin flatfield if n_spatial == 133: detector_flat = np.load(os.path.join(pkg_resources.resource_filename('maven_iuvs', 'ancillary/'), 'mvn_iuv_flatfield-133spa-muv.npy'))[:, :18] elif n_spatial == 50: detector_flat = np.load(os.path.join(pkg_resources.resource_filename('maven_iuvs', 'ancillary/'), 'mvn_iuv_flatfield-50spa-muv.npy'))[:, :18] else: detector_full = np.load(os.path.join(pkg_resources.resource_filename('maven_iuvs', 'ancillary/'), 'mvn_iuv_flatfield-133spa-muv.npy'))[:, :18] detector_flat = np.zeros((n_spatial, 18)) for i in range(18): detector_flat[:, i] = np.interp(np.linspace(0, 132, n_spatial), np.arange(133), detector_full[:, i]) # create a flatfield for the given number of integrations flatfield = np.repeat(detector_flat[None, :], n_integrations, axis=0) # return the stacked flatfield return flatfield
def highres_NearsidePerspective(projection, altitude, r=3389500.0)
-
Increases the resolution of the circular outline in cartopy.crs.NearsidePerspective projection.
Parameters
projection
:obj
- A cartopy.crs.NearsidePerspective() projection.
altitude
:int, float
- Apoapse altitude in meters.
r
:float
- The radius of the globe in meters (e.g., for Mars this is the radius of Mars in meters).
Returns
None. Changes the resolution of an existing projection.
Expand source code
def highres_NearsidePerspective(projection, altitude, r=R_Mars_km * 1e3): """ Increases the resolution of the circular outline in cartopy.crs.NearsidePerspective projection. Parameters ---------- projection : obj A cartopy.crs.NearsidePerspective() projection. altitude : int, float Apoapse altitude in meters. r : float The radius of the globe in meters (e.g., for Mars this is the radius of Mars in meters). Returns ------- None. Changes the resolution of an existing projection. """ # re-implement the cartopy code to figure out the new boundary shape a = np.float(projection.globe.semimajor_axis or r) h = np.float(altitude) max_x = a * np.sqrt(h / (2 * a + h)) t = np.linspace(0, 2 * np.pi, 3601) coords = np.vstack([max_x * np.cos(t), max_x * np.sin(t)])[:, ::-1] # update the projection boundary projection._boundary = LinearRing(coords.T)
def highres_Orthographic(projection, r=3389500.0)
-
Increases the resolution of the circular outline in cartopy.crs.Orthographic projection.
Parameters
projection
:obj
- A cartopy.crs.Orthographic() projection.
r
:float
- The radius of the globe in meters (e.g., for Mars this is the radius of Mars in meters).
Returns
None. Changes the resolution of an existing projection.
Expand source code
def highres_Orthographic(projection, r=R_Mars_km * 1e3): """ Increases the resolution of the circular outline in cartopy.crs.Orthographic projection. Parameters ---------- projection : obj A cartopy.crs.Orthographic() projection. r : float The radius of the globe in meters (e.g., for Mars this is the radius of Mars in meters). Returns ------- None. Changes the resolution of an existing projection. """ # re-implement the cartopy code to figure out the new boundary shape a = np.float(projection.globe.semimajor_axis or r) b = np.float(projection.globe.semiminor_axis or a) t = np.linspace(0, 2 * np.pi, 3601) coords = np.vstack([a * 0.99999 * np.cos(t), b * 0.99999 * np.sin(t)])[:, ::-1] # update the projection boundary projection._boundary = LinearRing(coords.T)
def latlon_grid(cx, cy, latitude, longitude, axis)
-
Places latitude/longitude grid lines and labels on an apoapse swath image.
Parameters
cx
:array
- Horizontal coordinate centers in angular space.
cy
:array
- Vertical coordinate centers in angular space.
latitude
:array
- Pixel latitude values (same shape as cx and vy).
longitude
:array
- Pixel longitude values (same shape as cx and vy).
axis
:Artist
- Axis in which you want the latitude/longitude lines drawn.
Expand source code
def latlon_grid(cx, cy, latitude, longitude, axis): """ Places latitude/longitude grid lines and labels on an apoapse swath image. Parameters ---------- cx : array Horizontal coordinate centers in angular space. cy : array Vertical coordinate centers in angular space. latitude : array Pixel latitude values (same shape as cx and vy). longitude : array Pixel longitude values (same shape as cx and vy). axis : Artist Axis in which you want the latitude/longitude lines drawn. """ # set line and label styles grid_style = dict(colors='white', linestyles='-', linewidths=0.5) label_style = dict(fmt=r'$%i\degree$', inline=True, fontsize=8) dlat = 30 dlon = 30 # set longitude to -180 to 180 longitude[np.where(longitude >= 180)] -= 360 # draw latitude contours, place labels, and remove label rotation latc = axis.contour(cx, cy, latitude, levels=np.arange(-90, 90, dlat), **grid_style) latl = axis.clabel(latc, **label_style) [l.set_rotation(0) for l in latl] # longitude contours are complicated... first up setting the hard threshold at -180 to 180 tlon = np.copy(longitude) tlon[np.where((tlon <= -170) | (tlon >= 170))] = np.nan lonc1 = axis.contour(cx, cy, tlon, levels=np.arange(-180, 180, dlon), **grid_style) lonl1 = axis.clabel(lonc1, **label_style) [l.set_rotation(0) for l in lonl1] # then the hard threshold at 360 to 0 using -180 as the label tlon = np.copy(longitude) tlon[np.where(tlon >= 0)] -= 360 tlon[np.where((tlon <= -190) | (tlon >= -170))] = np.nan lonc2 = axis.contour(cx, cy, tlon, levels=[-180], **grid_style) lonl2 = axis.clabel(lonc2, **label_style) [l.set_rotation(0) for l in lonl2]
def latlon_meshgrid(hdul)
-
Returns a latitude/longitude meshgrid suitable for display with matplotlib.pyplot.pcolormesh.
Parameters
hdul
:HDUList
- Opened level 1B FITS file.
Returns
X
:array
- An (n+1,m+1) array of pixel longitudes with "n" = number of slit elements and "m" = number of integrations.
Y
:array
- An (n+1,m+1) array of pixel latitudes with "n" = number of slit elements and "m" = number of integrations.
mask
:array
- A mask for eliminating pixels with incomplete geometry information.
Expand source code
def latlon_meshgrid(hdul): """ Returns a latitude/longitude meshgrid suitable for display with matplotlib.pyplot.pcolormesh. Parameters ---------- hdul : HDUList Opened level 1B FITS file. Returns ------- X : array An (n+1,m+1) array of pixel longitudes with "n" = number of slit elements and "m" = number of integrations. Y : array An (n+1,m+1) array of pixel latitudes with "n" = number of slit elements and "m" = number of integrations. mask : array A mask for eliminating pixels with incomplete geometry information. """ # get the latitude and longitude arrays latitude = hdul['pixelgeometry'].data['pixel_corner_lat'] longitude = hdul['pixelgeometry'].data['pixel_corner_lon'] altitude = hdul['pixelgeometry'].data['pixel_corner_mrh_alt'][:, :, 4] # make meshgrids to hold latitude and longitude grids for pcolormesh display X = np.zeros((latitude.shape[0] + 1, latitude.shape[1] + 1)) Y = np.zeros((longitude.shape[0] + 1, longitude.shape[1] + 1)) mask = np.ones((latitude.shape[0], latitude.shape[1])) # loop through pixel geometry arrays for i in range(int(latitude.shape[0])): for j in range(int(latitude.shape[1])): # there are some pixels where some of the pixel corner longitudes are undefined # if we encounter one of those, set the data value to missing so it isn't displayed # with pcolormesh if np.size(np.where(np.isfinite(longitude[i, j]))) != 5: mask[i, j] = np.nan # also mask out non-disk pixels if altitude[i, j] != 0: mask[i, j] = np.nan # place the longitude and latitude values in the meshgrids X[i, j] = longitude[i, j, 1] X[i + 1, j] = longitude[i, j, 0] X[i, j + 1] = longitude[i, j, 3] X[i + 1, j + 1] = longitude[i, j, 2] Y[i, j] = latitude[i, j, 1] Y[i + 1, j] = latitude[i, j, 0] Y[i, j + 1] = latitude[i, j, 3] Y[i + 1, j + 1] = latitude[i, j, 2] # set any of the NaN values to zero (otherwise pcolormesh will break even if it isn't displaying the pixel). X[np.where(~np.isfinite(X))] = 0 Y[np.where(~np.isfinite(Y))] = 0 # set to domain [-180,180) X[np.where(X > 180)] -= 360 # return the coordinate arrays and the mask return X, Y, mask
def mars_orbit_path(a, e, theta)
-
Generates Mars's orbital path around Sun with angles based on solar longitude (0 degrees points straight right).
Parameters
a
:float
- Semimajor axis in any units.
e
:float
- Orbital eccentricity.
theta
:array
- Angles in radians.
Returns
xr
:array
- Horizontal rectangular coordinates of rotated orbit.
yr
:array
- Vertical rectangular coordinates of rotated orbit.
Expand source code
def mars_orbit_path(a, e, theta): """ Generates Mars's orbital path around Sun with angles based on solar longitude (0 degrees points straight right). Parameters ---------- a : float Semimajor axis in any units. e : float Orbital eccentricity. theta : array Angles in radians. Returns ------- xr : array Horizontal rectangular coordinates of rotated orbit. yr : array Vertical rectangular coordinates of rotated orbit. """ # rotation of periapsis in degrees relative to unrotated ellipse theta_periapsis = 251 # calculate un-rotated orbit path x = a * (1 - e ** 2) / (1 + e * np.cos(theta)) * np.cos(theta) y = a * (1 - e ** 2) / (1 + e * np.cos(theta)) * np.sin(theta) # rotate base orbit path xr = x * np.cos(np.radians(theta_periapsis)) - y * np.sin(np.radians(theta_periapsis)) yr = x * np.sin(np.radians(theta_periapsis)) + y * np.cos(np.radians(theta_periapsis)) return xr, yr
def mars_orbit_path_position(a, e, solar_longitude)
-
Generates orbital path from Ls=0 to given solar longitude position.
Parameters
a
:float
- Semimajor axis in any units.
e
:float
- Orbital eccentricity.
solar_longitude
:float
- Solar longitude in degrees.
Returns
xr
:array
- Horizontal rectangular coordinates of the partial orbit.
yr
:array
- Vertical rectangular coordinates of the partial orbit.
Expand source code
def mars_orbit_path_position(a, e, solar_longitude): """ Generates orbital path from Ls=0 to given solar longitude position. Parameters ---------- a : float Semimajor axis in any units. e : float Orbital eccentricity. solar_longitude : float Solar longitude in degrees. Returns ------- xr : array Horizontal rectangular coordinates of the partial orbit. yr : array Vertical rectangular coordinates of the partial orbit. """ # calculate relative starting and stopping positions in rotated ellipse start = np.radians(90 + 19) stop = np.radians(solar_longitude + 90 + 19) # calculate the number of steps to maintain resolution n = int(1000 * solar_longitude / 360) + 1 # generate array of angles between starting and stopping position theta = np.linspace(start, stop, n) # calculate the rotated orbit path from start to stop x, y = mars_orbit_path(a, e, theta) # return the orbit path return x, y
def plot_solar_longitude(ax, solar_longitude)
-
Plots a Mars orbital path and position of Mars relative to Ls=0 with annotations showing periapsis, apoapsis, 90-degree solar longitude increments, the Sun, and Mars.
Parameters
ax
:Artist
- Axis in which to plot the path and place the annotations.
solar_longitude
:float
- Mars's solar longitude in degrees.
Returns
None.
Expand source code
def plot_solar_longitude(ax, solar_longitude): """ Plots a Mars orbital path and position of Mars relative to Ls=0 with annotations showing periapsis, apoapsis, 90-degree solar longitude increments, the Sun, and Mars. Parameters ---------- ax : Artist Axis in which to plot the path and place the annotations. solar_longitude : float Mars's solar longitude in degrees. Returns ------- None. """ # constants e = 0.0935 # eccentricity a = 1.524 # semi-major axis [AU] theta = np.linspace(0, np.radians(360), 1000) # plot orbital path x, y = mars_orbit_path(a, e, theta) ax.plot(x, y, color='k', linestyle='--', zorder=2) # plot 90-degree spokes x0, y0 = mars_orbit_path_position(a, e, 0) ax.plot([0, x0], [0, y0], color='k', linestyle='--', zorder=2) ax.scatter([x0], [y0], color='k', s=4, zorder=4) ax.text(x0 + 0.1, y0, r'$\mathrm{L_s = 0\degree}$', ha='left', va='center', fontsize=8) for i in [90, 180, 270]: x, y = mars_orbit_path_position(a, e, i) ax.scatter([x[-1]], [y[-1]], color='k', s=4, zorder=5) ax.plot([0, x[-1]], [0, y[-1]], color='k', linestyle='--', zorder=2) # plot semimajor axis xp, yp = mars_orbit_path_position(a, e, 251) xa, ya = mars_orbit_path_position(a, e, 71) ax.plot([xp[-1], xa[-1]], [yp[-1], ya[-1]], color='k', linestyle='--', zorder=2) ax.scatter([xp[-1]], [yp[-1]], color='k', s=4, zorder=5) ax.text(xp[-1] - 0.05, yp[-1] - 0.05, r'Perihelion ($\mathrm{L_s} = 251\degree$)', ha='right', va='top', fontsize=8) ax.scatter([xa[-1]], [ya[-1]], color='k', s=4, zorder=5) ax.text(xa[-1] + 0.05, ya[-1] + 0.05, r'Aphelion ($\mathrm{L_s} = 71\degree$)', ha='left', va='bottom', fontsize=8) # place Sun ax.scatter([0], [0], color=color_dict['yellow'], s=200, edgecolors='none', zorder=4) ax.text(0.25, 0.125, 'Sun', fontsize=8) # plot Mars position x0, y0 = mars_orbit_path_position(a, e, solar_longitude) ax.scatter([x0[-1]], [y0[-1]], color=color_dict['red'], edgecolors='none', s=50, zorder=4) # label Mars xl, yl = mars_orbit_path_position(a * 0.87, e, solar_longitude) ax.text(xl[-1], yl[-1], '$\u2642$', ha='center', va='center', fontsize=8, zorder=3, bbox=dict(facecolor='white', linewidth=0, boxstyle='circle,pad=0.2')) # set plot aspect ax.set_aspect('equal')
def rainbow_colormap(bad=None, n=256)
-
Generates a custom rainbow colormap based on my custom color dictionary.
Parameters
bad
:(3,) tuple
- Normalized color tuple (R,G,B) for missing data (NaN) display. Defaults to None (bad values are masked).
n
:int
- Number of colors to generate. Defaults to 256.
Returns
cmap
:object
- Special rainbow colormap.
Expand source code
def rainbow_colormap(bad=None, n=256): """ Generates a custom rainbow colormap based on my custom color dictionary. Parameters ---------- bad : (3,) tuple Normalized color tuple (R,G,B) for missing data (NaN) display. Defaults to None (bad values are masked). n : int Number of colors to generate. Defaults to 256. Returns ------- cmap : object Special rainbow colormap. """ # color sequence from red -> orange -> yellow -> green -> blue -> violet using my custom color dict rainbow_colors = [(0.839, 0.153, 0.157), (1, 0.498, 0.055), (0.992, 0.722, 0.075), (0.173, 0.627, 0.173), (0, 0.475, 0.757), (0.58, 0.404, 0.741)] # set colormap name cmap_name = 'rainbow' # make a colormap using the color sequence and chosen name cmap = colors.LinearSegmentedColormap.from_list(cmap_name, rainbow_colors, N=n) # set the nan color if bad is not None: try: cmap.set_bad(bad) except: raise Exception('Invalid choice for bad data color. Try a color tuple, e.g., (0,0,0).') # return the colormap return cmap
def reset_symlog_labels(fig, axes)
-
Changes 10^0 to 1 in the axis labels in a symmetric-logarithmic axis.
Parameters
fig
:object
- The figure in which the axis resides.
axes
:object, array-like
- The axes in need of a good reset.
Returns
None.
Expand source code
def reset_symlog_labels(fig, axes): """ Changes 10^0 to 1 in the axis labels in a symmetric-logarithmic axis. Parameters ---------- fig : object The figure in which the axis resides. axes : object, array-like The axes in need of a good reset. Returns ------- None. """ # draw canvas to place the labels fig.canvas.draw() # loop through axes for ax in axes: # get the horizontal axis tick labels labels = ax.get_xticklabels() # loop through the labels for label in labels: # if it's the label for -1, reset it if label.get_text() == r'$\mathdefault{-10^{0}}$': label.set_text(r'$\mathdefault{-1}$') # if it's the label for +1, reset it elif label.get_text() == r'$\mathdefault{10^{0}}$': label.set_text(r'$\mathdefault{1}$') # reset alignment to bottom instead of top ax.set_xticklabels(labels, va='bottom') # set tick padding above the label for tick in ax.get_xaxis().get_major_ticks(): tick.set_pad(11)
def resize_data(data, xdim, ydim)
-
Takes a data array of shape (n,m) and reshapes it to (ydim, xdim) using interpolation.
Parameters
data
:array-like
- The data values.
xdim
:int
- New number of columns.
ydim
:int
- New number of rows.
Returns
new_data
:array-like
- The reshaped data values.
Expand source code
def resize_data(data, xdim, ydim): """ Takes a data array of shape (n,m) and reshapes it to (ydim, xdim) using interpolation. Parameters ---------- data : array-like The data values. xdim : int New number of columns. ydim : int New number of rows. Returns ------- new_data : array-like The reshaped data values. """ # get data dimensions dims = np.shape(data) xdata = dims[1] ydata = dims[0] # determine if anti-aliasing is necessary if (xdata > xdim) | (ydata > ydim): anti_aliasing = True else: anti_aliasing = False # resize the image new_data = resize(data, [ydim, xdim], order=0, mode='edge', anti_aliasing=anti_aliasing) # return the resized data return new_data
def rotated_transform(et)
-
Calculate the rotated pole transform for a particular orbit to replicate the viewing geometry at MAVEN apoapse.
Parameters
et
:obj
- MAVEN apoapsis ephemeris time.
Returns
transform
:???
- A Cartopy rotated pole transform.
Expand source code
def rotated_transform(et): """ Calculate the rotated pole transform for a particular orbit to replicate the viewing geometry at MAVEN apoapse. Parameters ---------- et : obj MAVEN apoapsis ephemeris time. Returns ------- transform : ??? A Cartopy rotated pole transform. """ # calculate various parameters using SPICE target = 'Mars' frame = 'MAVEN_MME_2000' abcorr = 'LT+S' observer = 'MAVEN' state, ltime = spice.spkezr(target, et, frame, abcorr, observer) spoint, trgepc, srfvec = spice.subpnt('Intercept: ellipsoid', target, et, 'IAU_MARS', abcorr, observer) rpoint, colatpoint, lonpoint = spice.recsph(spoint) if lonpoint < 0.: lonpoint += 2 * np.pi G = 6.673e-11 * 6.4273e23 r = 1e3 * state[0:3] v = 1e3 * state[3:6] h = np.cross(r, v) n = h / np.linalg.norm(h) ev = np.cross(v, h) / G - r / np.linalg.norm(r) evn = ev / np.linalg.norm(ev) b = np.cross(evn, n) # get the sub-spacecraft latitude and longitude, and altitude (converted to meters) sublat = 90 - np.degrees(colatpoint) sublon = np.degrees(lonpoint) if sublon > 180: sublon -= 360 alt = np.sqrt(np.sum(srfvec ** 2)) * 1e3 # north pole unit vector in the IAU Mars basis polar_vector = [0, 0, 1] # when hovering over the sub-spacecraft point unrotated (the meridian of the point is a straight vertical line, # this is the exact view when using cartopy's NearsidePerspective or Orthographic with central_longitude and # central latitude set to the sub-spacecraft point), calculate the angle by which the planet must be rotated # about the sub-spacecraft point angle = np.arctan2(np.dot(polar_vector, -b), np.dot(polar_vector, n)) # first, rotate the pole to a different latitude given the subspacecraft latitude # cartopy's RotatedPole uses the location of the dateline (-180) as the lon_0 coordinate of the north pole pole_lat = 90 + sublat pole_lon = -180 # convert pole latitude to colatitude (for spherical coordinates) # also convert to radians for use with numpy trigonometric functions phi = pole_lon * np.pi / 180 theta = (90 - pole_lat) * np.pi / 180 # calculate the Cartesian vector pointing to the pole polar_vector = [np.cos(phi) * np.sin(theta), np.sin(phi) * np.sin(theta), np.cos(theta)] # by rotating the pole, the observer's sub-point in cartopy's un-transformed coordinates is (0,0) # the rotation axis is therefore the x-axis rotation_axis = [1, 0, 0] # rotate the polar vector by the calculated angle rotated_polar_vector = np.dot(rotation_matrix(rotation_axis, -angle), polar_vector) # get the new polar latitude and longitude after the rotation, with longitude offset to dateline rotated_polar_lon = np.arctan(rotated_polar_vector[1] / rotated_polar_vector[0]) * 180 / np.pi - 180 if sublat < 0: rotated_polar_lat = 90 - np.arccos(rotated_polar_vector[2] / np.linalg.norm(rotated_polar_vector)) * 180 / np.pi else: rotated_polar_lat = 90 + np.arccos(rotated_polar_vector[2] / np.linalg.norm(rotated_polar_vector)) * 180 / np.pi # calculate a RotatedPole transform for the rotated pole position transform = ccrs.RotatedPole(pole_latitude=rotated_polar_lat, pole_longitude=rotated_polar_lon, central_rotated_longitude=0) # transform the viewer (0,0) point tcoords = transform.transform_point(0, 0, ccrs.PlateCarree()) # find the angle by which the planet needs to be rotated about it's rotated polar axis and calculate a new # RotatedPole transform including this angle rotation rot_ang = sublon - tcoords[0] transform = ccrs.RotatedPole(pole_latitude=rotated_polar_lat, pole_longitude=rotated_polar_lon, central_rotated_longitude=rot_ang) return transform, alt
def sharpen_image(image)
-
Take an image and sharpen it using a high-pass filter matrix: |-----------| | 0 -1 0 | | -1 5 -1 | | 0 -1 0 | |-----------|
Parameters
image
:array-like
- An (m,n,3) array of RGB tuples (the image).
Returns
sharpened_image
:ndarray
- The original imaged sharpened by convolution with a high-pass filter.
Expand source code
def sharpen_image(image): """ Take an image and sharpen it using a high-pass filter matrix: |-----------| | 0 -1 0 | | -1 5 -1 | | 0 -1 0 | |-----------| Parameters ---------- image : array-like An (m,n,3) array of RGB tuples (the image). Returns ------- sharpened_image : ndarray The original imaged sharpened by convolution with a high-pass filter. """ # the array I'll need to determine the sharpened image will need to be the size of the image + a 1 pixel border sharpening_array = np.zeros((image.shape[0] + 2, image.shape[1] + 2, 3)) # fill the array: the interior is the same as the image, the sides are the same as the first/last row/column, # the corners can be whatever (here they are just 0) (this is only necessary to sharpen the edges of the image) sharpening_array[1:-1, 1:-1, :] = image sharpening_array[0, 1:-1, :] = image[0, :, :] sharpening_array[-1, 1:-1, :] = image[-1, :, :] sharpening_array[1:-1, 0, :] = image[:, 0, :] sharpening_array[1:-1, -1, :] = image[:, -1, :] # make a copy of the image, which will be modified as it gets sharpened sharpened_image = np.copy(image) # multiply each pixel by the sharpening matrix for integration in range(image.shape[0]): for position in range(image.shape[1]): for rgb in range(3): # if the pixel is not a border pixel in sharpening_array, this will execute try: sharpened_image[integration, position, rgb] = \ 5 * sharpening_array[integration + 1, position + 1, rgb] - \ sharpening_array[integration, position + 1, rgb] - \ sharpening_array[integration + 2, position + 1, rgb] - \ sharpening_array[integration + 1, position, rgb] - \ sharpening_array[integration + 1, position + 2, rgb] # if the pixel is a border pixel, no sharpening necessary except IndexError: continue # make sure new pixel rgb values aren't outside the range [0, 1] sharpened_image = np.where(sharpened_image > 1, 1, sharpened_image) sharpened_image = np.where(sharpened_image < 0, 0, sharpened_image) # return the new sharpened image return sharpened_image
def terminator(et)
-
Calculates a terminator image for display over a surface image.
Parameters
et
:float
- Ephemeris time at which to calculate Mars subsolar position.
Returns
longitudes
:array
- Meshgrid of longitudes in degrees.
latitudes
:array
- Meshgrid of latitudes in degrees.
terminator_array
:array
- Masking array which, when multiplied with a cylindrical map, changes the colors to represent twilight and nighttime.
Expand source code
def terminator(et): """ Calculates a terminator image for display over a surface image. Parameters ---------- et : float Ephemeris time at which to calculate Mars subsolar position. Returns ------- longitudes : array Meshgrid of longitudes in degrees. latitudes : array Meshgrid of latitudes in degrees. terminator_array : array Masking array which, when multiplied with a cylindrical map, changes the colors to represent twilight and nighttime. """ # set SPICE inputs target = 'Mars' abcorr = 'LT+S' observer = 'MAVEN' # calculate subsolar position sspoint, strgepc, ssrfvec = spice.subslr('Intercept: ellipsoid', target, et, 'IAU_MARS', abcorr, observer) srpoint, scolatpoint, slonpoint = spice.recsph(sspoint) if slonpoint > np.pi: slonpoint -= 2 * np.pi subsolar_latitude = 90 - np.degrees(scolatpoint) subsolar_longitude = np.degrees(slonpoint) # calculate solar zenith angles longitudes, latitudes, solar_zenith_angles = haversine(subsolar_latitude, subsolar_longitude) # make a mask and set the values terminator_mask = np.zeros_like(solar_zenith_angles) terminator_mask[np.where(solar_zenith_angles > 102)] = 0.4 terminator_mask[np.where(solar_zenith_angles < 90)] = 1 terminator_mask[np.where((solar_zenith_angles >= 90) & (solar_zenith_angles <= 102))] = 0.7 # make the mask 3-dimensional for RGB tuples terminator_array = np.repeat(terminator_mask[:, :, None], 3, axis=2) # return the terminator array with plotting meshgrids return longitudes, latitudes, terminator_array