import logging
from astropy import units as u
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse
from matplotlib.colors import LogNorm
from astropy.visualization.wcsaxes.frame import RectangularFrame
from spectral_cube import SpectralCube
[docs]def find_nannearest_idx(array,value):
idx = np.nanargmin(np.abs(array-value))
return [idx]
[docs]class EmissionCubeMixin(object):
[docs] def ad(self, bd):
"""
Calculates corresponding ellipse semi-major axis value given a semi-minor axis b
"""
if not isinstance(bd, u.Quantity):
bd = u.Quantity(bd, unit = u.kpc)
logging.warning("No units specified for semi-minor axis, assuming"
"{}".format(bd.unit))
return bd * self.el_constant1 + self.el_constant2 * bd * bd / self.bd_max
[docs] def xy_ellipse_patch(self):
"""
Returns a '~matplotlib.patches.Ellipse' artist object of the Elliptical Disk in the xy plane
Warning: Currently does not account for any alpha or beta tilt values...
"""
return Ellipse(xy = [0.,0.], width = self.bd_max.value * 2., height = self.ad(self.bd_max).value * 2., angle = -self.theta.value)
[docs] def lv_plot(self, latitude, swap_axes = False, fig = None, frame_class = RectangularFrame,
orientation = 'vertical', vmin = .5, vmax = 500., norm = LogNorm(), cmap = 'YlGnBu_r',
invert_xaxis = False, invert_yaxis = False, spectral_unit = u.km/u.s, over_contour = False,
levels = 5, cmap_contour = 'Reds', subpad = 0.8,
contour_options = {}, **kwargs):
"""
Plot a Longitude-Velocity slice of the data at the specified latitude
Parameters
----------
latitude: 'Quantity or int'
Latitude to extract slice from
if 'int' then index value to slice at
swap_axes: 'bool', optional, must be keyword
if True, swaps x and y axes
Default is Longitude on y-axis and Velocity on x-axis
fig: '~matplotlib.pyplot.figure', optional, must be keyword
if provided, axis will be added to this figure instance
frame_class: 'astropy.visualization.wcsaxes.frame', optional, must be keyword
if provided, specifies astropy frame to use for Plot, such as EllipticalFrame
aspect: 'str or int', optional, must be keyword
aspect keyword used for 'matplotlib.pyplot.imshow'
orientation: 'str', optional, must be keyword
orientation of colorbar, keyword passed to 'matplotlib.pyplot.colorbar'
vmin: 'number', optional, must be keyword
min intensity to plot, keyword passed to 'matplotlib.pyplot.imshow'
vmax: 'number', optional, must be keyword
max intensity to plot, keyword passed to 'matplotlib.pyplot.imshow'
norm: 'matplotlib.colors.', optional, must be keyword
norm to pass into 'matplotlib.plt.imshow' for color scaling
cmap: 'str', optional, must be keyword
cmap keyword to pass into 'matplotlib.plg.imshow'
invert_xaxis: 'bool', optional, must be keyword
if True, will invert xaxis
invert_yaxis: 'bool', optional, must be keyword
if True, will invert yaxis
spectral_unit: 'astropy.units', optional, must be keyword
if provided, convert spectral axis to these units
over_contour: 'EmissionCube', or 'SpectralCube', or bool, optional, must be keyword
if provided, will overplot contours of this cube on image
if True, will overplot contours from self
levels: 'list', optional, must be keyword
contour levels to plot or number of contours to plot
passed to 'matplotlib.plt.contour'
cmap: 'str', optional, must be keyword
cmap keyword to pass into 'matplotlib.plt.contour'
contour_options: 'dict', optional, must be keyword
additional keywords to pass into 'matplotlib.plt.contour'
subpad: 'number', optional, must be keyword
passed to fig.subplot_adjust
useful for placing colorbar better
"""
# Initiate figure instance if needed
if not fig:
fig = plt.figure()
# Find Latitude slice index
if isinstance(latitude, u.Quantity):
vel_unit, lat_axis_values, lon_unit = self.world[int(self.shape[0]/2), :, int(self.shape[2]/2)]
lat_slice = find_nannearest_idx(lat_axis_values, latitude)[0]
else:
if latitude in range(self.shape[1]):
lat_slice = latitude
_,_,lon_unit = self.world[int(self.shape[1]/2), :, int(self.shape[2]/2)]
else:
raise TypeError()
print("lat_slice must either be a Quantity value or an index. Current index is out of range")
if not swap_axes:
data = self.hdu.data[:,lat_slice,:].transpose()
slices = ('y', lat_slice, 'x')
else:
data = self.hdu.data[:,lat_slice,:]
slices = ('x', lat_slice, 'y')
# Create wcs axis object
ax = fig.add_subplot(111, projection = self.wcs, slices = slices, frame_class = frame_class)
# Plot image
im = ax.imshow(data, cmap = cmap, norm = norm,
vmin = vmin, vmax = vmax, **kwargs)
if invert_xaxis:
ax.invert_xaxis()
if invert_yaxis:
ax.invert_yaxis()
xlim = ax.get_xlim()
ylim = ax.get_ylim()
if slices[0] == 'x':
lim_world = self.wcs.wcs_pix2world(xlim, (0,0), ylim, 0)
elif slices[0] == 'y':
lim_world = self.wcs.wcs_pix2world(ylim, (0,0), xlim, 0)
if orientation == 'vertical':
fig.subplots_adjust(right=subpad)
elif orientation == 'horizontal':
fig.subplots_adjust(top=subpad)
# ax.set_aspect(aspect)
if isinstance(over_contour, bool):
if over_contour:
lat_slice_over = lat_slice
ct = ax.contour(data, cmap = cmap_contour, levels = levels, **contour_options)
else:
cube = over_contour
if isinstance(latitude, u.Quantity):
vel_unit_over, lat_axis_values_over, lon_unit_over = cube.world[int(cube.shape[0]/2), :, int(cube.shape[2]/2)]
lat_slice_over = find_nannearest_idx(lat_axis_values_over, latitude)[0]
else:
if latitude in range(self.shape[1]):
lat_slice_over = latitude
else:
raise TypeError()
print("lat_slice must either be a Quantity value or an index. Current index is out of range")
if not swap_axes:
data_over = cube.hdu.data[:,lat_slice_over,:].transpose()
slices_over = ('y', lat_slice_over, 'x')
else:
data_over = cube.hdu.data[:,lat_slice_over,:]
slices_over = ('x', lat_slice_over, 'y')
#Create New Axes and make transparent
newax = fig.add_axes(ax.get_position(),
projection = cube.wcs,
slices = slices_over,
frame_class = frame_class)
newax.patch.set_visible(False)
newax.yaxis.set_visible(False)
newax.xaxis.set_visible(False)
ct = newax.contour(data_over, cmap = cmap_contour, levels = levels, **contour_options)
lim_pix_over = cube.wcs.wcs_world2pix(lim_world[0], (0,0), lim_world[2], 0)
newax.tick_params(labelbottom=False, axis = 'x')
if slices_over[0] == 'x':
newax.set_xlim(lim_pix_over[0])
newax.set_ylim(lim_pix_over[2])
elif slices_over[0] == 'y':
newax.set_xlim(lim_pix_over[2])
newax.set_ylim(lim_pix_over[0])
newax.get_xaxis().set_visible(False)
newax.get_yaxis().set_visible(False)
newax.coords[2].set_format_unit(spectral_unit)
if orientation == 'vertical':
fig.subplots_adjust(right=subpad)
elif orientation == 'horizontal':
fig.subplots_adjust(top=subpad)
# newax.set_aspect(aspect)
ax.coords[2].set_format_unit(spectral_unit)
# plt.tight_layout(pad = 7)
# Add axis labels
lon_label = 'Galactic Longitude ' + '({})'.format(lon_unit.unit)
vel_label = 'LSR Velocity ' + '({})'.format(spectral_unit)
if swap_axes:
ax.set_ylabel(vel_label, fontsize = 16)
ax.set_xlabel(lon_label, fontsize = 16)
else:
ax.set_ylabel(lon_label, fontsize = 16)
ax.set_xlabel(vel_label, fontsize = 16)
# Add colorbar
if orientation == 'vertical':
cbar_ax = fig.add_axes([0.85, 0.12, 0.04, 0.75])
cbar = fig.colorbar(im, orientation = orientation, cax = cbar_ax)
cbar.set_label('{}'.format(self.unit), size = 16)
elif orientation == 'horizontal':
# fig.subplots_adjust(top=0.8)
cbar_ax = fig.add_axes([0.12, 0.85, 0.75, 0.04])
cbar = fig.colorbar(im, orientation = orientation, cax = cbar_ax)
cbar.set_label('{}'.format(self.unit), size = 16)
cbar_ax.xaxis.set_ticks_position('top')
cbar_ax.xaxis.set_label_position('top')
# plt.tight_layout(pad = 7)
return fig
[docs] def lv_contour(self, latitude, swap_axes = False, fig = None, frame_class = RectangularFrame, aspect = 'auto',
cmap = 'Reds', levels = (0.1, 0.4, 1.8, 3.8, 7., 11., 16., 24.),
invert_xaxis = False, invert_yaxis = False, spectral_unit = u.km/u.s, **kwargs):
"""
Plot contours of a Longitude-Velocity slice of the data at the specified latitude
Parameters
----------
latitude: 'Quantity or int'
Latitude to extract slice from
if 'int' then index value to slice at
swap_axes: 'bool', optional, must be keyword
if True, swaps x and y axs
Default is Longitude on y-axis and Velocity on x-axis
fig: '~matplotlib.pyplot.figure', optional, must be keyword
if provided, axis will be added to this figure instance
frame_class: 'astropy.visualization.wcsaxes.frame', optional, must be keyword
if provided, specifies astropy frame to use for Plot, such as EllipticalFrame
aspect: 'str', optional, must be keyword
aspect keyword to pass into 'matplotlib.plt.contour'
orientation: 'str', optional, must be keyword
keyword to pass into 'matplotlib.plt.colorbar'
vmin: 'number', optional, must be keyword
min value to show - keyword to pass into 'matplotlib.plt.imshow'
vmax: 'number', optional, must be keyword
max value to show - keyword to pass into 'matplotlib.plt.imshow'
cmap keyword to pass into 'matplotlib.plt.contour'
levels: 'list', optional, must be keyword
contour levels to plot or number of contours to plot
passed to 'matplotlib.plt.contour'
invert_xaxis: 'bool', optional, must be keyword
if True, will invert xaxis
invert_yaxis: 'bool', optional, must be keyword
if True, will invert yaxis
spectral_unit: 'astropy.units', optional, must be keyword
if provided, convert spectral axis to these units
"""
# Initiate figure instance if needed
if not fig:
fig = plt.figure(figsize = (18,12))
# Find Latitude slice index
if isinstance(latitude, u.Quantity):
vel_unit, lat_axis_values, lon_unit = self.world[int(self.shape[1]/2), :, int(self.shape[2]/2)]
lat_slice = find_nannearest_idx(lat_axis_values, latitude)[0]
else:
lat_slice = latitude
if not swap_axes:
data = self.hdu.data[:,lat_slice,:].transpose()
slices = ('y', lat_slice, 'x')
else:
data = self.hdu.data[:,lat_slice,:]
slices = ('x', lat_slice, 'y')
# Create wcs axis object
ax = fig.add_subplot(111, projection = self.wcs, slices = slices, frame_class = frame_class, aspect = aspect)
# Plot contours
ax.contour(data, cmap = cmap, levels = levels, **kwargs)
if invert_xaxis:
ax.invert_xaxis()
if invert_yaxis:
ax.invert_yaxis()
ax.coords[2].set_format_unit(spectral_unit)
# Add axis labels
lon_label = 'Galactic Longitude ' + '({})'.format(lon_unit.unit)
vel_label = 'LSR Velocity ' + '({})'.format(spectral_unit)
if swap_axes:
ax.set_ylabel(vel_label, fontsize = 16)
ax.set_xlabel(lon_label, fontsize = 16)
else:
ax.set_ylabel(lon_label, fontsize = 16)
ax.set_xlabel(vel_label, fontsize = 16)
return fig