def convolve_cube(cube, newbeam, res_tol=0.0, min_coverage=0.8, append_raw=False, verbose=False, suppress_error=False): """ Convolve a spectral cube to a specified beam. This function is essentially a wrapper around `~spectral_cube.SpectralCube.convolve_to()`, but it treats NaN values / edge effect in a more careful way (see the documentation of keyword 'min_coverage' below). Parameters ---------- cube : ~spectral_cube.SpectralCube object Input spectral cube newbeam : radio_beam.Beam object Target beam to convolve to res_tol : float, optional Tolerance on the difference between input/output resolution By default, a convolution is performed on the input cube whenever its native resolution is different from (sharper than) the target resolution. Use this keyword to specify a tolerance on resolution, within which no convolution will be performed. For example, res_tol=0.1 will allow a 10% tolerance. min_coverage : float or None, optional When the convolution meets NaN values or edges, the output is calculated based on beam-weighted average. This keyword specifies the minimum beam covering fraction of valid (np.finite) values. All pixels with less beam covering fraction will be assigned NaNs. Default is 80% beam covering fraction (min_coverage=0.8). If the user would rather use the interpolation strategy in `astropy.convolution.convolve_fft`, set this keyword to None. Note that the NaN pixels will be kept as NaN. append_raw : bool, optional Whether to append the raw convolved cube and weight cube Default is not to append. verbose : bool, optional Whether to print the detailed processing information in terminal Default is to not print. suppress_error : bool, optional Whether to suppress the error when convolution is unsuccessful Default is to not suppress. Returns ------- outcube : SpectralCube object or tuple Convolved spectral cube (when append_raw=False), or a tuple comprising 3 cubes (when append_raw=True) """ from functools import partial from astropy.convolution import convolve_fft if min_coverage is None: # Skip coverage check and preserve NaN values. # This uses the default interpolation strategy # implemented in 'astropy.convolution.convolve_fft' convolve_func = partial(convolve_fft, preserve_nan=True, allow_huge=True, quiet=~verbose) else: # Do coverage check to determine the mask on the output convolve_func = partial(convolve_fft, nan_treatment='fill', boundary='fill', fill_value=0., allow_huge=True, quiet=~verbose) if (res_tol > 0) and (newbeam.major != newbeam.minor): raise ValueError("You cannot specify a non-zero resolution " "torelance if the target beam is not round") tol = newbeam.major * np.array([1-res_tol, 1+res_tol]) if ((tol[0] < cube.beam.major < tol[1]) and (tol[0] < cube.beam.minor < tol[1])): if verbose: print("Native resolution within tolerance - " "Copying original cube...") my_append_raw = False newcube = cube.unmasked_copy().with_mask(cube.mask.include()) else: if verbose: print("Convolving cube...") try: convcube = cube.convolve_to(newbeam, convolve=convolve_func) if min_coverage is not None: # divide the raw convolved image by the weight image my_append_raw = True wtcube = SpectralCube( cube.mask.include().astype('float'), cube.wcs, beam=cube.beam) wtcube = wtcube.convolve_to(newbeam, convolve=convolve_func) newcube = convcube / wtcube.unmasked_data[:] # mask all pixels w/ weight smaller than min_coverage threshold = min_coverage * u.dimensionless_unscaled newcube = newcube.with_mask(wtcube >= threshold) else: my_append_raw = False newcube = convcube print("") # force line break after the progress bar except ValueError as err: if suppress_error: return else: raise ValueError( "Unsuccessful convolution: {}\nOld: {}\nNew: {}" "".format(err, cube.beam, newbeam)) if append_raw and my_append_raw: return newcube, convcube, wtcube else: return newcube
def cleansplit(filename, galaxy=None, Vwindow=650 * u.km / u.s, Vgalaxy=300 * u.km / u.s, blorder=3, HanningLoops=0, maskfile=None, circleMask=True, edgeMask=False, weightCut=0.2, spectralSetup=None, spatialSmooth=1.0): """ Takes a raw DEGAS cube and produces individual cubes for each spectral line. Paramters --------- filename : str The file to split. Keywords -------- galaxy : Galaxy object Currently unused Vwindow : astropy.Quantity Width of the window in velocity units Vgalaxy : astropy.Quantity Line of sight velocity of the galaxy centre blorder : int Baseline order HanningLoops : int Number of times to smooth and resample the data edgeMask : bool Determine whether to apply an edgeMask weightCut : float Minimum weight value to include in the data spatialSmooth : float Factor to increase the (linear) beam size by in a convolution. spectralSetup : str String to determine how we set up the spectrum 'hcn_hcop' -- split based on HCN/HCO+ setup '13co_c18o' -- split based on 13CO/C18O setup '12co' -- don't split; assume single line """ Cube = SpectralCube.read(filename) CatalogFile = get_pkg_data_filename('./data/dense_survey.cat', package='degas') Catalog = Table.read(CatalogFile, format='ascii') # Find which galaxy in our catalog corresponds to the object we # are mapping if galaxy is None: RABound, DecBound = Cube.world_extrema match = np.zeros_like(Catalog, dtype=np.bool) for index, row in enumerate(Catalog): galcoord = SkyCoord(row['RA'], row['DEC'], unit=(u.hourangle, u.deg)) if (galcoord.ra < RABound[1] and galcoord.ra > RABound[0] and galcoord.dec < DecBound[1] and galcoord.dec > DecBound[0]): match[index] = True MatchRow = Catalog[match] galcoord = SkyCoord(MatchRow['RA'], MatchRow['DEC'], unit=(u.hourangle, u.deg)) Galaxy = MatchRow['NAME'].data[0] print("Catalog Match with " + Galaxy) V0 = MatchRow['CATVEL'].data[0] * u.km / u.s # Check spectral setups. Use the max frequencies present to # determine which spectral setup we used if not specifed. if spectralSetup is None: if (Cube.spectral_axis.max() > 105 * u.GHz and Cube.spectral_axis.max() < 113 * u.GHz): warnings.warn("assuming 13CO/C18O spectral setup") spectralSetup = '13CO_C18O' filestr = '13co_c18o' if (Cube.spectral_axis.max() > 82 * u.GHz and Cube.spectral_axis.max() < 90 * u.GHz): warnings.warn("assuming HCN/HCO+ spectral setup") spectralSetup = 'HCN_HCO+' filestr = 'hcn_hcop' if (Cube.spectral_axis.max() > 113 * u.GHz): warnings.warn("assuming 12CO spectral setup") spectralSetup = '12CO' filestr = '12co' if spectralSetup == '13CO_C18O': CEighteenO = Cube.with_spectral_unit(u.km / u.s, velocity_convention='radio', rest_value=109.78217 * u.GHz) ThirteenCO = Cube.with_spectral_unit(u.km / u.s, velocity_convention='radio', rest_value=110.20135 * u.GHz) CubeList = (CEighteenO, ThirteenCO) LineList = ('C18O', '13CO') elif spectralSetup == 'HCN_HCO+': HCN = Cube.with_spectral_unit(u.km / u.s, velocity_convention='radio', rest_value=88.631847 * u.GHz) HCOp = Cube.with_spectral_unit(u.km / u.s, velocity_convention='radio', rest_value=89.188518 * u.GHz) CubeList = (HCN, HCOp) LineList = ('HCN', 'HCOp') elif spectralSetup == '12CO': TwelveCO = Cube.with_spectral_unit(u.km / u.s, velocity_convention='radio', rest_value=115.27120180 * u.GHz) CubeList = (TwelveCO, ) LineList = ('12CO', ) for ThisCube, ThisLine in zip(CubeList, LineList): if circleMask: x0, y0, _ = ThisCube.wcs.wcs_world2pix(galcoord.ra, galcoord.dec, 0, 0) ThisCube = circletrim(ThisCube, filename.replace('.fits', '_wts.fits'), x0, y0, weightCut=weightCut) if edgeMask: ThisCube = edgetrim(ThisCube, filename.replace('.fits', '_wts.fits'), weightCut=weightCut) # Trim each cube to the specified velocity range ThisCube = ThisCube.spectral_slab(V0 - Vwindow, V0 + Vwindow) ThisCube.write(Galaxy + '_' + ThisLine + '.fits', overwrite=True) StartChan = ThisCube.closest_spectral_channel(V0 - Vgalaxy) EndChan = ThisCube.closest_spectral_channel(V0 + Vgalaxy) if maskfile is not None: maskLookup = buildMaskLookup(maskfile) shp = ThisCube.shape TmpCube = ThisCube.with_spectral_unit(u.Hz) spaxis = TmpCube.spectral_axis spaxis = spaxis.value data = ThisCube.filled_data[:].value for y in np.arange(shp[1]): for x in np.arange(shp[2]): spectrum = data[:, y, x] if np.any(np.isnan(spectrum)): continue coords = ThisCube.world[:, y, x] mask = maskLookup(coords[2].value, coords[1].value, spaxis) spectrum = robustBaseline(spectrum, blorder=blorder, baselineIndex=~mask) data[:, y, x] = spectrum ThisCube = SpectralCube(data * ThisCube.unit, ThisCube.wcs, header=ThisCube.header, meta={'BUNIT': ThisCube.header['BUNIT']}) ThisCube.write(Galaxy + '_' + ThisLine + '_rebase{0}.fits'.format(blorder), overwrite=True) else: gbtpipe.Baseline.rebaseline(Galaxy + '_' + ThisLine + '.fits', baselineRegion=[ slice(0, StartChan, 1), slice(EndChan, ThisCube.shape[0], 1) ], blorder=blorder) ThisCube = SpectralCube.read(Galaxy + '_' + ThisLine + '_rebase{0}'.format(blorder) + '.fits') # Smooth Kern = Kernel1D(array=np.array([0.5, 1.0, 0.5])) for i in range(HanningLoops): ThisCube.spectral_smooth(Kern) ThisCube = ThisCube[::2, :, :] # Spatial Smooth if spatialSmooth > 1.0: newBeam = Beam(major=ThisCube.beam.major * spatialSmooth, minor=ThisCube.beam.minor * spatialSmooth) ThisCube.convolve_to(newBeam) smoothstr = '_smooth{0}'.format(spatialSmooth) else: smoothstr = '' # Final Writeout ThisCube.write(Galaxy + '_' + ThisLine + '_rebase{0}'.format(blorder) + smoothstr + '_hanning{0}.fits'.format(HanningLoops), overwrite=True)
def convolve_cube(incube, newbeam, mode='datacube', res_tol=0.0, min_coverage=0.8, nan_treatment='fill', boundary='fill', fill_value=0., append_raw=False, verbose=False, suppress_error=False): """ Convolve a spectral cube or an rms noise cube to a specified beam. 'datacube' mode: This is basically a wrapper around `~spectral_cube.SpectralCube.convolve_to()`, but it treats NaN values / edge effect in a more careful way (see the description of keyword 'min_coverage' below). 'noisecube' mode: It handles rms noise cubes in a way that it correctly predicts the rms noise for the corresponding data cube convolved to the same specified beam. Parameters ---------- incube : FITS HDU object or SpectralCube object Input spectral cube newbeam : radio_beam.Beam object Target beam to convolve to mode : {'datacube', 'noisecube'}, optional Whether the input cube is a data cube or an rms noise cube. In the former case, a direct convolution is performed; in the latter case, the convolution attempts to mimic the error propagation process to the specified lower resolution. (Default: 'datacube') res_tol : float, optional Tolerance on the difference between input/output resolution By default, a convolution is performed on the input cube when its native resolution is different from (sharper than) the target resolution. Use this keyword to specify a tolerance on resolution, within which no convolution will be performed. For example, ``res_tol=0.1`` will allow a 10% tolerance. min_coverage : float or None, optional This keyword specifies a minimum beam covering fraction of valid pixels for convolution (Default: 0.8). Locations with a beam covering fraction less than this value will be overwritten to "NaN" in the convolved cube. If the user would rather use the ``preserve_nan`` mode in `astropy.convolution.convolve_fft`, set this keyword to None. nan_treatment: {'interpolate', 'fill'}, optional To be passed to `astropy.convolution.convolve_fft`. (Default: 'fill') boundary: {'fill', 'wrap'}, optional To be passed to `astropy.convolution.convolve_fft`. (Default: 'fill') fill_value : float, optional To be passed to `astropy.convolution.convolve_fft`. (Default: 0) append_raw : bool, optional Whether to append the raw convolved cube and weight cube. Default is not to append. verbose : bool, optional Whether to print the detailed processing log in terminal. Default is to not print. suppress_error : bool, optional Whether to suppress the error message when convolution is unsuccessful. Default is to not suppress. Returns ------- outcube : FITS HDU objects or SpectralCube objects Convolved spectral cubes (when append_raw=False), or a 3-tuple including a masked verson, an unmaked version, and a coverage fraction cube (when append_raw=True). The output will be the same type of objects as the input. """ if isinstance(incube, SpectralCube): cube = incube elif isinstance(incube, (fits.PrimaryHDU, fits.ImageHDU)): if 'BUNIT' in incube.header: unit = u.Unit(incube.header['BUNIT'], parse_strict='warn') if isinstance(unit, u.UnrecognizedUnit): unit = u.dimensionless_unscaled else: unit = u.dimensionless_unscaled cube = SpectralCube(data=incube.data * unit, wcs=WCS(incube.header), header=incube.header, allow_huge_operations=True).with_mask( np.isfinite(incube.data)) else: raise ValueError("`incube` needs to be either a SpectralCube object " "or a FITS HDU object") if (res_tol > 0) and (newbeam.major != newbeam.minor): raise ValueError("Cannot handle a non-zero resolution torelance " "when the target beam is not round") if min_coverage is None: # Skip coverage check and preserve NaN values. # This uses the default 'preserve_nan' scheme # implemented in 'astropy.convolution.convolve_fft' convolve_func = partial(convolve_fft, fill_value=fill_value, nan_treatment=nan_treatment, boundary=boundary, preserve_nan=True, allow_huge=True) else: # Do coverage check to determine the mask on the output convolve_func = partial(convolve_fft, fill_value=fill_value, nan_treatment=nan_treatment, boundary=boundary, allow_huge=True) convolve_func_w = partial(convolve_fft, fill_value=0., boundary='fill', allow_huge=True) tol = newbeam.major * np.array([1 - res_tol, 1 + res_tol]) if ((tol[0] < cube.beam.major < tol[1]) and (tol[0] < cube.beam.minor < tol[1])): if verbose: print("Native resolution within tolerance - " "copying original cube...") my_append_raw = False convcube = wtcube = None newcube = cube.unmasked_copy().with_mask(cube.mask.include()) else: if verbose: print("Deconvolving beam...") try: beamdiff = newbeam.deconvolve(cube.beam) except ValueError as err: if suppress_error: if verbose: print("Unsuccessful beam deconvolution: " "{}\nOld: {}\nNew: {}" "".format(err, cube.beam, newbeam)) print("Exiting...") return else: raise ValueError("Unsuccessful beam deconvolution: " "{}\nOld: {}\nNew: {}" "".format(err, cube.beam, newbeam)) if verbose: print("Convolving cube...") if mode == 'datacube': # do convolution convcube = cube.convolve_to(newbeam, convolve=convolve_func) if min_coverage is not None: my_append_raw = True wtcube = SpectralCube(cube.mask.include().astype('float'), cube.wcs, beam=cube.beam).with_mask( np.ones(cube.shape).astype('?')) wtcube.allow_huge_operations = (cube.allow_huge_operations) wtcube = wtcube.convolve_to(newbeam, convolve=convolve_func_w) # divide the raw convolved image by the weight image # to correct for filling fraction newcube = convcube / wtcube.unmasked_data[:] # mask all pixels w/ weight smaller than min_coverage threshold = min_coverage * u.dimensionless_unscaled newcube = newcube.with_mask(wtcube >= threshold) else: my_append_raw = False newcube = convcube wtcube = None elif mode == 'noisecube': # Empirically derive a noise cube at the lower resolution # Step 1: square the high resolution noise cube cubesq = cube**2 # Step 2: convolve the squared noise cube with a kernel # that is sqrt(2) times narrower than the one # used for data cube convolution (this is because # the Gaussian weight needs to be squared in # error propagation) beamdiff_small = Beam(major=beamdiff.major / np.sqrt(2), minor=beamdiff.minor / np.sqrt(2), pa=beamdiff.pa) newbeam_small = cube.beam.convolve(beamdiff_small) convcubesq = cubesq.convolve_to(newbeam_small, convolve=convolve_func) del cubesq # release memory if min_coverage is not None: my_append_raw = True wtcube = SpectralCube(cube.mask.include().astype('float'), cube.wcs, beam=cube.beam).with_mask( np.ones(cube.shape).astype('?')) wtcube.allow_huge_operations = (cube.allow_huge_operations) # divide the raw convolved cube by the weight cube # to correct for filling fraction wtcube_d = wtcube.convolve_to(newbeam_small, convolve=convolve_func_w) newcubesq = convcubesq / wtcube_d.unmasked_data[:] del convcubesq, wtcube_d # release memory # mask all pixels w/ weight smaller than min_coverage # (here I force the masking of the noise cube to be # consistent with that of the data cube) wtcube = wtcube.convolve_to(newbeam, convolve=convolve_func_w) threshold = min_coverage * u.dimensionless_unscaled newcubesq = newcubesq.with_mask(wtcube >= threshold) else: my_append_raw = False newcubesq = convcubesq wtcube = None # Step 3: find the sqrt of the convolved noise cube convcube = np.sqrt(convcubesq) del convcubesq # release memory newcube = np.sqrt(newcubesq) del newcubesq # release memory # Step 4: apply a multiplicative factor, which accounts # for the decrease in rms noise due to averaging convcube *= np.sqrt(cube.beam.sr / newbeam.sr).to('').value newcube *= np.sqrt(cube.beam.sr / newbeam.sr).to('').value else: raise ValueError("Invalid `mode` value: {}".format(mode)) if isinstance(incube, SpectralCube): if append_raw and my_append_raw: return newcube, convcube, wtcube else: return newcube elif isinstance(incube, (fits.PrimaryHDU, fits.ImageHDU)): if append_raw and my_append_raw: return newcube.hdu, convcube.hdu, wtcube.hdu else: return newcube.hdu
def smooth_cube( incube=None, outfile=None, angular_resolution=None, linear_resolution=None, distance=None, velocity_resolution=None, nan_treatment='interpolate', # can also be 'fill' tol=None, make_coverage_cube=False, collapse_coverage=False, coveragefile=None, coverage2dfile=None, dtype=np.float32, overwrite=True ): """ Smooth an input cube to coarser angular or spectral resolution. This lightly wraps spectral cube and some of the error checking is left to that. tol is a fraction. When the target beam is within tol of the original beam, we just copy. Optionally, also calculate a coverage footprint in which original (finite) cube coverage starts at 1.0 and the output cube shows the fraction of finite pixels. """ # &%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&% # Error checking # &%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&% # Require a valid cube or map input if type(incube) is SpectralCube: cube = incube elif type(incube) == type("hello"): cube = SpectralCube.read(incube) else: logger.error("Input must be a SpectralCube object or a filename.") # Allow huge operations. If the speed or segfaults become a huge # problem, we will adjust our strategy here. cube.allow_huge_operations = True # Check that only one target scale is set if (angular_resolution is not None) and (linear_resolution is not None): logger.error('Only one of angular_resolution or ', 'linear_resolution can be set') return(None) # Work out the target angular resolution if angular_resolution is not None: if type(angular_resolution) is str: angular_resolution = u.Quantity(angular_resolution) if linear_resolution is not None: if distance is None: logger.error('Convolution to linear resolution requires a distance.') return(None) if type(distance) is str: distance = u.Quantity(distance) if type(linear_resolution) is str: linear_resolution = u.Quantity(linear_resolution) angular_resolution = (linear_resolution / distance * u.rad).to(u.arcsec) dist_mpc_val = float(distance.to(u.pc).value) / 1e6 cube._header.append(('DIST_MPC',dist_mpc_val,'Used in convolution')) if tol is None: tol = 0.0 # &%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&% # Convolution to coarser beam # &%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&% if angular_resolution is not None: logger.info("... convolving from beam: "+str(cube.beam)) target_beam = Beam(major=angular_resolution, minor=angular_resolution, pa=0 * u.deg) logger.info("... convolving to beam: "+str(target_beam)) new_major = float(target_beam.major.to(u.arcsec).value) old_major = float(cube.beam.major.to(u.arcsec).value) delta = (new_major-old_major)/old_major logger.info("... fractional change: "+str(delta)) if make_coverage_cube: coverage = SpectralCube(np.isfinite(cube.unmasked_data[:])*1.0, wcs=cube.wcs, header=cube.header, meta={'BUNIT': ' ', 'BTYPE': 'Coverage'}) coverage = coverage.with_mask(LazyMask(np.isfinite,cube=coverage)) # Allow huge operations. If the speed or segfaults become a huge # problem, we will adjust our strategy here. coverage.allow_huge_operations = True if delta > tol: logger.info("... proceeding with convolution.") cube = cube.convolve_to(target_beam, nan_treatment=nan_treatment) if make_coverage_cube: coverage = coverage.convolve_to(target_beam, nan_treatment=nan_treatment) if np.abs(delta) < tol: logger.info("... current resolution meets tolerance.") if delta < -1.0*tol: logger.info("... resolution cannot be matched. Returning") return(None) # &%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&% # Spectral convolution # &%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&% # This is only a boxcar smooth right now and does not downsample # or update the header. if velocity_resolution is not None: if type(velocity_resolution) is str: velocity_resolution = u.Quantity(velocity_resolution) dv = scdr.channel_width(cube) nChan = (velocity_resolution / dv).to(u.dimensionless_unscaled).value if nChan > 1: cube = cube.spectral_smooth(Box1DKernel(nChan)) if make_coverage_cube: coverage = coverage.spectral_smooth(Box1DKernel(nChan)) # &%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&% # Write or return as requested # &%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&%&% if outfile is not None: # cube.write(outfile, overwrite=overwrite) hdu = fits.PrimaryHDU(np.array(cube.filled_data[:], dtype=dtype), header=cube.header) hdu.writeto(outfile, overwrite=overwrite) if make_coverage_cube: if coveragefile is not None: hdu = fits.PrimaryHDU(np.array(coverage.filled_data[:], dtype=dtype), header=coverage.header) hdu.writeto(coveragefile, overwrite=overwrite) if collapse_coverage: if coveragefile and not coverage2dfile: coverage2dfile = coveragefile.replace('.fits','2d.fits') coverage_collapser(coverage, coverage2dfile=coverage2dfile, overwrite=overwrite) # coverage.write(coveragefile, overwrite=overwrite) return(cube)
def common_cube(filename, target_rms=None, target_resolution=None, datadir='', outdir='', outname='common.fits', cube=None, rmsname=None, rmsmap=None, distance=None, overwrite=True, return_cube=False, huge=True): """ Convolves and adds noise to a data cube to arrive at a target linear resolution and noise level Parameters: filename : str File name of spectral data cube to process outname : str File name for output cube target_rms : astropy.Quantity Target RMS level in units of cube brightness target_resolution : astropy.Quantity Target resolution expressed as either length (linear resolution) or angle (angular resolution) datadir : str Directory for input data outdir : str Directory for output data rmsname : str Name for input RMS data cube rmsmap : np.ndarray numpy array of noise values at positions corresponding to the spectral cube. distance : astropy.Quantity Distance to target; required if a linear target resolution is requested overwrite : bool Overwrite output products if they already exist? return_cube : bool Return a SpectralCube? """ if cube is None: cube = SpectralCube.read(os.path.join(datadir, filename)) cube.allow_huge_operations = huge orig_beam_area = cube.beam.sr target_beam = None if target_resolution is not None: if target_resolution.unit.is_equivalent(u.pc): if distance is None: warnings.warn( "A distance with units must be provided to reach a target linear resolution" ) raise ValueError target_beam_size = (target_resolution / distance).to( u.dimensionless_unscaled) * u.rad elif target_resolution.is_equivalent(u.rad): target_beam_size = target_resolution else: warnings.warn( "target_resolution must be either linear distance or angle") raise ValueError target_beam = Beam(major=target_beam_size, minor=target_beam_size, pa=0 * u.deg) if target_beam > cube.beam: cube = cube.convolve_to(target_beam) else: warnings.warn('Target linear resolution unreachable.') if target_rms is not None: if target_beam is None: target_beam = cube.beam noisevals = np.random.randn(*cube.shape) null_beam = Beam(major=1e-7 * u.deg, minor=1e-7 * u.deg, pa=0 * u.deg) noisevals[~np.isfinite(cube)] = np.nan noisecube = SpectralCube(noisevals, cube.wcs, header=cube.header, beam=null_beam, meta={'BUNIT': 'K'}) noisecube = noisecube.with_mask(np.isfinite(cube)) noisecube.allow_huge_operations = huge area_ratio = (orig_beam_area / target_beam.sr).to( u.dimensionless_unscaled).value noisecube = noisecube.convolve_to(target_beam) if (rmsname is not None) and (rmsmap is None): rmsmap = fits.getdata(os.path.join(datadir, rmsname)) * u.K if rmsmap is None: warnings.warn( "One of rmsmap or rmsname must be provided for noise homogenization" ) raise FileNotFoundError rmsamplitude = rmsmap * area_ratio**0.5 if np.all(rmsamplitude > target_rms): warnings.warn("All noise values larger than target value") else: addamplitude = (target_rms**2 - rmsamplitude**2) addamplitude[addamplitude < 0] = 0.0 addamplitude.shape = addamplitude.shape noisevals = noisecube.filled_data[:] noisevals /= np.nanstd(noisevals) noisevals *= (addamplitude**0.5) cube.allow_huge_operations = huge cube += noisevals if type(outname) is str: cube.write(outdir + outname, overwrite=overwrite) if return_cube: return (cube) else: return (True)