def test_from_sequence(self): # Load a SExtractor catalog into memory and pass all of its stars to # Catalog.from_sequence(): both Catalogs must be equal. This is not the # case if the order of the stars passed to from_sequence() is altered, # of course, as tuples are compared position by position. catalog = Catalog(self.SAMPLE_CATALOG_PATH) stars = catalog[:] identical = Catalog.from_sequence(*stars) self.assertEqual(type(identical), Catalog) self.assertEqual(identical, catalog) different = Catalog.from_sequence(*stars[::-1]) self.assertEqual(type(different), Catalog) self.assertNotEqual(different, catalog) # Now create a second Catalog containing those stars whose magnitude is # greater than or equal to sixteen. There are nineteen of these stars, # but this number is anyway irrelevant for our purposes. What matters # is that all the stars passed to from_sequence() must also be in the # returned Catalog, in the same order. stars = [star for star in catalog if star.mag > 16] self.assertEqual(len(stars), 19) faint_catalog = Catalog.from_sequence(*stars) self.assertEqual(type(faint_catalog), Catalog) self.assertEqual(list(faint_catalog), list(stars))
def test_get_sky_coordinates(self): catalog = Catalog(self.SAMPLE_CATALOG_PATH) self.assertEqual(len(catalog), 127) coordinates = catalog.get_sky_coordinates() # There should be as many Coordinates objects, and in the same order, # as sources there are in the SExtractor catalog. Therefore, the right # ascension and declination of the i-th Coordinates object must match # those of the i-th source in the Catalog. self.assertEqual(len(coordinates), len(catalog)) for coord, star in zip(coordinates, catalog): self.assertEqual(coord.ra, star.alpha) self.assertEqual(coord.dec, star.delta)
def test_immutability(self): """ Make sure that the Catalog class is immutable """ catalog = Catalog(self.SAMPLE_CATALOG_PATH) with self.assertRaises(AttributeError): catalog.path = self.SAMPLE_INCOMPLETE_PATH with self.assertRaises(AttributeError): del catalog.path for index in xrange(len(catalog)): star = StarTest.random() with self.assertRaises(TypeError): catalog[index] = star with self.assertRaises(TypeError): del catalog[index]
def test_init(self): path = self.SAMPLE_CATALOG_PATH catalog = Catalog(path) self.assertEqual(catalog.path, path) self.assertEqual(len(catalog), 127) star = catalog[0] self.assertEqual(star.x, 844.359) self.assertEqual(star.y, 434.109) self.assertEqual(star.alpha, 100.2910553) self.assertEqual(star.delta, +9.4697032) self.assertEqual(star.area, 8085) self.assertEqual(star.mag, 8.2460) self.assertFalse(star.saturated) # S/N = FLUX_ISO / FLUXERR_ISO = 8545557 / 12811.09 self.assertAlmostEqual(star.snr, 667.043709786) self.assertAlmostEqual(star.fwhm, 12.668) # FLUX_RADIUS x 2 self.assertEqual(star.elongation, 1.562) star = catalog[68] self.assertEqual(star.x, 4.189) self.assertEqual(star.y, 1995.196) self.assertEqual(star.alpha, 100.1844554) self.assertEqual(star.delta, +9.2744660) self.assertEqual(star.area, 16) self.assertEqual(star.mag, 14.8506) self.assertFalse(star.saturated) # S/N = FLUX_ISO / FLUXERR_ISO = 10539.52 / 569.9098 self.assertAlmostEqual(star.snr, 18.49331245) self.assertAlmostEqual(star.fwhm, 2.126) # FLUX_RADIUS x 2 self.assertEqual(star.elongation, 1.339) star = catalog[126] self.assertEqual(star.x, 107.033) self.assertEqual(star.y, 1953.500) self.assertEqual(star.alpha, 100.1971009) self.assertEqual(star.delta, +9.2796855) self.assertEqual(star.area, 10) self.assertEqual(star.mag, 15.9567) self.assertTrue(star.saturated) # S/N = FLUX_ISO / FLUXERR_ISO = 3833.773 / 450.5532 self.assertAlmostEqual(star.snr, 8.509035115) self.assertAlmostEqual(star.fwhm, 2.412) # FLUX_RADIUS x 2 self.assertEqual(star.elongation, 1.613) # IOError is raised if the SExtractor catalog does not exist; in order # to get the path to a non-existent file, we use NamedTemporaryFile and # close the file (deleting it) immediately after. with tempfile.NamedTemporaryFile(suffix='.cat') as fd: pass self.assertFalse(os.path.exists(fd.name)) self.assertRaises(IOError, Catalog, fd.name) # ValueError is raised if one of the required SExtractor parameters is # not in the catalog, or if it was not saved in the ASCII_HEAD format # and therefore there are no comment lines listing column labels, which # are needed in order to determine in which column each parameter is. self.assertRaises(ValueError, Catalog, self.SAMPLE_INCOMPLETE_PATH) self.assertRaises(ValueError, Catalog, self.SAMPLE_NOASCIIHEAD_PATH)
def test_flag_saturated(self): # Numbers in the [0, 255] range whose 3rd least significant bit is one saturated_flags = set([ 4, 5, 6, 7, 12, 13, 14, 15, 20, 21, 22, 23, 28, 29, 30, 31, 36, 37, 38, 39, 44, 45, 46, 47, 52, 53, 54, 55, 60, 61, 62, 63, 68, 69, 70, 71, 76, 77, 78, 79, 84, 85, 86, 87, 92, 93, 94, 95, 100, 101, 102, 103, 108, 109, 110, 111, 116, 117, 118, 119, 124, 125, 126, 127, 132, 133, 134, 135, 140, 141, 142, 143, 148, 149, 150, 151, 156, 157, 158, 159, 164, 165, 166, 167, 172, 173, 174, 175, 180, 181, 182, 183, 188, 189, 190, 191, 196, 197, 198, 199, 204, 205, 206, 207, 212, 213, 214, 215, 220, 221, 222, 223, 228, 229, 230, 231, 236, 237, 238, 239, 244, 245, 246, 247, 252, 253, 254, 255 ]) for flag in xrange(0, 256): is_saturated = Catalog.flag_saturated(flag) if flag in saturated_flags: self.assertTrue(is_saturated) else: self.assertFalse(is_saturated) # Flags outside of the range raise ValueError self.assertRaises(ValueError, Catalog.flag_saturated, -2) self.assertRaises(ValueError, Catalog.flag_saturated, -1) self.assertRaises(ValueError, Catalog.flag_saturated, 256) self.assertRaises(ValueError, Catalog.flag_saturated, 257)
def test_flag_saturated(self): # Numbers in the [0, 255] range whose 3rd least significant bit is one saturated_flags = set([4, 5, 6, 7, 12, 13, 14, 15, 20, 21, 22, 23, 28, 29, 30, 31, 36, 37, 38, 39, 44, 45, 46, 47, 52, 53, 54, 55, 60, 61, 62, 63, 68, 69, 70, 71, 76, 77, 78, 79, 84, 85, 86, 87, 92, 93, 94, 95, 100, 101, 102, 103, 108, 109, 110, 111, 116, 117, 118, 119, 124, 125, 126, 127, 132, 133, 134, 135, 140, 141, 142, 143, 148, 149, 150, 151, 156, 157, 158, 159, 164, 165, 166, 167, 172, 173, 174, 175, 180, 181, 182, 183, 188, 189, 190, 191, 196, 197, 198, 199, 204, 205, 206, 207, 212, 213, 214, 215, 220, 221, 222, 223, 228, 229, 230, 231, 236, 237, 238, 239, 244, 245, 246, 247, 252, 253, 254, 255]) for flag in xrange(0, 256): is_saturated = Catalog.flag_saturated(flag) if flag in saturated_flags: self.assertTrue(is_saturated) else: self.assertFalse(is_saturated) # Flags outside of the range raise ValueError self.assertRaises(ValueError, Catalog.flag_saturated, -2) self.assertRaises(ValueError, Catalog.flag_saturated, -1) self.assertRaises(ValueError, Catalog.flag_saturated, 256) self.assertRaises(ValueError, Catalog.flag_saturated, 257)
def test_sextractor(self): # Note that, being a third-party program, we cannot write a unit test # to check that SExtractor works as it should (i.e., that it correctly # detects astronomical sources on FITS images): that is something that # Emmanuel Bertin, its developer, certainly takes care of. What we must # test for is whether astromatic.sextractor(), our Python wrapper to # call SExtractor, works as we expect and raises the appropriate errors # when something goes wrong. Nothing less, nothing more. # First, the most probable scenario: our wrapper should run SExtractor # on any FITS image that it receives (here we use some downloaded from # the STScI Digitized Sky Survey) and return the path to the output # catalog, which astromatic.Catalog should be always able to parse. kwargs = dict(stdout=open(os.devnull), stderr=open(os.devnull)) for img_path in dss_images.TEST_IMAGES: catalog_path = astromatic.sextractor(img_path, **kwargs) try: Catalog(catalog_path) finally: os.unlink(catalog_path) # SExtractorNotInstalled must be raised if no SExtractor executable is # detected. In order to simulate this, mock os.environ and clear the # 'PATH' environment variable. In this manner, as the list of paths to # directories where executables may be found is empty, SExtractor (and # any other command) will appear as not installed on the system. environment_copy = copy.deepcopy(os.environ) with mock.patch.object(os, 'environ', environment_copy) as mocked: mocked['PATH'] = '' with self.assertRaises(astromatic.SExtractorNotInstalled): astromatic.sextractor(img_path) # IOError is raised if any of the four SExtractor configuration files # is not readable or does no exist. To test that, mock the module-level # variables so that they first refer to an unreadable file and later to # a nonexistent one. for variable in self.SEXTRACTOR_MODULE_VARS: path = eval('astromatic.%s' % variable) ext = os.path.splitext(path)[1] copy_path = get_nonexistent_path(ext=ext) shutil.copy2(path, copy_path) with mock.patch.object(astromatic, variable, copy_path): # chmod u-r mode = stat.S_IMODE(os.stat(copy_path)[stat.ST_MODE]) mode ^= stat.S_IRUSR os.chmod(copy_path, mode) args = IOError, astromatic.sextractor, img_path self.assertFalse(os.access(copy_path, os.R_OK)) self.assertRaises(*args) os.unlink(copy_path) self.assertFalse(os.path.exists(copy_path)) self.assertRaises(*args) # SExtractorUpgradeRequired must be raised if the version of SExtractor # that is installed on the system is older than that defined in the # SEXTRACTOR_REQUIRED_VERSION module-level variable. We cannot (easily, # at least) change the version that is installed, but we can test for # this by changing the value of the required version, setting it to a # tuple that is greater than the installed version of SExtractor. # From, for example, (2, 8, 6) to (2, 8, 7) version = list(astromatic.sextractor_version()) version[-1] += 1 version = tuple(version) with mock.patch.object(astromatic, 'SEXTRACTOR_REQUIRED_VERSION', version): with self.assertRaises(astromatic.SExtractorUpgradeRequired): astromatic.sextractor(img_path) # The SExtractorError exception is raised if anything goes wrong during # the execution of SExtractor (in more technical terms, if its return # code is other than zero). For example, the FITS image may not exist, # or a non-numerical value may be assigned to a parameter that expects # one, or we could try to run SExtractor on a FITS extension that does # not exist. These three are just examples: SExtractor may fail for # many different reasons that we cannot even foresee. # (1) Try to run SExtractor on a non-existent image kwargs = dict(stdout=open(os.devnull), stderr=open(os.devnull)) nonexistent_path = get_nonexistent_path(ext='.fits') with self.assertRaises(astromatic.SExtractorError): astromatic.sextractor(nonexistent_path, **kwargs) # (2) The DETECT_MINAREA parameter (minimum number of pixels above the # threshold needed to trigger detection) expects an integer. Assigning # to it a string will cause SExtractor to complain ("keyword out of # range") and abort its execution. kwargs['options'] = dict(DETECT_MINAREA='Y') with self.assertRaises(astromatic.SExtractorError): astromatic.sextractor(img_path, **kwargs) del kwargs['options'] # (3) Try to run SExtractor on a nonexistent FITS extension. hdulist = pyfits.open(img_path, mode='readonly') nextensions = len(hdulist) hdulist.close() kwargs['ext'] = nextensions + 1 with self.assertRaises(astromatic.SExtractorError): astromatic.sextractor(img_path, **kwargs) # TypeError raised if 'options' is not a dictionary kwargs = dict(options=['DETECT_MINAREA', '5']) with self.assertRaises(TypeError): astromatic.sextractor(img_path, **kwargs) # ... or if any of its elements is not a string. kwargs['options'] = {'DETECT_MINAREA': 125} with self.assertRaises(TypeError): astromatic.sextractor(img_path, **kwargs) # TypeError also raised if 'ext' is not an integer... kwargs = dict(ext=0.56) with self.assertRaises(TypeError): astromatic.sextractor(img_path, **kwargs) # ... even if it is a float but has nothing after the decimal point # (for which the built-in is_integer() function would return True). kwargs = dict(ext=0.0) with self.assertRaises(TypeError): astromatic.sextractor(img_path, **kwargs)