Exemple #1
0
def spectrum_aperture():
    """
    Extract aperture spectrum at the specified position.
    """
    filename = "./data/Pisco.cube.50kms.image.fits"
    ra, dec = (205.533741, 9.477317341)  # we know where the source is
    scale = 1e3  # map units are Jy/beam, will use to scale fluxes to mJy

    cub = Cube(filename)
    aper = 1.3
    flux, err = cub.aperture_value(ra=ra, dec=dec, radius=aper, calc_error=True)
    # flux, err _ = cub.spectrum(ra=ra, dec=dec, radius=1.3, calc_error=True)  # alternatively

    fig, ax = plt.subplots(figsize=(4.8, 3))
    ax.set_title("Integrated aperture spectrum")
    ax.plot(cub.freqs, flux * scale, color="black", drawstyle='steps-mid', lw=0.75,
            label="Spectrum within r=" + str(aper) + '"')
    ax.fill_between(cub.freqs, flux * scale, 0, color="skyblue", step='mid', lw=0, alpha=0.3)
    ax.plot(cub.freqs, err * scale, color="gray", ls=":", label=r"1$\sigma$ error")  # 1sigma error
    ax.tick_params(direction='in', which="both")
    ax.set_xlabel("Frequency (GHz)")
    ax.set_ylabel("Aperture flux density (mJy)")
    ax.legend(frameon=False)

    plt.savefig("./plots/spectrum_aperture.pdf", bbox_inches="tight")  # save plot
    plt.savefig("./thumbnails/spectrum_aperture.png", bbox_inches="tight", dpi=72)  # web raster version

    plt.show()
Exemple #2
0
def spectrum_single_pixel():
    """
    Extract the spectrum from the central pixel.
    """
    filename = "./data/Pisco.cube.50kms.image.fits"
    scale = 1e3  # map units are Jy/beam, will use to scale fluxes to mJy

    cub = Cube(filename)
    flux = cub.single_pixel_value()  # without coords gv=iven, use central pixel in the map
    # flux, _, _ = cub.spectrum()  # alternatively

    fig, ax = plt.subplots(figsize=(4.8, 3))
    ax.set_title("Single pixel spectrum")
    ax.plot(cub.freqs, flux * scale, color="black", drawstyle='steps-mid', lw=0.75, label="Spectrum")
    ax.fill_between(cub.freqs, flux * scale, 0, color="skyblue", step='mid', lw=0, alpha=0.3)
    ax.plot(cub.freqs, cub.rms * scale, color="gray", ls=":", label="Noise rms")
    ax.tick_params(direction='in', which="both")
    ax.set_xlabel("Frequency (GHz)")
    ax.set_ylabel("Flux density (mJy / beam)")
    ax.legend(frameon=False)

    plt.savefig("./plots/spectrum_single_pixel.pdf", bbox_inches="tight")  # save plot
    plt.savefig("./thumbnails/spectrum_single_pixel.png", bbox_inches="tight", dpi=72)  # web raster version

    plt.show()
Exemple #3
0
def growing_aperture():
    """
    Compute azimuthally averaged profile and the curve of growth (cumulative flux in aperture) as a function of radius.
    """

    filename = "./data/Pisco.cii.455kms.image.fits"
    ra, dec = (205.533741, 9.477317341)  # we know where the source is

    cub = Cube(filename)  # load the map

    scale = 1e3  # map units are Jy/beam, will use to scale fluxes to mJy/beam
    # This map is a single channel collapse over a [CII] emission line, total width of 455 km/s
    # If line fluxes in units of Jy.km/s are preferred, use the lower scaling
    # scale = cub.deltavel()  # channel width in kms, will scale to Jy/beam.km/s (profile) and Jy.km/s (cumulative)

    radius, profile, err = cub.profile_r(ra=ra, dec=dec, maxradius=3, calc_error=True)
    # or alternatively, with same results:
    # radius, flux, err, _ = cub.growing_aperture(ra=ra, dec=dec, maxradius=3, calc_error=True, profile=True)

    print("rms mjy", cub.rms * scale)
    print("err[0]", err[0] * scale)
    fig, ax = plt.subplots(figsize=(4.8, 3))

    col = "navy"
    ax.plot(radius, profile * scale, color=col)
    ax.fill_between(radius, (profile - err) * scale, (profile + err) * scale, color=col, lw=0, alpha=0.2)
    ax.tick_params(direction='in', which="both")
    ax.set_xlabel("Radius (arcsec)")
    ax.set_ylabel("Azimuthal average (mJy/beam)", color=col)
    ax.tick_params(axis='y', colors=col)
    ax.set_xlim(0, 3)

    radius, flux, err = cub.aperture_r(ra=ra, dec=dec, maxradius=3, calc_error=True)
    # radius, flux, err, _ = cub.growing_aperture(ra=ra, dec=dec, maxradius=3, calc_error=True)  # alternatively

    col = "firebrick"
    ax2 = ax.twinx()
    ax2.plot(radius, flux * scale, color=col)
    ax2.fill_between(radius, (flux - err) * scale, (flux + err) * scale, color=col, lw=0, alpha=0.2)
    ax2.tick_params(direction='in', which="both")
    ax2.set_ylabel("Cumulative flux density (mJy)", color=col)
    ax2.tick_params(axis='y', colors=col)

    plt.savefig("./plots/growing_aperture.pdf", bbox_inches="tight")  # save plot
    plt.savefig("./thumbnails/growing_aperture.png", bbox_inches="tight", dpi=72)  # web raster version

    plt.show()
Exemple #4
0
def growing_aperture_psf():
    """
	Compute azimuthally averaged profile and the curve of growth (cumulative flux in aperture) as a function of radius.
	Applied to point spread function (the dirty beam).
	"""

    filename = "./data/Pisco.cii.455kms.psf.fits"
    scale = 1  # no need to scale units, PSF is 1 at its maximum by definition

    cub = Cube(filename)
    radius, profile = cub.profile_r(
        maxradius=5)  # profile, no coords are given, assume central pixel

    # Compute the beam FWHM. It is elliptical so use geometric mean to obtain a single value for the size.
    mean_beam_fwhm = np.sqrt(
        cub.beam["bmaj"][0] *
        cub.beam["bmin"][0])  # [0] is the first (and only) channel
    print("Beam FWHM [arcsec]", mean_beam_fwhm)

    fig, ax = plt.subplots(figsize=(4.8, 3))
    ax.set_title("Point spread function")

    col = "navy"
    ax.plot(radius, profile * scale, color=col)
    ax.tick_params(direction='in', which="both")
    ax.set_xlabel("Radius (arcsec)")
    ax.set_ylabel("Azimuthal average", color=col)
    ax.tick_params(axis='y', colors=col)
    ax.set_xlim(0, 5)

    radius, flux = cub.aperture_r(maxradius=5)  # cumulative

    col = "firebrick"
    ax2 = ax.twinx()
    ax2.plot(radius, flux * scale, color=col)
    ax2.tick_params(direction='in', which="both")
    ax2.set_ylabel("Cumulative", color=col)
    ax2.tick_params(axis='y', colors=col)

    plt.savefig("./plots/growing_aperture_psf.pdf",
                bbox_inches="tight")  # save plot
    plt.savefig("./thumbnails/growing_aperture_psf.png",
                bbox_inches="tight",
                dpi=72)  # web raster version

    plt.show()
Exemple #5
0
def map_channels_wcsaxes():
    """
    Plot a channel maps from the cube using wcsaxes and lin-log scaling. Disclaimer: ccolorbar size is optimized for 3x3
    grid.
    """
    filename = "./data/Pisco.cube.50kms.image.fits"

    cutout = 2  # arcsec
    scale = 1e3  # Jy/beam to mJy/beam

    ra, dec, freq = (205.533741, 9.477317341, 222.547)

    # set up the channel map grid (change the figure size if necessary for font scaling)
    nrows = 3
    ncols = 3
    figsize = (8, 8)
    idx_center = int(0.5 * nrows * ncols)  # index of the central panel

    # if needed, set offset_v to make channel maps blue- or red-wards of the line, but keeping the line as the
    # reference to write in the title of each channel (e.g. \pm Delta_v = XX km/s)
    offset_v = 0

    cub = Cube(filename)  # load map
    cutout_pix = cutout / cub.pixsize
    ch_peak = cub.freq2pix(
        freq * (1 + offset_v / 3e5))  # referent channel in the cube - one with the peak line emission

    # velocity offset of each channel from the referent frequency
    # velocities = cub.vels(freq)  # use the frequency from the spectral fit
    velocities = cub.vels(cub.freqs[ch_peak])  # it's nicer if the ch_peak velocity is set to exactly 0 km/s.

    fig = plt.figure(figsize=figsize)

    # get the extent of the cutouts
    px, py = cub.radec2pix(ra, dec)
    r = int(np.round(cutout * 1.05 / cub.pixsize))  # slightly larger cutout than required, for edge bleeding

    # use the reference channel to set the colorbar scale
    # vmax = np.nanmax(cub.im[px - r:px + r + 1, py - r:py + r + 1, ch_peak]) * scale
    # or use a range of channels to find the max value for colorbar scaling:
    vmax = np.nanmax(cub.im[px - r:px + r + 1, py - r:py + r + 1,
                     ch_peak - idx_center:ch_peak - idx_center + nrows * ncols]) * scale
    linthres = 3 * np.std(cub.im[px - r:px + r + 1, py - r:py + r + 1,
                          ch_peak - idx_center:ch_peak - idx_center + nrows * ncols]) * scale

    ax_list = []

    for i in range(nrows * ncols):
        ax = fig.add_subplot(str(nrows) + str(ncols) + str(i + 1), projection=cub.wcs, label='overlays',
                             slices=('x', 'y', 50))
        ax_list.append(ax)
        ra = ax.coords[0]
        dec = ax.coords[1]

        ch = ch_peak - idx_center + i  # this will put the peak channel on i = idx_center position
        subim = cub.im[:, :, ch] * scale  # scale units

        # show image
        axim = ax.imshow(subim.T, origin='lower', cmap="PuOr_r", vmin=-vmax, vmax=vmax, zorder=-1,
                         norm=colors.SymLogNorm(linthresh=linthres,
                                                linscale=0.5,
                                                vmin=-vmax, vmax=vmax))

        # set limits to exact cutout size
        ax.set_xlim(cub.im.shape[0] / 2 - cutout_pix, cub.im.shape[0] / 2 + cutout_pix)
        ax.set_ylim(cub.im.shape[1] / 2 - cutout_pix, cub.im.shape[1] / 2 + cutout_pix)

        # calc rms and plot contours
        rms = cub.rms[ch] * scale
        ax.contour(subim.T, colors="gray", levels=np.array([-8, -4, -2]) * rms, zorder=1,
                   linewidths=0.5, linestyles="--")
        ax.contour(subim.T, colors="black", levels=np.array([2, 4, 8, 16, 32]) * rms, zorder=1,
                   linewidths=0.5, linestyles="-")

        # add beam, angle is between north celestial pole and major axis, angle increases toward increasing RA
        ellipse = Ellipse(xy=(cub.im.shape[0] / 2 - cutout_pix * 0.75, cub.im.shape[1] / 2 - cutout_pix * 0.75),
                          width=cub.beam["bmin"][ch] / cub.pixsize, height=cub.beam["bmaj"][ch] / cub.pixsize,
                          angle=cub.beam["bpa"][ch], edgecolor='black', fc='w', lw=0.75)
        ax.add_patch(ellipse)

        # add circular aperture to the central panel
        if i == idx_center:
            aper_rad = 1.3
            ellipse = Ellipse(xy=(cub.im.shape[0] / 2, cub.im.shape[1] / 2),
                              width=2 * aper_rad / cub.pixsize, height=2 * aper_rad / cub.pixsize, angle=0,
                              edgecolor='firebrick', fc="none", lw=1, ls=":")
            ax.add_patch(ellipse)

        # add text on top of the map
        paneltext = str(int(velocities[ch] + offset_v)) + " km/s"
        ax.text(0.5, 0.95, paneltext,
                path_effects=[pe.Stroke(linewidth=3, foreground='k'), pe.Normal()],
                va='top', ha='center', color="white", transform=ax.transAxes)

        ax.tick_params(direction='in', which="both")

        dec.set_axislabel(' ')
        ra.set_axislabel(' ')
        ra.set_ticklabel_visible(False)
        dec.set_ticklabel_visible(False)

        # writing RA and DEC and ticklabels only on outer edges of the grid
        if (i) % nrows == 0:
            dec.set_ticklabel_visible(True)
            dec.set_ticks(number=5)
        if i >= (nrows - 1) * ncols:
            ra.set_ticklabel_visible(True)
            ra.set_ticks(number=2)
        if i == nrows * ncols - 2:
            ra.set_axislabel("RA", fontsize=14)
        if i == int(nrows / 2) * ncols:
            dec.set_axislabel("DEC", fontsize=14)

    fig.subplots_adjust(right=0.92)
    cbar_ax = fig.add_axes([0.94, 0.11, 0.03, 0.77])
    fig.colorbar(axim, cax=cbar_ax)
    cbar_ax.yaxis.set_label_text(r"$S_\nu$ (mJy beam$^{-1}$)", fontsize=14)
    cbar_ax.yaxis.set_label_position('right')

    fig.subplots_adjust(hspace=0.1, wspace=0.1)

    plt.savefig("./plots/map_channels_wcsaxes.pdf", bbox_inches="tight", dpi=600)
    plt.savefig("./thumbnails/map_channels_wcsaxes.png", bbox_inches="tight", dpi=72)
    plt.show()

    return None
Exemple #6
0
def map_wcsaxes():
    """
    Plot a map with wcsaxes and semi-logarithmic scaling. Scaling can be controlled to ensure consistency across several
    plots. Prints fluxes of the central source.
    """

    # Two parameters that control the scaling to ensure consistency across plots. If none, uses maximum of map and
    # 5sigma rms noise aas threshold to go from linear to logarithmmic
    vmax = None
    linthres = None

    filename = "./data/Pisco.cii.455kms.image.fits"
    cub = Cube(filename)

    # we know where the source is
    cutout = 4  # radius of the cutout in arcsec (full panel is 2xcutout)
    cutout_pix = cutout / cub.pixsize  # DEC radius of cutout in arcsec
    aper_rad = 2

    scale = 1e3  # Jy/beam to mJy/beam

    fig = plt.figure(figsize=(3, 3))

    # Use the ImageGrid to display a map and a colorbar to the right
    ax = plt.subplot(projection=cub.wcs, label='overlays', slices=('x', 'y', 200))
    lon = ax.coords[0]
    lat = ax.coords[1]

    # for color scaling
    if vmax == None:
        vmax = np.nanmax(cub.im[:, :, 0] * scale)
        linthres = 5 * np.std(cub.im[:, :, 0].T * scale)

    # show image
    axim = ax.imshow(cub.im[:, :, 0].T * scale, origin='lower', cmap="PuOr_r", vmin=-vmax, vmax=vmax, zorder=-1,
                     norm=colors.SymLogNorm(linthresh=linthres,
                                            linscale=0.5,
                                            vmin=-vmax, vmax=vmax))

    # calc rms and plot contours
    rms = cub.rms[0] * scale

    ax.contour(cub.im[:, :, 0].T * scale, colors="gray", levels=np.array([-8, -4, -2]) * rms, zorder=1,
               linewidths=0.5, linestyles="--")
    ax.contour(cub.im[:, :, 0].T * scale, colors="black", levels=np.array([2, 4, 8, 16, 32]) * rms, zorder=1,
               linewidths=0.5, linestyles="-")

    # add beam, angle is between north celestial pole and major axis, angle increases toward increasing RA
    ellipse = Ellipse(xy=(cub.im.shape[0] / 2 - cutout_pix * 0.75, cub.im.shape[0] / 2 - cutout_pix * 0.75),
                      width=cub.beam["bmin"] / cub.pixsize, height=cub.beam["bmaj"] / cub.pixsize,
                      angle=cub.beam["bpa"], edgecolor='black', fc='w', lw=0.75)
    ax.add_patch(ellipse)

    # set limits to exact cutout size
    ax.set_xlim(cub.im.shape[0] / 2 - cutout_pix, cub.im.shape[0] / 2 + cutout_pix)
    ax.set_ylim(cub.im.shape[1] / 2 - cutout_pix, cub.im.shape[1] / 2 + cutout_pix)

    # add circular aperture
    ellipse = Ellipse(xy=(cub.im.shape[1] / 2, cub.im.shape[1] / 2), width=2 * aper_rad / cub.pixsize,
                      height=2 * aper_rad / cub.pixsize, angle=0,
                      edgecolor='maroon', fc="none", lw=1, ls=":")
    ax.add_patch(ellipse)

    # add text on top of the map
    ax.text(0.5, 0.95, r'[CII] 158 $\mu$m',
            path_effects=[pe.Stroke(linewidth=2, foreground='k'), pe.Normal()],
            va='top', ha='center', color="white", transform=ax.transAxes)

    divider = make_axes_locatable(ax)
    cax = divider.append_axes("top", size="5%", axes_class=maxes.Axes)
    cbar = fig.colorbar(axim, cax=cax, orientation='horizontal',
                        ticks=[-int(vmax), -2 * linthres, -linthres / 2, linthres / 2, 2 * linthres, int(vmax)],
                        format='%0.1f')
    cax.xaxis.set_ticks_position('top')
    cax.xaxis.set_label_text(r"$S_\nu$ (mJy beam$^{-1}$)")
    cax.xaxis.set_label_position('top')

    lon.set_ticks(number=4)
    lat.set_minor_frequency(1)
    ax.tick_params(direction='in', which="both")
    ax.set_xlabel(r"RA ")
    ax.set_ylabel(r"Dec")

    plt.savefig("./plots/map_wcsaxes.pdf", bbox_inches="tight", dpi=600)  # need higher dpi for crisp data pixels
    plt.savefig("./thumbnails/map_wcsaxes.png", bbox_inches="tight", dpi=72)  # web raster version

    plt.show()

    return vmax, linthres
Exemple #7
0
def map_channels_paper():
    """
    Plot a channel maps from the cube.
    """
    filename = "./data/Pisco.cube.50kms.image.fits"
    ra, dec, freq = (205.533741, 9.477317341, 222.547)  # we know where the source is
    cutout = 1.5  # arcsec
    scale = 1e3  # Jy/beam to mJy/beam

    # set up the channel map grid (change the figure size if necessary for font scaling)
    nrows = 3
    ncols = 3
    figsize = (6, 6)
    idx_center = int(0.5 * nrows * ncols)  # index of the central panel

    cub = Cube(filename)  # load map

    ch_peak = cub.freq2pix(freq)  # referent channel in the cube - one with the peak line emission

    # velocity offset of each channel from the referent frequency
    velocities = cub.vels(freq)  # use the frequency from the spectral fit
    # velocities = cub.vels(cub.freqs[ch_peak])  # it's nicer if the ch_peak velocity is set to exactly 0 km/s.

    fig = plt.figure(figsize=figsize)
    grid = ImageGrid(fig, 111, nrows_ncols=(nrows, ncols), axes_pad=0.05, share_all=True,
                     cbar_location="right", cbar_mode="single", cbar_size="3%", cbar_pad=0.05)

    # get the extent of the cutouts
    px, py = cub.radec2pix(ra, dec)
    r = int(np.round(cutout * 1.05 / cub.pixsize))  # slightly larger cutout than required, for edge bleeding
    edgera, edgedec = cub.pix2radec([px - r, px + r], [py - r, py + r])  # coordinates of the two opposite corners
    extent = [(edgera - ra) * 3600, (edgedec - dec) * 3600]
    extent = extent[0].tolist() + extent[1].tolist()  # concat two lists

    # use the reference channel to set the colorbar scale
    # vmax = np.nanmax(cub.im[px - r:px + r + 1, py - r:py + r + 1, ch_peak]) * scale
    # or use a range of channels to find the max value for colorbar scaling:
    vmax = np.nanmax(cub.im[px - r:px + r + 1, py - r:py + r + 1,
                     ch_peak - idx_center:ch_peak - idx_center + nrows * ncols]) * scale
    vmin = -0.1 * vmax

    for i in range(nrows * ncols):
        ax = grid[i]
        ch = ch_peak - idx_center + i  # this will put the peak channel on i = idx_center position
        subim = cub.im[px - r:px + r + 1, py - r:py + r + 1, ch] * scale  # scale units

        # show image
        axim = ax.imshow(subim.T, origin='lower', cmap="RdBu_r", vmin=vmin, vmax=vmax, extent=extent)

        # set limits to exact cutout size
        ax.set_xlim(cutout, -cutout)
        ax.set_ylim(-cutout, cutout)

        # calc rms and plot contours
        rms = cub.rms[ch] * scale
        ax.contour(subim.T, extent=extent, colors="gray", levels=np.array([-8, -4, -2]) * rms, zorder=1,
                   linewidths=0.5, linestyles="--")
        ax.contour(subim.T, extent=extent, colors="black", levels=np.array([2, 4, 8, 16, 32]) * rms, zorder=1,
                   linewidths=0.5, linestyles="-")

        # add beam, angle is between north celestial pole and major axis, angle increases toward increasing RA
        ellipse = Ellipse(xy=(cutout * 0.8, -cutout * 0.8),
                          width=cub.beam["bmin"][ch], height=cub.beam["bmaj"][ch], angle=-cub.beam["bpa"][ch],
                          edgecolor='black', fc='w', lw=0.75)
        ax.add_patch(ellipse)

        # add circular aperture to the central panel
        if i == idx_center:
            aper_rad = 1.3
            ellipse = Ellipse(xy=(0, 0), width=2 * aper_rad, height=2 * aper_rad, angle=0,
                              edgecolor='white', fc="none", lw=1, ls=":")
            ax.add_patch(ellipse)

        # add text on top of the map
        # paneltext = str(cub.freqs[ch])
        paneltext = str(int(velocities[ch])) + " km/s"
        ax.text(0.5, 0.95, paneltext,
                path_effects=[pe.Stroke(linewidth=3, foreground='k'), pe.Normal()],
                va='top', ha='center', color="white", transform=ax.transAxes)

        ax.tick_params(direction='in', which="both")

        # Could put global labels to the figure, but in this case just put labels to the middle edge panels
        if i == (ncols * int(nrows / 2)):
            ax.set_ylabel(r"$\Delta$ Dec (arcsec)")
        if i == (nrows * ncols - int(ncols / 2) - 1):
            ax.set_xlabel(r"$\Delta$ RA (arcsec)")

    # add colorbar
    cb = ax.cax.colorbar(axim)
    cb.set_label_text(r"$S_\nu$ (mJy beam$^{-1}$)")

    plt.savefig("./plots/map_channels_paper.pdf", bbox_inches="tight", dpi=600)  # need higher dpi for crisp data pixels
    plt.savefig("./thumbnails/map_channels_paper.png", bbox_inches="tight", dpi=72)  # web raster version

    plt.show()

    return None
Exemple #8
0
def map_single_paper():
    """
    Plot a single 2D map with contours, synthesised beam, colorbar, and text overlay.
    """
    filename = "./data/Pisco.cii.455kms.image.fits"
    ra, dec = (205.533741, 9.477317341)  # we know where the source is
    cutout = 3  # radius of the cutout in arcsec (full panel is 2xcutout)
    aper_rad = 1.3
    titletext = r"[CII] 158 $\mu$m"

    cub = Cube(filename)  # load map

    scale = 1e3  # Jy/beam to mJy/beam
    # scale = cub.deltavel()  # use this to scale units to Jy/beam km/s

    fig = plt.figure(figsize=(3, 3))  # nrows_ncols=(2,4)

    # Use the ImageGrid to display a map and a colorbar to the right
    grid = ImageGrid(fig, 111, nrows_ncols=(1, 1), axes_pad=0.05, share_all=True,
                     cbar_location="right", cbar_mode="single", cbar_size="3%", cbar_pad=0.05)
    ax = grid[0]

    # create a smaller cutout for plotting, a subimage
    # Note: there should be a better way to plot cutouts, but this works.
    px, py = cub.radec2pix(ra, dec)
    r = int(np.round(cutout * 1.05 / cub.pixsize))  # slightly larger cutout than required for edge bleeding
    edgera, edgedec = cub.pix2radec([px - r, px + r], [py - r, py + r])  # coordinates of the two opposite corners
    extent = [(edgera - ra) * 3600, (edgedec - dec) * 3600]
    extent = extent[0].tolist() + extent[1].tolist()  # concat two lists

    # get the cutout; warning: no index out of bounds checking is done here
    subim = cub.im[px - r:px + r + 1, py - r:py + r + 1, 0] * scale  # scale units

    # for color scaling
    vmax = np.nanmax(subim)
    vmin = -0.1 * vmax

    # show image
    axim = ax.imshow(subim.T, origin='lower', cmap="RdBu_r", vmin=vmin, vmax=vmax, extent=extent)

    # calc rms and plot contours
    rms = cub.rms[0] * scale
    ax.contour(subim.T, extent=extent, colors="gray", levels=np.array([-8, -4, -2]) * rms, zorder=1, linewidths=0.5,
               linestyles="--")
    ax.contour(subim.T, extent=extent, colors="black", levels=np.array([2, 4, 8, 16, 32]) * rms, zorder=1,
               linewidths=0.5,
               linestyles="-")

    # add beam, angle is between north celestial pole and major axis, angle increases toward increasing RA
    ellipse = Ellipse(xy=(cutout * 0.8, -cutout * 0.8),
                      width=cub.beam["bmin"], height=cub.beam["bmaj"], angle=-cub.beam["bpa"],
                      edgecolor='black', fc='w', lw=0.75)
    ax.add_patch(ellipse)

    # set limits to exact cutout size
    ax.set_xlim(cutout, -cutout)
    ax.set_ylim(-cutout, cutout)

    # add circular aperture
    ellipse = Ellipse(xy=(0, 0), width=2 * aper_rad, height=2 * aper_rad, angle=0,
                      edgecolor='white', fc="none", lw=1, ls=":")
    ax.add_patch(ellipse)

    # add text on top of the map
    ax.text(0.5, 0.95, titletext,
            path_effects=[pe.Stroke(linewidth=2, foreground='k'), pe.Normal()],
            va='top', ha='center', color="white", transform=ax.transAxes)

    # add colorbar
    cb = ax.cax.colorbar(axim)
    cb.set_label_text(r"$S_\nu$ (mJy beam$^{-1}$)")

    # ax.tick_params(direction='in', which="both")
    ax.set_xlabel(r"$\Delta$ RA (arcsec)")
    ax.set_ylabel(r"$\Delta$ Dec (arcsec)")

    plt.savefig("./plots/map_single_paper.pdf", bbox_inches="tight", dpi=600)  # need higher dpi for crisp data pixels
    plt.savefig("./thumbnails/map_single_paper.png", bbox_inches="tight", dpi=72)  # web raster version

    plt.show()