def run(img, coordinates, epoch, aperture, annulus, dannulus, maximum, datek, timek, exptimek, uncimgk, cbox=0): """ Do photometry on a FITS image. This convenience function does photometry on a FITSImage object, applying proper-motion correction to a series of astronomical objects and measuring them. The FITS image must have been previously calibrated astrometrically, so that right ascensions and declinations are meaningful. Returns a QPhot object, using None as the magnitude of those astronomical objects that are INDEF (i.e., so faint that qphot could not measure anything) and positive infinity if they are saturated (i.e., if one or more pixels in the aperture are above the saturation level). Arguments: img - the fitsimage.FITSImage object on which to do photometry. coordinates - an iterable of astromatic.Coordinates objects, one for each astronomical object to be measured. epoch - the epoch of the coordinates of the astronomical objects, used to compute the proper-motion correction. Must be an integer, such as 2000 for J2000. aperture - the aperture radius, in pixels. annulus - the inner radius of the sky annulus, in pixels. dannulus - the width of the sky annulus, in pixels. maximum - number of ADUs at which saturation arises. If one or more pixels in the aperture are above this value, the magnitude of the astronomical object is set to positive infinity. For coadded observations, the effective saturation level is obtained by multiplying this value by the number of coadded images. datek - the image header keyword containing the date of the observation, in the format specified in the FITS Standard. The old date format was 'yy/mm/dd' and may be used only for dates from 1900 through 1999. The new Y2K compliant date format is 'yyyy-mm-dd' or 'yyyy-mm-ddTHH:MM:SS[.sss]'. This keyword is not necessary if none of the astromatic.Coordinates objects have a known proper motion. When that is the case, it is not even read from the FITS header. timek - the image header keyword containing the time at which the observation started, in the format HH:MM:SS[.sss]. This keyword is not necessary (and, therefore, is ignored) if the time is included directly as part of the 'datek' keyword value with the format 'yyyy-mm-ddTHH:MM:SS[.sss]'. As with 'datek', if no object has a proper motion this keyword is ignored and not read from the header. exptimek - the image header keyword containing the exposure time. Needed by qphot in order to normalize the computed magnitudes to an exposure time of one time unit. uncimgk - the image header keyword containing the path to the image used to check for saturation. It is expected to be the original FITS file (that is, before any calibration step, since corrections such as flat-fielding may move a saturated pixel below the saturation level) of the very image on which photometry is done. If this argument is set to an empty string or None, saturation is checked for on the same FITS image used for photometry, 'img'. cbox - the width of the centering box, in pixels. Accurate centers for each astronomical object are computed using the centroid centering algorithm. This means that, unless this argument is zero (the default value), photometry is not done exactly on the specified coordinates, but instead where IRAF has determined that the actual, accurate center of each object is. This is usually a good thing, and helps improve the photometry. """ kwargs = dict(date_keyword=datek, time_keyword=timek, exp_keyword=exptimek) # The date of observation is only actually needed when we need to apply # proper motion corrections. Therefore, don't call FITSImage.year() unless # one or more of the astromatic.Coordinates objects have a proper motion. # This avoids an unnecessary KeyError exception when we do photometry on a # FITS image without the 'datek' or 'timek' keywords (for example, a mosaic # created with IPAC's Montage): when that happens we cannot apply proper # motion corrections, that's right, but that's not an issue if none of our # objects have a known proper motion. for coord in coordinates: if coord.pm_ra or coord.pm_dec: try: year = img.year(**kwargs) break except KeyError as e: # Include the missing FITS keyword in the exception message regexp = "keyword '(?P<keyword>.*?)' not found" match = re.search(regexp, str(e)) assert match is not None msg = ("{0}: keyword '{1}' not found. It is needed in order " "to be able to apply proper-motion correction, as one " "or more astronomical objects have known proper motions" .format(img.path, match.group('keyword'))) raise KeyError(msg) else: # No object has a known proper motion, so don't call # FITSImage.year(). Use the same value as the epoch, so that when # get_coords_file() below applies the proper motion correction the # input and output coordinates are the same. year = epoch # The proper-motion corrected objects coordinates coords_path = get_coords_file(coordinates, year, epoch) img_qphot = QPhot(img.path, coords_path) img_qphot.run(annulus, dannulus, aperture, exptimek, cbox=cbox) # How do we know whether one or more pixels in the aperture are above a # saturation threshold? As suggested by Frank Valdes at the IRAF.net # forums, we can make a mask of the saturated values, on which we can do # photometry using the same aperture. If we get a non-zero flux, we know it # has saturation: http://iraf.net/forum/viewtopic.php?showtopic=1466068 if not uncimgk: orig_img_path = img.path else: orig_img_path = img.read_keyword(uncimgk) if not os.path.exists(orig_img_path): msg = "image %s (keyword '%s' of image %s) does not exist" args = orig_img_path, uncimgk, img.path raise IOError(msg % args) try: # Temporary file to which the saturation mask is saved basename = os.path.basename(orig_img_path) mkstemp_prefix = "%s_satur_mask_%d_ADUS_" % (basename, maximum) kwargs = dict(prefix=mkstemp_prefix, suffix='.fits', text=True) mask_fd, satur_mask_path = tempfile.mkstemp(**kwargs) os.close(mask_fd) # IRAF's imexpr won't overwrite the file. Instead, it will raise an # IrafError exception stating that "IRAF task terminated abnormally # ERROR (1121, "FXF: EOF encountered while reading FITS file". os.unlink(satur_mask_path) # The expression that will be given to 'imexpr'. The space after the # colon is needed to avoid sexigesimal interpretation. 'a' is the first # and only operand, linked to our image at the invokation of the task. expr = "a>%d ? 1 : 0" % maximum logging.debug("%s: imexpr = '%s'" % (img.path, expr)) logging.debug("%s: a = %s" % (img.path, orig_img_path)) logging.info("%s: Running IRAF's imexpr..." % img.path) pyraf.iraf.images.imexpr(expr, a=orig_img_path, output=satur_mask_path, verbose='yes', Stdout=methods.LoggerWriter('debug')) assert os.path.exists(satur_mask_path) msg = "%s: IRAF's imexpr OK" % img.path logging.info(msg) msg = "%s: IRAF's imexpr output = %s" logging.debug(msg % (img.path, satur_mask_path)) # Now we just do photometry again, on the same pixels, but this time on # the saturation mask. Those objects for which we get a non-zero flux # will be known to be saturated and their magnitude set to infinity. # If 'cbox' is other than zero, the center of each object may have been # recentered by qphot using the centroid centering algorithm. When that # is the case, we need to feed run() with these more accurate centers. # Since the QPhotResult objects contain x- and y-coordinates (qphot # does not support 'world' coordinates as the output system), we need # to convert them back to right ascension and declination. if cbox: root, _ = os.path.splitext(os.path.basename(img.path)) kwargs = dict(prefix=root + '_', suffix='_satur.coords', text=True) os.unlink(coords_path) fd, coords_path = tempfile.mkstemp(**kwargs) for object_phot in img_qphot: centered_x, centered_y = object_phot.x, object_phot.y ra, dec = img.pix2world(centered_x, centered_y) os.write(fd, "{0} {1}\n".format(ra, dec)) os.close(fd) mask_qphot = QPhot(satur_mask_path, coords_path) # No centering this time: if cbox != 0 the accurate centers for each # astronomical object have been computed using the centroid centering # algorithm, so we're already feeding run() with the accurate values. mask_qphot.run(annulus, dannulus, aperture, exptimek, cbox=0) os.unlink(coords_path) assert len(img_qphot) == len(mask_qphot) for object_phot, object_mask in itertools.izip(img_qphot, mask_qphot): if __debug__: # In cbox != 0 we cannot expect the coordinates to be the exact # same: the previous call to run() returned x and y coordinates # that we converted to celestial coordinates, and now qphot is # giving as output image coordinates again. It is unavoidable # to lose some precision. Anyway, this does not affect the # result: photometry was still done on almost the absolute # exact coordinates that we wanted it to. if not cbox: assert object_phot.x == object_mask.x assert object_phot.y == object_mask.y if object_mask.flux > 0: object_phot = object_phot._replace(mag=float('infinity')) finally: # Remove saturation mask. The try-except is necessary because an # exception may be raised before 'satur_mask_path' is defined. try: methods.clean_tmp_files(satur_mask_path) except NameError: pass return img_qphot
with open(solved_file, 'rb') as fd: if ord(fd.read()) != 1: raise AstrometryNetUnsolvedField(path) return output_path except subprocess.CalledProcessError, e: raise AstrometryNetError(e.returncode, e.cmd) # If .solved file doesn't exist or contain one except (IOError, AstrometryNetUnsolvedField): raise AstrometryNetUnsolvedField(path) except subprocess.TimeoutExpired: raise AstrometryNetTimeoutExpired(path, timeout) finally: null_fd.close() methods.clean_tmp_files(output_dir) @methods.print_exception_traceback def parallel_astrometry(args): """ Function argument of map_async() to do astrometry in parallel. This will be the first argument passed to multiprocessing.Pool.map_async(), which chops the iterable into a number of chunks that are submitted to the process pool as separate tasks. 'args' must be a three-element tuple with (1) a string with the path to the FITS image, (2) a string with the path to the output directory and (3) 'options', the optparse.Values object returned by optparse.OptionParser.parse_args(). This function does astrometry on each FITS image with the astrometry_net() function. The output FITS files, containing the WCS headers calculated by Astrometry.net, are written to the output directory with the same basename
assert stdev_str == 'INDEF' msg = "%s: stdev = None ('INDEF')" % self.path logging.debug(msg) stdev = None args = xcenter, ycenter, mag, sum_, flux, stdev self.append(QPhotResult(*args)) finally: # Remove temporary files. The try-except is necessary because an # exception may be raised before 'qphot_output' and 'txdump_output' # have been defined. try: methods.clean_tmp_files(qphot_output) except NameError: pass try: methods.clean_tmp_files(txdump_output) except NameError: pass return len(self) def get_coords_file(coordinates, year, epoch): """ Return a coordinates file with the exact positions of the objects. Loop over 'coordinates', an iterable of astromatic.Coordinates objects, and
def run(img, coordinates, epoch, aperture, annulus, dannulus, maximum, datek, timek, exptimek, uncimgk): """ Do photometry on a FITS image. This convenience function does photometry on a FITSImage object, applying proper-motion correction to a series of astronomical objects and measuring them. The FITS image must have been previously calibrated astrometrically, so that right ascensions and declinations are meaningful. Returns a QPhot object, using None as the magnitude of those astronomical objects that are INDEF (i.e., so faint that qphot could not measure anything) and positive infinity if they are saturated (i.e., if one or more pixels in the aperture are above the saturation level). Arguments: img - the fitsimage.FITSImage object on which to do photometry. coordinates - an iterable of astromatic.Coordinates objects, one for each astronomical object to be measured. epoch - the epoch of the coordinates of the astronomical objects, used to compute the proper-motion correction. Must be an integer, such as 2000 for J2000. aperture - the aperture radius, in pixels. annulus - the inner radius of the sky annulus, in pixels. dannulus - the width of the sky annulus, in pixels. maximum - number of ADUs at which saturation arises. If one or more pixels in the aperture are above this value, the magnitude of the astronomical object is set to positive infinity. For coadded observations, the effective saturation level is obtained by multiplying this value by the number of coadded images. datek - the image header keyword containing the date of the observation, in the format specified in the FITS Standard. The old date format was 'yy/mm/dd' and may be used only for dates from 1900 through 1999. The new Y2K compliant date format is 'yyyy-mm-dd' or 'yyyy-mm-ddTHH:MM:SS[.sss]'. timek - the image header keyword containing the time at which the observation started, in the format HH:MM:SS[.sss]. This keyword is not necessary (and, therefore, is ignored) if the time is included directly as part of the 'datek' keyword value with the format 'yyyy-mm-ddTHH:MM:SS[.sss]'. exptimek - the image header keyword containing the exposure time. Needed by qphot in order to normalize the computed magnitudes to an exposure time of one time unit. uncimgk - the image header keyword containing the path to the image used to check for saturation. It is expected to be the original FITS file (that is, before any calibration step, since corrections such as flat-fielding may move a saturated pixel below the saturation level) of the very image on which photometry is done. If this argument is set to an empty string or None, saturation is checked for on the same FITS image used for photometry, 'img'. """ kwargs = dict(date_keyword = datek, time_keyword = timek, exp_keyword = exptimek) year = img.year(**kwargs) # The proper-motion corrected objects coordinates coords_path = get_coords_file(coordinates, year, epoch) img_qphot = QPhot(img.path, coords_path) img_qphot.run(annulus, dannulus, aperture, exptimek) # How do we know whether one or more pixels in the aperture are above a # saturation threshold? As suggested by Frank Valdes at the IRAF.net # forums, we can make a mask of the saturated values, on which we can do # photometry using the same aperture. If we get a non-zero flux, we know it # has saturation: http://iraf.net/forum/viewtopic.php?showtopic=1466068 if not uncimgk: orig_img_path = img.path else: orig_img_path = img.read_keyword(uncimgk) if not os.path.exists(orig_img_path): msg = "image %s (keyword '%s' of image %s) does not exist" args = orig_img_path, uncimgk, img.path raise IOError(msg % args) try: # Temporary file to which the saturation mask is saved basename = os.path.basename(orig_img_path) mkstemp_prefix = "%s_satur_mask_%d_ADUS_" % (basename, maximum) kwargs = dict(prefix = mkstemp_prefix, suffix = '.fits', text = True) mask_fd, satur_mask_path = tempfile.mkstemp(**kwargs) os.close(mask_fd) # IRAF's imexpr won't overwrite the file. Instead, it will raise an # IrafError exception stating that "IRAF task terminated abnormally # ERROR (1121, "FXF: EOF encountered while reading FITS file". os.unlink(satur_mask_path) # The expression that will be given to 'imexpr'. The space after the # colon is needed to avoid sexigesimal interpretation. 'a' is the first # and only operand, linked to our image at the invokation of the task. expr = "a>%d ? 1 : 0" % maximum logging.debug("%s: imexpr = '%s'" % (img.path, expr)) logging.debug("%s: a = %s" % (img.path, orig_img_path)) logging.info("%s: Running IRAF's imexpr..." % img.path) pyraf.iraf.images.imexpr(expr, a = orig_img_path, output = satur_mask_path, verbose = 'yes', Stdout = methods.LoggerWriter('debug')) assert os.path.exists(satur_mask_path) msg = "%s: IRAF's imexpr OK" % img.path logging.info(msg) msg = "%s: IRAF's imexpr output = %s" logging.debug(msg % (img.path, satur_mask_path)) # Now we just do photometry again, on the same pixels, but this time on # the saturation mask. Those objects for which we get a non-zero flux # will be known to be saturated and their magnitude set to infinity. mask_qphot = QPhot(satur_mask_path, coords_path) mask_qphot.run(annulus, dannulus, aperture, exptimek) os.unlink(coords_path) assert len(img_qphot) == len(mask_qphot) for object_phot, object_mask in itertools.izip(img_qphot, mask_qphot): assert object_phot.x == object_mask.x assert object_phot.y == object_mask.y if object_mask.flux > 0: object_phot = object_phot._replace(mag = float('infinity')) finally: # Remove saturation mask. The try-except is necessary because an # exception may be raised before 'satur_mask_path' is defined. try: methods.clean_tmp_files(satur_mask_path) except NameError: pass return img_qphot
def run(img, coordinates, epoch, aperture, annulus, dannulus, maximum, datek, timek, exptimek, uncimgk, cbox = 0): """ Do photometry on a FITS image. This convenience function does photometry on a FITSImage object, applying proper-motion correction to a series of astronomical objects and measuring them. The FITS image must have been previously calibrated astrometrically, so that right ascensions and declinations are meaningful. Returns a QPhot object, using None as the magnitude of those astronomical objects that are INDEF (i.e., so faint that qphot could not measure anything) and positive infinity if they are saturated (i.e., if one or more pixels in the aperture are above the saturation level). Arguments: img - the fitsimage.FITSImage object on which to do photometry. coordinates - an iterable of astromatic.Coordinates objects, one for each astronomical object to be measured. epoch - the epoch of the coordinates of the astronomical objects, used to compute the proper-motion correction. Must be an integer, such as 2000 for J2000. aperture - the aperture radius, in pixels. annulus - the inner radius of the sky annulus, in pixels. dannulus - the width of the sky annulus, in pixels. maximum - number of ADUs at which saturation arises. If one or more pixels in the aperture are above this value, the magnitude of the astronomical object is set to positive infinity. For coadded observations, the effective saturation level is obtained by multiplying this value by the number of coadded images. datek - the image header keyword containing the date of the observation, in the format specified in the FITS Standard. The old date format was 'yy/mm/dd' and may be used only for dates from 1900 through 1999. The new Y2K compliant date format is 'yyyy-mm-dd' or 'yyyy-mm-ddTHH:MM:SS[.sss]'. This keyword is not necessary if none of the astromatic.Coordinates objects have a known proper motion. When that is the case, it is not even read from the FITS header. timek - the image header keyword containing the time at which the observation started, in the format HH:MM:SS[.sss]. This keyword is not necessary (and, therefore, is ignored) if the time is included directly as part of the 'datek' keyword value with the format 'yyyy-mm-ddTHH:MM:SS[.sss]'. As with 'datek', if no object has a proper motion this keyword is ignored and not read from the header. exptimek - the image header keyword containing the exposure time. Needed by qphot in order to normalize the computed magnitudes to an exposure time of one time unit. uncimgk - the image header keyword containing the path to the image used to check for saturation. It is expected to be the original FITS file (that is, before any calibration step, since corrections such as flat-fielding may move a saturated pixel below the saturation level) of the very image on which photometry is done. If this argument is set to an empty string or None, saturation is checked for on the same FITS image used for photometry, 'img'. cbox - the width of the centering box, in pixels. Accurate centers for each astronomical object are computed using the centroid centering algorithm. This means that, unless this argument is zero (the default value), photometry is not done exactly on the specified coordinates, but instead where IRAF has determined that the actual, accurate center of each object is. This is usually a good thing, and helps improve the photometry. """ kwargs = dict(date_keyword = datek, time_keyword = timek, exp_keyword = exptimek) # The date of observation is only actually needed when we need to apply # proper motion corrections. Therefore, don't call FITSImage.year() unless # one or more of the astromatic.Coordinates objects have a proper motion. # This avoids an unnecessary KeyError exception when we do photometry on a # FITS image without the 'datek' or 'timek' keywords (for example, a mosaic # created with IPAC's Montage): when that happens we cannot apply proper # motion corrections, that's right, but that's not an issue if none of our # objects have a known proper motion. for coord in coordinates: if coord.pm_ra or coord.pm_dec: try: year = img.year(**kwargs) break except KeyError as e: # Include the missing FITS keyword in the exception message regexp = "keyword '(?P<keyword>.*?)' not found" match = re.search(regexp, str(e)) assert match is not None msg = ("{0}: keyword '{1}' not found. It is needed in order " "to be able to apply proper-motion correction, as one " "or more astronomical objects have known proper motions" .format(img.path, match.group('keyword'))) raise KeyError(msg) else: # No object has a known proper motion, so don't call # FITSImage.year(). Use the same value as the epoch, so that when # get_coords_file() below applies the proper motion correction the # input and output coordinates are the same. year = epoch # The proper-motion corrected objects coordinates coords_path = get_coords_file(coordinates, year, epoch) img_qphot = QPhot(img.path, coords_path) img_qphot.run(annulus, dannulus, aperture, exptimek, cbox=cbox) # How do we know whether one or more pixels in the aperture are above a # saturation threshold? As suggested by Frank Valdes at the IRAF.net # forums, we can make a mask of the saturated values, on which we can do # photometry using the same aperture. If we get a non-zero flux, we know it # has saturation: http://iraf.net/forum/viewtopic.php?showtopic=1466068 if not uncimgk: orig_img_path = img.path else: orig_img_path = img.read_keyword(uncimgk) if not os.path.exists(orig_img_path): msg = "image %s (keyword '%s' of image %s) does not exist" args = orig_img_path, uncimgk, img.path raise IOError(msg % args) try: # Temporary file to which the saturation mask is saved basename = os.path.basename(orig_img_path) mkstemp_prefix = "%s_satur_mask_%d_ADUS_" % (basename, maximum) kwargs = dict(prefix = mkstemp_prefix, suffix = '.fits', text = True) mask_fd, satur_mask_path = tempfile.mkstemp(**kwargs) os.close(mask_fd) # IRAF's imexpr won't overwrite the file. Instead, it will raise an # IrafError exception stating that "IRAF task terminated abnormally # ERROR (1121, "FXF: EOF encountered while reading FITS file". os.unlink(satur_mask_path) # The expression that will be given to 'imexpr'. The space after the # colon is needed to avoid sexigesimal interpretation. 'a' is the first # and only operand, linked to our image at the invokation of the task. expr = "a>%d ? 1 : 0" % maximum logging.debug("%s: imexpr = '%s'" % (img.path, expr)) logging.debug("%s: a = %s" % (img.path, orig_img_path)) logging.info("%s: Running IRAF's imexpr..." % img.path) pyraf.iraf.images.imexpr(expr, a = orig_img_path, output = satur_mask_path, verbose = 'yes', Stdout = methods.LoggerWriter('debug')) assert os.path.exists(satur_mask_path) msg = "%s: IRAF's imexpr OK" % img.path logging.info(msg) msg = "%s: IRAF's imexpr output = %s" logging.debug(msg % (img.path, satur_mask_path)) # Now we just do photometry again, on the same pixels, but this time on # the saturation mask. Those objects for which we get a non-zero flux # will be known to be saturated and their magnitude set to infinity. # If 'cbox' is other than zero, the center of each object may have been # recentered by qphot using the centroid centering algorithm. When that # is the case, we need to feed run() with these more accurate centers. # Since the QPhotResult objects contain x- and y-coordinates (qphot # does not support 'world' coordinates as the output system), we need # to convert them back to right ascension and declination. if cbox: root, _ = os.path.splitext(os.path.basename(img.path)) kwargs = dict(prefix = root + '_', suffix = '_satur.coords', text = True) os.unlink(coords_path) fd, coords_path = tempfile.mkstemp(**kwargs) for object_phot in img_qphot: centered_x, centered_y = object_phot.x, object_phot.y ra, dec = img.pix2world(centered_x, centered_y) os.write(fd, "{0} {1}\n".format(ra, dec)) os.close(fd) mask_qphot = QPhot(satur_mask_path, coords_path) # No centering this time: if cbox != 0 the accurate centers for each # astronomical object have been computed using the centroid centering # algorithm, so we're already feeding run() with the accurate values. mask_qphot.run(annulus, dannulus, aperture, exptimek, cbox=0) os.unlink(coords_path) assert len(img_qphot) == len(mask_qphot) for object_phot, object_mask in itertools.izip(img_qphot, mask_qphot): if __debug__: # In cbox != 0 we cannot expect the coordinates to be the exact # same: the previous call to run() returned x and y coordinates # that we converted to celestial coordinates, and now qphot is # giving as output image coordinates again. It is unavoidable # to lose some precision. Anyway, this does not affect the # result: photometry was still done on almost the absolute # exact coordinates that we wanted it to. if not cbox: assert object_phot.x == object_mask.x assert object_phot.y == object_mask.y if object_mask.flux > 0: object_phot = object_phot._replace(mag = float('infinity')) finally: # Remove saturation mask. The try-except is necessary because an # exception may be raised before 'satur_mask_path' is defined. try: methods.clean_tmp_files(satur_mask_path) except NameError: pass return img_qphot