def convert_jytok(infile=None, outfile=None, overwrite=False, inplace=False):
    """
    Convert a cube from Jy/beam to K.
    """

    if infile is None or (outfile is None and inplace == False):
        logger.error("Missing required input.")
        return (False)

    if os.path.isdir(infile) == False:
        logger.error("Input file not found: " + infile)
        return (False)

    if inplace == False:
        if os.path.isdir(outfile) or os.path.isfile(outfile):
            if overwrite:
                os.system('rm -rf ' + outfile)
            else:
                logger.error("Output file already present: " + outfile)
                return (False)

        os.system('cp -r ' + infile + ' ' + outfile)
        target_file = outfile
    else:
        target_file = infile

    hdr = casaStuff.imhead(target_file, mode='list')
    unit = hdr['bunit']
    if unit != 'Jy/beam':
        logger.error("Input unit is not Jy/beam for file " + target_file +
                     " . Instead found " + unit)
        return (False)

    jytok = calc_jytok(hdr=hdr)

    multiply_cube_by_value(target_file,
                           jytok,
                           brightness_unit='K',
                           allow_huge=True)

    casaStuff.imhead(target_file, mode='put', hdkey='JYTOK', hdvalue=jytok)

    return (True)
def calc_jytok(hdr=None, infile=None):
    """
    Calculate the Jy/beam -> Kelvin conversion. Accepts a header
    already read using imhead or a file name that it will parse.
    """

    c = 2.99792458e10
    h = 6.6260755e-27
    kb = 1.380658e-16

    if hdr is None:
        if infile is None:
            logger.error("No header and no infile. Returning.")
            return (None)
        hdr = casaStuff.imhead(target_file, mode='list')

    if hdr['cunit3'] != 'Hz':
        logger.error(
            "I expected frequency as the third axis but did not find it. Returning."
        )
        return (None)

    crpix3 = hdr['crpix3']
    cdelt3 = hdr['cdelt3']
    crval3 = hdr['crval3']
    naxis3 = hdr['shape'][2]
    faxis_hz = (np.arange(naxis3) + 1. - crpix3) * cdelt3 + crval3
    freq_hz = np.mean(faxis_hz)

    bmaj_unit = hdr['beammajor']['unit']
    if bmaj_unit != 'arcsec':
        logger.error(
            "Beam unit is not arcsec, which I expected. Returning. Unit instead is "
            + bmaj_unit)
        return (None)
    bmaj_as = hdr['beammajor']['value']
    bmin_as = hdr['beamminor']['value']
    bmaj_sr = bmaj_as / 3600. * np.pi / 180.
    bmin_sr = bmin_as / 3600. * np.pi / 180.
    beam_in_sr = np.pi * (bmaj_sr / 2.0 * bmin_sr / 2.0) / np.log(2)

    jytok = c**2 / beam_in_sr / 1e23 / (2 * kb * freq_hz**2)

    return (jytok)
def write_mask(infile, outfile, mask, allow_huge=True):
    """
    Write a CASA mask out as a CASA image. Includes a switch for large cubes, where putchunk may fail.
    """

    os.system('rm -rf ' + outfile)
    os.system('cp -r ' + infile + ' ' + outfile)

    if allow_huge:
        casaStuff.exportfits(imagename=outfile,
                             fitsimage=outfile + '.fits',
                             stokeslast=False, overwrite=True)
        hdu = pyfits.open(outfile + '.fits')[0]
        hdu.data = mask.T
        hdu.header['BITPIX'] = -32

        # Match up the WCS so tclean doesn't throw an error (this is some rounding to the nth decimal place...)
        header = casaStuff.imhead(infile, mode='list')
        wcs_names = ['cdelt1', 'cdelt2', 'cdelt3', 'cdelt4',
                     'crval1', 'crval2', 'crval3', 'crval4']

        for wcs_name in wcs_names:
            hdu.header[wcs_name.upper()] = header[wcs_name]

        hdu.writeto(outfile + '.fits', clobber=True)
        casaStuff.importfits(fitsimage=outfile + '.fits',
                             imagename=outfile,
                             overwrite=True)

        # Remove the intermediate fits file
        os.system('rm -rf ' + outfile + '.fits')
    else:
        myia.open(outfile)
        myia.putchunk(mask)
        myia.close()

    return True
def common_res_for_mosaic(infile_list=None,
                          outfile_list=None,
                          target_res=None,
                          pixel_padding=2.0,
                          do_convolve=True,
                          overwrite=False):
    """
    Convolve multi-part cubes to a common res for mosaicking. 

    infile_list : list of input files.

    outfile_list : if do_convolve is true, a list of output files that
    will get the convolved data. Can be a dictionary or a list. If
    it's a list then matching is by order, so that the first infile
    goes to first outfile, etc. If it's a dictionary, it looks for the
    infile name as a key.

    target_res : force this target resolution.

    pixel_padding (default 2.0) : the number of pixels to add to the
    largest common beam (in quadrature) to ensure robust convolution.

    do_convolve (default True) : do the convolution. Otherwise just
    calculates and returns the target resolution.

    overwrite (default False) : Delete existing files. You probably
    want to set this to True but it's a user decision.

    Unless a target resolution is supplied, the routine first
    calculates the common resolution based on the beam size of all of
    the input files. This target resolution is returned as the output
    of the routine. The supplied pixel_padding is used to ensure that
    a convolution kernel can be built by imregrid, since CASA can't
    currently keep the major axis fixed and convolve the minor axis.

    If do_convolve is True, it also convolves all of the input files
    to output files with that resolution. For this it needs a list of
    output files matched to the input file list, either as another
    list or a dictionary.
    """

    # Check inputs.

    # First check that input files are supplied and exist.

    if infile_list is None:
        logger.error("Missing required infile_list.")
        return (None)

    for this_file in infile_list:
        if os.path.isdir(this_file) == False:
            logger.error("File not found " + this_file)
            return (None)

    # If do_convolve is True then make sure that we have output files
    # and that they match the input files.

    if do_convolve:

        if outfile_list is None:
            logger.error("Missing outfile_list required for convolution.")
            return (None)

        if (type(outfile_list) != type([])) and (type(outfile_list) != type(
            {})):
            logger.error("outfile_list must be dictionary or list.")
            return (None)

        if type(outfile_list) == type([]):
            if len(infile_list) != len(outfile_list):
                logger.error("Mismatch in input and output list lengths.")
                return (None)
            outfile_dict = {}
            for ii in range(len(infile_list)):
                outfile_dict[infile_list[ii]] = outfile_list[ii]

        if type(outfile_list) == type({}):
            outfile_dict = outfile_list

        missing_keys = 0
        for infile in infile_list:
            if infile not in outfile_dict.keys():
                logger.error("Missing output file for infile: " + infile)
                missing_keys += 1
            if missing_keys > 0:
                logger.error("Missing " + str(missing_keys) +
                             " output file names.")
                return (None)

    # Figure out the target resolution if it is not supplied by the user

    if target_res is None:
        logger.debug("Calculating target resolution ... ")

        bmaj_list = []
        pix_list = []

        for this_infile in infile_list:
            logger.info("Checking " + this_infile)

            hdr = casaStuff.imhead(this_infile)

            if (hdr['axisunits'][0] != 'rad'):
                logger.error(
                    "ERROR: Based on CASA experience. I expected units of radians."
                )
                logger.error(
                    "I did not find this. Returning. Adjust code or investigate file "
                    + this_infile)
                return (None)
            this_pixel = abs(hdr['incr'][0] / np.pi * 180.0 * 3600.)

            if (hdr['restoringbeam']['major']['unit'] != 'arcsec'):
                logger.error(
                    "ERROR: Based on CASA experience. I expected units of arcseconds for the beam."
                )
                logger.error(
                    "I did not find this. Returning. Adjust code or investigate file "
                    + this_infile)
                return (None)
            this_bmaj = hdr['restoringbeam']['major']['value']

            bmaj_list.append(this_bmaj)
            pix_list.append(this_pixel)

        max_bmaj = np.max(bmaj_list)
        max_pix = np.max(pix_list)
        target_bmaj = np.sqrt((max_bmaj)**2 + (pixel_padding * max_pix)**2)
    else:
        target_bmaj = force_beam

    logger.info('I found a common beam size of ' + str(target_bmaj))

    if not do_convolve:
        return (target_bmaj)

    # With a target resolution and matched lists we can proceed.

    for this_infile in infile_list:
        this_outfile = outfile_dict[this_infile]
        logger.debug("Convolving " + this_infile + ' to ' + this_outfile)

        casaStuff.imsmooth(imagename=this_infile,
                           outfile=this_outfile,
                           targetres=True,
                           major=str(target_bmaj) + 'arcsec',
                           minor=str(target_bmaj) + 'arcsec',
                           pa='0.0deg',
                           overwrite=overwrite)

    return (target_bmaj)
def calculate_mosaic_extent(
    infile_list=None,
    force_ra_ctr=None,
    force_dec_ctr=None,
    force_freq_ctr=None,
):
    """
    Given a list of input files, calculate the center and extent of
    the mosaic needed to cover them all. Return the results as a
    dictionary.

    infile_list : list of input files to loop over.

    force_ra_ctr (default None) : if set then force the RA center of
    the mosaic to be this value, and the returned extent is the
    largest separation of any image corner from this value in RA.

    force_dec_ctr (default None) : as force_ra_ctr but for
    Declination.

    If the RA and Dec. centers are supplied, then they are assumed to
    be in decimal degrees.
    """

    # Check inputs

    if infile_list is None:
        logger.error("Missing required infile_list.")
        return (None)

    for this_infile in infile_list:
        if not os.path.isdir(this_infile):
            logger.error("File not found " + this_infile + "Returning.")
            return (None)

    # Initialize the list of corner RA and Dec positions.

    ra_list = []
    dec_list = []
    # TBD - right now we assume matched frequency/velocity axis
    freq_list = []

    # Loop over input files and calculate RA and Dec coordinates of
    # the corners.

    myia = au.createCasaTool(casaStuff.iatool)

    for this_infile in infile_list:

        this_hdr = casaStuff.imhead(this_infile)

        if this_hdr['axisnames'][0] != 'Right Ascension':
            logger.error("Expected axis 0 to be Right Ascension. Returning.")
            return (None)
        if this_hdr['axisunits'][0] != 'rad':
            logger.error("Expected axis units to be radians. Returning.")
            return (None)
        if this_hdr['axisnames'][1] != 'Declination':
            logger.error("Expected axis 1 to be Declination. Returning.")
            return (None)
        if this_hdr['axisunits'][1] != 'rad':
            logger.error("Expected axis units to be radians. Returning.")
            return (None)

        this_shape = this_hdr['shape']
        xlo = 0
        xhi = this_shape[0] - 1
        ylo = 0
        yhi = this_shape[1] - 1

        pixbox = str(xlo) + ',' + str(ylo) + ',' + str(xlo) + ',' + str(ylo)
        blc = casaStuff.imval(this_infile, stokes='I', box=pixbox)

        pixbox = str(xlo) + ',' + str(yhi) + ',' + str(xlo) + ',' + str(yhi)
        tlc = casaStuff.imval(this_infile, stokes='I', box=pixbox)

        pixbox = str(xhi) + ',' + str(yhi) + ',' + str(xhi) + ',' + str(yhi)
        trc = casaStuff.imval(this_infile, stokes='I', box=pixbox)

        pixbox = str(xhi) + ',' + str(ylo) + ',' + str(xhi) + ',' + str(ylo)
        brc = casaStuff.imval(this_infile, stokes='I', box=pixbox)

        ra_list.append(blc['coords'][:, 0])
        ra_list.append(tlc['coords'][:, 0])
        ra_list.append(trc['coords'][:, 0])
        ra_list.append(brc['coords'][:, 0])

        dec_list.append(blc['coords'][:, 1])
        dec_list.append(tlc['coords'][:, 1])
        dec_list.append(trc['coords'][:, 1])
        dec_list.append(brc['coords'][:, 1])

        freq_list.append(blc['coords'][:, 2])
        freq_list.append(tlc['coords'][:, 2])
        freq_list.append(trc['coords'][:, 2])
        freq_list.append(brc['coords'][:, 2])

    # Get the minimum and maximum RA and Declination.

    # TBD - this breaks straddling the meridian (RA = 0) or the poles
    # (Dec = 90). Add catch cases or at least error calls for
    # this. Meridian seems more likely to come up, so just that is
    # probably fine.

    min_ra = np.min(np.concatenate(ra_list))
    max_ra = np.max(np.concatenate(ra_list))
    min_dec = np.min(np.concatenate(dec_list))
    max_dec = np.max(np.concatenate(dec_list))

    # TBD - right now we assume matched frequency/velocity axis

    min_freq = np.min(np.concatenate(freq_list))
    max_freq = np.max(np.concatenate(freq_list))

    # If we do not force the center of the mosaic, then take it to be
    # the average of the min and max, so that the image will be a
    # square.

    if force_ra_ctr == None:
        ra_ctr = (max_ra + min_ra) * 0.5
    else:
        ra_ctr = force_ra_ctr * np.pi / 180.

    if force_dec_ctr == None:
        dec_ctr = (max_dec + min_dec) * 0.5
    else:
        dec_ctr = force_dec_ctr * np.pi / 180.

    if force_freq_ctr == None:
        freq_ctr = (max_freq + min_freq) * 0.5
    else:
        freq_ctr = force_freq_ctr

    # Now calculate the total extent of the mosaic given the center.

    delta_ra = 2.0 * np.max([np.abs(max_ra - ra_ctr), np.abs(min_ra - ra_ctr)])
    delta_ra *= np.cos(dec_ctr)
    delta_dec = 2.0 * np.max(
        [np.abs(max_dec - dec_ctr),
         np.abs(min_dec - dec_ctr)])
    delta_freq = 2.0 * np.max(
        [np.abs(max_freq - freq_ctr),
         np.abs(min_freq - freq_ctr)])

    # Put the output into a dictionary.

    output = {
        'ra_ctr': [ra_ctr * 180. / np.pi, 'degrees'],
        'dec_ctr': [dec_ctr * 180. / np.pi, 'degrees'],
        'delta_ra': [delta_ra * 180. / np.pi * 3600., 'arcsec'],
        'delta_dec': [delta_dec * 180. / np.pi * 3600., 'arcsec'],
        'freq_ctr': [freq_ctr, 'Hz'],
        'delta_freq': [delta_freq, 'Hz'],
    }
    return (output)
def import_and_align_mask(
        in_file=None,
        out_file=None,
        template=None,
        blank_to_match=False,
):
    """
    Align a mask to a target astrometry. This includes some klugy
    steps (especially related to axes and interpolation) to make this
    work, e.g., for clean masks, most of the time.
    """

    # Import from FITS (could make optional)
    os.system('rm -rf ' + out_file + '.temp_copy' + ' 2>/dev/null')
    logger.debug('Importing mask file: "' + in_file + '"')
    casaStuff.importfits(fitsimage=in_file,
                         imagename=out_file + '.temp_copy',
                         overwrite=True)

    # Prepare analysis utility tool
    myia = au.createCasaTool(casaStuff.iatool)
    myim = au.createCasaTool(casaStuff.imtool)

    # Read mask data
    # myia.open(out_file+'.temp_copy')
    # mask = myia.getchunk(dropdeg=True)
    # myia.close()
    # print('**********************')
    # print('type(mask)', type(mask), 'mask.dtype', mask.dtype, 'mask.shape', mask.shape) # note that here the mask is in F dimension order, not Pythonic.
    # print(np.max(mask), np.min(mask))
    # print('**********************')

    # Read template image header
    hdr = casaStuff.imhead(template)
    # print('hdr', hdr)

    maskhdr = casaStuff.imhead(out_file + '.temp_copy')
    # print('maskhdr', maskhdr)

    # Check if 2D or 3D
    logger.debug('Template data axis names: ' + str(hdr['axisnames']) + ', shape: ' + str(hdr['shape']))
    logger.debug('Mask data axis names: ' + str(maskhdr['axisnames']) + ', shape: ' + str(maskhdr['shape']))
    is_template_2D = (np.prod(list(hdr['shape'])) == np.prod(list(hdr['shape'])[0:2]))
    is_mask_2D = (np.prod(list(maskhdr['shape'])) == np.prod(list(maskhdr['shape'])[0:2]))
    if is_template_2D and not is_mask_2D:
        logger.debug('Template image is 2D but mask is 3D, collapsing the mask over channel axes: ' + str(
            np.arange(maskhdr['ndim'] - 1, 2 - 1, -1)))
        # read mask array
        myia.open(out_file + '.temp_copy')
        mask = myia.getchunk(dropdeg=True)
        myia.close()
        # print('**********************')
        # print('type(mask)', type(mask), 'mask.dtype', mask.dtype, 'mask.shape', mask.shape) # Note that here array shapes are in F dimension order, i.e., axis 0 is RA, axis 1 is Dec, axis 2 is Frequency, etc.
        # print('**********************')
        # 
        # collapse channel and higher axes
        # mask = np.any(mask.astype(int).astype(bool), axis=np.arange(maskhdr['ndim']-1, 2-1, -1)) # Note that here array shapes are in F dimension order, i.e., axis 0 is RA, axis 1 is Dec, axis 2 is Frequency, etc.
        # mask = mask.astype(int)
        # while len(mask.shape) < len(hdr['shape']):
        #    mask = np.expand_dims(mask, axis=len(mask.shape)) # Note that here array shapes are in F dimension order, i.e., axis 0 is RA, axis 1 is Dec, axis 2 is Frequency, etc.
        # print('**********************')
        # print('type(mask)', type(mask), 'mask.dtype', mask.dtype, 'mask.shape', mask.shape) # Note that here array shapes are in F dimension order, i.e., axis 0 is RA, axis 1 is Dec, axis 2 is Frequency, etc.
        # print('**********************')
        # os.system('rm -rf '+out_file+'.temp_collapsed'+' 2>/dev/null')
        ##myia.open(out_file+'.temp_copy')
        # newimage = myia.newimagefromarray(outfile=out_file+'.temp_collapsed', pixels=mask.astype(int), overwrite=True)
        # newimage.done()
        # myia.close()
        # 
        # collapse channel and higher axes
        os.system('rm -rf ' + out_file + '.temp_collapsed' + ' 2>/dev/null')
        myia.open(out_file + '.temp_copy')
        collapsed = myia.collapse(outfile=out_file + '.temp_collapsed', function='max',
                                  axes=np.arange(maskhdr['ndim'] - 1, 2 - 1, -1))
        collapsed.done()
        myia.close()
        # ia tools -- https://casa.nrao.edu/docs/CasaRef/image-Tool.html
        os.system('rm -rf ' + out_file + '.temp_copy' + ' 2>/dev/null')
        os.system('cp -r ' + out_file + '.temp_collapsed' + ' ' + out_file + '.temp_copy' + ' 2>/dev/null')
        os.system('rm -rf ' + out_file + '.temp_collapsed' + ' 2>/dev/null')

    # Align to the template grid
    os.system('rm -rf ' + out_file + '.temp_aligned' + ' 2>/dev/null')
    casaStuff.imregrid(imagename=out_file + '.temp_copy',
                       template=template,
                       output=out_file + '.temp_aligned',
                       asvelocity=True,
                       interpolation='nearest',
                       replicate=False,
                       overwrite=True)

    # Make an EXACT copy of the template, avoids various annoying edge cases
    os.system('rm -rf ' + out_file + ' 2>/dev/null')
    myim.mask(image=template, mask=out_file)

    # Pull the data out of the aligned mask and place it in the output file
    myia.open(out_file + '.temp_aligned')
    mask = myia.getchunk(dropdeg=True)
    myia.close()

    # If requested, blank the mask wherever the cube is non-finite.
    if blank_to_match:
        myia.open(template)
        nans = np.invert(myia.getchunk(dropdeg=True, getmask=True))
        myia.close()
        mask[nans] = 0.0

    # Shove the mask into the data set
    if is_template_2D and not is_mask_2D:
        while len(mask.shape) < len(hdr['shape']):
            mask = np.expand_dims(mask, axis=len(mask.shape))
        # print('**********************')
        # print('type(mask)', type(mask), 'mask.dtype', mask.dtype, 'mask.shape', mask.shape) # Note that here array shapes are in F dimension order, i.e., axis 0 is RA, axis 1 is Dec, axis 2 is Frequency, etc.
        # print('**********************')
        myia.open(out_file)
        data = myia.getchunk(dropdeg=False)
        data = mask
        myia.putchunk(data)
        myia.close()
    else:
        # Need to make sure this works for two dimensional cases, too.
        if (hdr['axisnames'][3] == 'Frequency') and (hdr['ndim'] == 4):
            myia.open(out_file)
            data = myia.getchunk(dropdeg=False)
            data[:, :, 0, :] = mask
            myia.putchunk(data)
            myia.close()
        elif (hdr['axisnames'][2] == 'Frequency') and (hdr['ndim'] == 4):
            myia.open(out_file)
            data = myia.getchunk(dropdeg=False)
            data[:, :, :, 0] = mask
            myia.putchunk(data)
            myia.close()
        else:
            logger.info("ALERT! Did not find a case.")

    os.system('rm -rf ' + out_file + '.temp_copy' + ' 2>/dev/null')
    os.system('rm -rf ' + out_file + '.temp_aligned' + ' 2>/dev/null')
    return ()
def signal_mask(
        cube_root=None,
        out_file=None,
        suffix_in='',
        suffix_out='',
        operation='AND',
        high_snr=4.0,
        low_snr=2.0,
        absolute=False,
        do_roll=True,
):
    """
    A simple signal mask creation routine used to make masks on the
    fly during imaging. Leverages CASA statistics and scipy.
    """

    if not os.path.isdir(cube_root + '.image' + suffix_in):
        logger.error('Data file not found: "' + cube_root + '.image' + suffix_in + '"')
        logger.info('Need CUBE_ROOT.image to be an image file.')
        logger.info('Returning. Generalize the code if you want different syntax.')
        return

    if os.path.isdir(cube_root + '.residual' + suffix_in):
        stats = stat_cube(cube_root + '.residual' + suffix_in)
    else:
        stats = stat_cube(cube_root + '.image' + suffix_in)
    rms = stats['medabsdevmed'][0] / 0.6745
    hi_thresh = high_snr * rms
    low_thresh = low_snr * rms

    header = casaStuff.imhead(cube_root + '.image' + suffix_in)
    if header['axisnames'][2] == 'Frequency':
        spec_axis = 2
    else:
        spec_axis = 3

    logger.info('Reading cube.')
    cube = read_cube(cube_root + '.image' + suffix_in, allow_huge=True)

    logger.info('Building high mask.')
    if absolute:
        hi_mask = (np.abs(cube) > hi_thresh)
    else:
        hi_mask = (cube > hi_thresh)

    if high_snr > low_snr:
        logger.info('Expanding mask.')
        logger.info('Building low mask.')
        if absolute:
            low_mask = (np.abs(cube) > low_thresh)
        else:
            low_mask = (cube > low_thresh)
        if do_roll:
            logger.info('... rolling.')
            rolled_low_mask = \
                (low_mask + np.roll(low_mask, 1, axis=spec_axis) + \
                 np.roll(low_mask, -1, axis=spec_axis)) >= 1
            low_mask = rolled_low_mask

        logger.info('... joining low mask with high mask via dilation.')
        mask = ndimage.binary_dilation(hi_mask,
                                       mask=low_mask,
                                       iterations=-1)
        del low_mask
        del hi_mask
        if do_roll:
            del rolled_low_mask
    else:
        logger.info('No expansion requested.')
        if do_roll:
            logger.info('... rolling.')
            mask = \
                (hi_mask + np.roll(hi_mask, 1, axis=spec_axis) + \
                 np.roll(hi_mask, -1, axis=spec_axis)) >= 1
            del hi_mask
        else:
            mask = hi_mask

    # Expect to be here with minimal memory footprint and mask
    # created.

    if operation == 'AND' or operation == 'OR':
        if os.path.isdir(cube_root + '.mask' + suffix_out):
            old_mask = read_cube(cube_root + '.mask' + suffix_out, allow_huge=True)
        else:
            logger.info("Operation AND/OR requested but no previous mask found.")
            logger.info("... will set operation=NEW.")
            operation = 'NEW'

    logger.info('Joining with old mask.')
    if operation == 'AND':
        mask = mask * old_mask
    if operation == 'OR':
        mask = (mask + old_mask) > 0
    if operation == 'NEW':
        mask = mask

    del old_mask

    logger.info('Recasting as an int.')
    # this might be better: mask.astype(np.int, copy=False)
    # mask = mask.astype(int)
    mask = mask.astype(np.int32)

    # Export the image to fits, put in the mask and convert back to a CASA image
    logger.info('Writing mask to disk')

    write_mask(cube_root + '.image' + suffix_in, cube_root + '.mask' + suffix_out, mask, allow_huge=True)
示例#8
0
def prep_sd_for_feather(sdfile_in=None,
                        sdfile_out=None,
                        interf_file=None,
                        do_import=True,
                        do_dropdeg=True,
                        do_align=True,
                        do_checkunits=True,
                        overwrite=False):
    """
    Prepare single dish data for feathering. 

    sdfile_in : the input single dish file. Can be a FITS file (with
    do_import) or a CASA image.

    sdfile_out : the output of the program. If all flags are called,
    this will be a CASA image on the same astrometric grid as the
    interferometric file with units of Jy/beam and no degenerate axes.

    interf_file : the interferometric file being used in the feather
    call. Used as a template here to astrometrically align the data.

    do_import (default True) : If True and the infile is a FITS file,
    then import the data from FITS.

    do_dropdeg (default True) : if True then pare degenerate axes from
    the signle dish file. In general this is a good idea for
    postprocessing, where mixing and matching degenerate axes causes
    many CASA routines to fail.

    do_align (default True) : If True then align the single dish data to the
    astrometric grid of the the interferometer file.

    do_checkunits (default True) : If True then check the units to make
    sure that they are in Jy/beam, which is required by feather.

    overwrite (default False) : Delete existing files. You probably
    want to set this to True but it's a user decision.
    """

    # Check inputs

    if (os.path.isdir(interf_file) == False):
        logger.error("Interferometric file not found: " + interf_file)
        return (None)

    if (os.path.isdir(sdfile_in) == False) and (os.path.isfile(sdfile_in)
                                                == False):
        logger.error("Single dish file not found: " + sdfile_in)
        return (None)

    if sdfile_out is None:
        logger.error(
            "Output single dish file name not supplied via sdfile_out=")
        return (None)

    # Initialize file handling

    current_infile = sdfile_in
    current_outfile = sdfile_out
    tempfile_name = sdfile_out + '.temp'

    # Import from FITS if needed. Keep blanks as not-a-numbers.

    if do_import:
        if ((current_infile[-4:] == 'FITS') or \
                (current_infile[-4:] == 'fits')) and \
                os.path.isfile(current_infile):
            logger.info("Importing from FITS.")

            casaStuff.importfits(fitsimage=current_infile,
                                 imagename=current_outfile,
                                 zeroblanks=False,
                                 overwrite=overwrite)
            current_infile = current_outfile

    # Drop degenerate axes using a call to imsubimage

    if do_dropdeg:
        if current_infile == current_outfile:
            if os.path.isdir(tempfile_name) or os.path.isfile(tempfile_name):
                if overwrite:
                    os.system('rm -rf ' + tempfile_name)
                else:
                    logger.error(
                        "Temp file needed but exists and overwrite=False - " +
                        tempfile_name)
                    return (None)
            os.system('cp -r ' + current_infile + ' ' + tempfile_name)
            current_infile = tempfile_name
            os.system('rm -rf ' + current_outfile)

        if os.path.isdir(current_outfile) or os.path.isfile(current_outfile):
            if overwrite:
                os.system('rm -rf ' + current_outfile)
            else:
                logger.error(
                    "Output file needed exists and overwrite=False - " +
                    current_outfile)
                return (None)

        casaStuff.imsubimage(imagename=current_infile,
                             outfile=current_outfile,
                             dropdeg=True)

        current_infile = current_outfile

    # Align the single dish data to the astrometric grid of the interferometric data

    if do_align:
        if current_infile == current_outfile:
            if os.path.isdir(tempfile_name) or os.path.isfile(tempfile_name):
                if overwrite:
                    os.system('rm -rf ' + tempfile_name)
                else:
                    logger.error(
                        "Temp file needed but exists and overwrite=False - " +
                        tempfile_name)
                    return (None)
            os.system('cp -r ' + current_infile + ' ' + tempfile_name)
            current_infile = tempfile_name
            os.system('rm -rf ' + current_outfile)

        casaStuff.imregrid(imagename=current_infile,
                           template=interf_file,
                           output=current_outfile,
                           asvelocity=True,
                           axes=[-1],
                           interpolation='cubic',
                           overwrite=overwrite)

        current_infile = current_outfile

    # Check the units on the singledish file and convert from K to Jy/beam if needed.

    if do_checkunits:
        hdr = casaStuff.imhead(current_outfile, mode='list')
        unit = hdr['bunit']
        if unit == 'K':
            logger.info("Unit is Kelvin. Converting.")
            ccr.convert_ktojy(infile=current_outfile,
                              overwrite=overwrite,
                              inplace=True)

    # Remove leftover temporary files.

    if (os.path.isdir(tempfile_name) or os.path.isfile(tempfile_name)):
        if overwrite:
            os.system('rm -rf ' + tempfile_name)

    return (None)
def convolve_to_round_beam(infile=None,
                           outfile=None,
                           force_beam=None,
                           overwrite=False):
    """
    Convolve supplied image to have a round beam. Optionally, force
    that beam to some size, else it figures out the beam.
    """

    if infile is None or outfile is None:
        logger.error("Missing required input.")
        return (None)

    if os.path.isdir(infile) == False:
        logger.error("Input file missing - " + infile)
        return (None)

    hdr = casaStuff.imhead(infile)

    if (hdr['axisunits'][0] != 'rad'):
        logger.error(
            "Based on CASA experience. I expected units of radians. I did not find this."
        )
        logger.error("Adjust code or investigate file " + infile)
        return (None)
    pixel_as = abs(hdr['incr'][0] / np.pi * 180.0 * 3600.)

    if 'perplanebeams' in hdr.keys():
        beamnames = hdr['perplanebeams']['beams'].keys()
        majorlist = [
            hdr['perplanebeams']['beams'][b]['*0']['major']['value']
            for b in beamnames
        ]
        bmaj = np.max(majorlist)
    else:
        if (hdr['restoringbeam']['major']['unit'] != 'arcsec'):
            logger.error(
                "Based on CASA experience. I expected units of arcseconds for the beam. I did not find this."
            )
            logger.error("Adjust code or investigate file " + infile)
            return (None)
        bmaj = hdr['restoringbeam']['major']['value']

    if force_beam is None:
        target_bmaj = np.sqrt((bmaj)**2 + (2.0 * pixel_as)**2)
    else:
        min_bmaj = np.sqrt((bmaj)**2 + (2.0 * pixel_as)**2)
        if force_beam < min_bmaj:
            logger.warning("Requested beam is too small for convolution.")
            return (None)
        target_bmaj = force_beam

    casaStuff.imsmooth(imagename=infile,
                       outfile=outfile,
                       targetres=True,
                       major=str(target_bmaj) + 'arcsec',
                       minor=str(target_bmaj) + 'arcsec',
                       pa='0.0deg',
                       overwrite=overwrite)

    # Copy over mask
    copy_mask(infile, outfile, allow_huge=True)

    return (target_bmaj)
def trim_cube(infile=None,
              outfile=None,
              overwrite=False,
              inplace=False,
              min_pixperbeam=3,
              pad=1):
    """
    Trim empty space from around the edge of a cube. Also rebin the
    cube to smaller size, while ensuring a minimum number of pixels
    across the beam. Used to reduce the volume of cubes.
    """

    if infile is None or outfile is None:
        logger.error("Missing required input.")
        return (False)

    if os.path.isdir(infile) == False:
        logger.error("Input file not found: " + infile)
        return (False)

    # First, rebin if needed
    hdr = casaStuff.imhead(infile)
    if (hdr['axisunits'][0] != 'rad'):
        logger.error(
            "ERROR: Based on CASA experience. I expected units of radians. I did not find this. Returning."
        )
        logger.error("Adjust code or investigate file " + infile)
        return (False)

    pixel_as = abs(hdr['incr'][0] / np.pi * 180.0 * 3600.)

    if (hdr['restoringbeam']['major']['unit'] != 'arcsec'):
        logger.error(
            "ERROR: Based on CASA experience. I expected units of arcseconds for the beam. I did not find this. Returning."
        )
        logger.error("Adjust code or investigate file " + infile)
        return (False)
    bmaj = hdr['restoringbeam']['major']['value']

    pix_per_beam = bmaj * 1.0 / pixel_as * 1.0

    if pix_per_beam > 6:
        casaStuff.imrebin(
            imagename=infile,
            outfile=outfile + '.temp',
            factor=[2, 2, 1],
            crop=True,
            dropdeg=True,
            overwrite=overwrite,
        )
    else:
        os.system('cp -r ' + infile + ' ' + outfile + '.temp')

    # Figure out the extent of the image inside the cube

    myia = au.createCasaTool(casaStuff.iatool)
    myia.open(outfile + '.temp')
    myia.adddegaxes(outfile=outfile + '.temp_deg', stokes='I', overwrite=True)
    myia.close()

    mask = get_mask(outfile, allow_huge=True)

    this_shape = mask.shape

    mask_spec_x = np.sum(np.sum(mask * 1.0, axis=2), axis=1) > 0
    xmin = np.max([0, np.min(np.where(mask_spec_x)) - pad])
    xmax = np.min([np.max(np.where(mask_spec_x)) + pad, mask.shape[0] - 1])

    mask_spec_y = np.sum(np.sum(mask * 1.0, axis=2), axis=0) > 0
    ymin = np.max([0, np.min(np.where(mask_spec_y)) - pad])
    ymax = np.min([np.max(np.where(mask_spec_y)) + pad, mask.shape[1] - 1])

    mask_spec_z = np.sum(np.sum(mask * 1.0, axis=0), axis=0) > 0
    zmin = np.max([0, np.min(np.where(mask_spec_z)) - pad])
    zmax = np.min([np.max(np.where(mask_spec_z)) + pad, mask.shape[2] - 1])

    box_string = '' + str(xmin) + ',' + str(ymin) + ',' + str(
        xmax) + ',' + str(ymax)
    chan_string = '' + str(zmin) + '~' + str(zmax)

    logger.info("... box selection: " + box_string)
    logger.info("... channel selection: " + chan_string)

    if overwrite:
        os.system('rm -rf ' + outfile)
        casaStuff.imsubimage(
            imagename=outfile + '.temp',
            outfile=outfile,
            box=box_string,
            chans=chan_string,
        )

    os.system('rm -rf ' + outfile + '.temp')

    return (True)