def get_rstar(self, df, source): # print(df.epo_x.values) # exit() cstar = SkyCoord( ra=source.cat.ra.values * u.rad, dec=source.cat.dec.values * u.rad, # distance = 1/(rad2arcsec*source.cat.par.values) * u.pc,frame='icrs') pm_ra_cosdec=source.cat.mu_a.values * u.rad / u.yr * np.cos(source.cat.dec.values), pm_dec=source.cat.mu_d.values * u.rad / u.yr, distance=1 / (rad2arcsec * source.cat.par.values) * u.pc, frame='icrs') cstar = cstar.apply_space_motion(dt=df.epo_x.values * u.year) if debug: print("cstar radec + pm =", cstar) cstar.representation_type = 'cartesian' rstar = np.column_stack( [cstar.x.to(u.m), cstar.y.to(u.m), cstar.z.to(u.m)]) if debug: print("cstar =", cstar) print("cstar =", rstar, np.linalg.norm(rstar)) print("cstar normalized =", rstar / np.linalg.norm(rstar)) return rstar
def image_gaia_query(image, *args, limit=3000, correct_pm=True, wcs=True, circular=True, fov=None): if wcs: center = image.wcs.pixel_to_world(*(np.array(image.shape) / 2)[::-1]) else: center = image.skycoord if fov is None: fov = image.fov table = gaia_query(center, fov, "*", limit=limit, circular=circular) if correct_pm: skycoord = SkyCoord(ra=table['ra'].quantity, dec=table['dec'].quantity, pm_ra_cosdec=table['pmra'].quantity, pm_dec=table['pmdec'].quantity, distance=table['parallax'].quantity, obstime=Time('2015-06-01 00:00:00.0')) with warnings.catch_warnings(): warnings.simplefilter("ignore") skycoord = skycoord.apply_space_motion(Time(image.date)) table["ra"] = skycoord.ra table["dec"] = skycoord.dec return table
def _correct_with_proper_motion(ra, dec, pm_ra, pm_dec, equinox, new_time): """Return proper-motion corrected RA / Dec. It also return whether proper motion correction is applied or not.""" # all parameters have units if ra is None or dec is None or \ pm_ra is None or pm_dec is None or (np.all(pm_ra == 0) and np.all(pm_dec == 0)) or \ equinox is None: return ra, dec, False # To be more accurate, we should have supplied distance to SkyCoord # in theory, for Gaia DR2 data, we can infer the distance from the parallax provided. # It is not done for 2 reasons: # 1. Gaia DR2 data has negative parallax values occasionally. Correctly handling them could be tricky. See: # https://www.cosmos.esa.int/documents/29201/1773953/Gaia+DR2+primer+version+1.3.pdf/a4459741-6732-7a98-1406-a1bea243df79 # 2. For our purpose (ploting in various interact usage) here, the added distance does not making # noticeable significant difference. E.g., applying it to Proxima Cen, a target with large parallax # and huge proper motion, does not change the result in any noticeable way. # c = SkyCoord(ra, dec, pm_ra_cosdec=pm_ra, pm_dec=pm_dec, frame='icrs', obstime=equinox) # Suppress ErfaWarning temporarily as a workaround for: # https://github.com/astropy/astropy/issues/11747 with warnings.catch_warnings(): # the same warning appears both as an ErfaWarning and a astropy warning # so we filter by the message instead warnings.filterwarnings("ignore", message="ERFA function") new_c = c.apply_space_motion(new_obstime=new_time) return new_c.ra, new_c.dec, True
def coordinate_propagator(ra, dec, parallax, pmra, pmdec, ref_epoch, target_epoch, tic_id): '''Authors: Patrick Tamburo, Boston University, July 2020 Purpose: Given a target's position, parallax, proper motion, and reference epoch, propagate space motion to target epoch. See https://docs.astropy.org/en/stable/coordinates/apply_space_motion.html for more examples. Inputs: ra (float): The target's DECIMAL RA at your reference epoch dec (float): The target's DECIMAL Dec at your reference epoch parallax (float): The target's parallax in milliarcsec pmra (flaot): The target's RA proper motion in milliarcsec/year pmdec (float): The target's Dec proper motion in millarcsec/year ref_epoch (float): Decimal year representing the epoch when coordinates, parallax, and proper motions were measured (e.g., 2015.5 for Gaia measurements) target_epoch (str): A string representing the epoch you want to propagate motion to (e.g., '2019-09-25') tic_id (str): The target's TIC ID (e.g., 'TIC 326109505') Outputs: new_ra, new_dec (float): The propagated position of the target at target_epoch. TODO: ''' c = SkyCoord(ra=ra * u.deg, dec=dec * u.deg, distance=Distance(parallax=parallax * u.mas), pm_ra_cosdec=pmra * u.mas / u.yr, pm_dec=pmdec * u.mas / u.yr, obstime=Time(ref_epoch, format='decimalyear')) target_obstime = Time(target_epoch) c_target_epoch = c.apply_space_motion( target_obstime) #Apply space motion to TESS epoch new_ra = c_target_epoch.ra.value new_dec = c_target_epoch.dec.value return new_ra, new_dec
def properMotion(self, SkyCoo, properMotion, dt): c = SkyCoord(ra=skyCoo[0] * u.degree, dec=skyCoo[1] * u.degree, pm_l_cosb=properMotion[0] * u.mas / u.yr, pm_b=properMotion[1] * u.mas / u.yr, frame='galactic', obstime=Time('1999-01-01T00:00:00.123')) return c.apply_space_motion(dt=dt * u.year)
def vizierLocationForTarget(exp, target, doMotionCorrection): """Get the target location from Vizier optionally correction motion. Parameters ---------- target : `str` The name of the target, e.g. 'HD 55852' Returns ------- targetLocation : `lsst.geom.SpherePoint` or `None` Location of the target object, optionally corrected for proper motion and parallax. Raises ------ ValueError If object not found in Hipparcos2 via Vizier. This is quite common, even for bright objects. """ # do not import at the module level - tests crash due to a race # condition with directory creation from astroquery.vizier import Vizier result = Vizier.query_object( target) # result is an empty table list for an unknown target try: star = result['I/311/hip2'] except TypeError: # if 'I/311/hip2' not in result (result doesn't allow easy checking without a try) raise ValueError epoch = "J1991.25" coord = SkyCoord( ra=star[0]['RArad'] * u.Unit(star['RArad'].unit), dec=star[0]['DErad'] * u.Unit(star['DErad'].unit), obstime=epoch, pm_ra_cosdec=star[0]['pmRA'] * u.Unit(star['pmRA'].unit), # NB contains cosdec already pm_dec=star[0]['pmDE'] * u.Unit(star['pmDE'].unit), distance=Distance(parallax=star[0]['Plx'] * u.Unit(star['Plx'].unit))) if doMotionCorrection: expDate = exp.getInfo().getVisitInfo().getDate() obsTime = astropy.time.Time(expDate.get(expDate.EPOCH), format='jyear', scale='tai') newCoord = coord.apply_space_motion(new_obstime=obsTime) else: newCoord = coord raRad, decRad = newCoord.ra.rad, newCoord.dec.rad ra = geom.Angle(raRad) dec = geom.Angle(decRad) targetLocation = geom.SpherePoint(ra, dec) return targetLocation
def test_regression_10092(): """ Check that we still get a proper motion even for SkyCoords without distance """ c = SkyCoord(l=10*u.degree, b=45*u.degree, pm_l_cosb=34*u.mas/u.yr, pm_b=-117*u.mas/u.yr, frame='galactic', obstime=Time('1988-12-18 05:11:23.5')) with pytest.warns(ErfaWarning, match='ERFA function "pmsafe" yielded .*'): # expect ErfaWarning here newc = c.apply_space_motion(dt=10*u.year) assert_quantity_allclose(newc.pm_l_cosb, 33.99980714*u.mas/u.yr, atol=1.0e-5*u.mas/u.yr)
def precess_gaia_coordinates(gaia_df, new_epoch=2000.0): ''' Precesses target coordinates from Gaia (i.e., using a parallax) from the original epoch to a new epoch. Radial velocities are assumed to be zero (because they're measured poorly). "The new position of the source is determined assuming that it moves in a straight line with constant velocity in an inertial frame." Adapted from the astropy docs: https://docs.astropy.org/en/stable/coordinates/apply_space_motion.html [Note that the *equinox* is a currently obsolete concept, formerly tied to equatorial coordinate systems centered on the Earth. The ICRS reference system, which Gaia uses, has an origin at the solar system barycenter and kinematic axes that are fixed and non-rotating. The nutation of the Earth does not affect these coordinates. See https://www.cosmos.esa.int/web/gaia/faqs#ICRSICRF] Parameters ---------- gaia_df: pandas DataFrame of Gaia data. It's assumed that you're starting from Gaia data (any data release). Keys must include 'ra', 'dec', 'parallax', 'pmra', 'pmdec', 'ref_epoch'. This can also be a dict. new_epoch: float Target epoch in Julian year format to precess from origin epoch. This is a float, like: 2000.0, 2021.0, etc. Returns ------- c, c_new_epoch: astropy Coordinate Original and precessed astropy coordinate objects corresponding to new_epoch. ''' c = SkyCoord(ra=nparr(gaia_df['ra']) * u.deg, dec=nparr(gaia_df['dec']) * u.deg, distance=Distance(parallax=nparr(gaia_df['parallax']) * u.mas), pm_ra_cosdec=nparr(gaia_df['pmra']) * u.mas/u.yr, pm_dec=nparr(gaia_df['pmdec']) * u.mas/u.yr, obstime=Time(nparr(gaia_df['ref_epoch']), format='jyear')) new_epoch = Time(new_epoch, format='jyear') c_new_epoch = c.apply_space_motion(new_epoch) return c, c_new_epoch
def propagate_pm(ticstars): ''' propagate proper motions to target epoch, re-compute the radial distances and sort the array by distances again ''' nrstars = len(ticstars) epochs = np.ndarray(nrstars, dtype='object') epochs.fill(ticepoch) mask = ~np.isnan(ticstars['pmRA'].data.data) & ~np.isnan( ticstars['pmDEC'].data.data) gd = np.where(mask) ngd = nrw(gd) if ngd == 0: # nothing to do return # assume 1000 pc distance for stars without known distance dist = ticstars['d'] bd = np.where(np.isnan(dist.data.data)) dist[bd] = 1000.0 # setup astropy coordinates, apply space motion and correct coordinates coords = SkyCoord(ra=ticstars['ra'][gd] * u.deg, dec=ticstars['dec'][gd] * u.deg, pm_ra_cosdec=ticstars['pmRA'][gd] * u.mas / u.yr, pm_dec=ticstars['pmDEC'][gd] * u.mas / u.yr, obstime=ticepoch, distance=dist[gd] * u.pc) newcoords = coords.apply_space_motion(Time(options.targetepoch)) newra = newcoords.ra.degree newdec = newcoords.dec.degree oldra = deepcopy(ticstars['ra']) olddec = deepcopy(ticstars['dec']) ticstars['ra'][gd] = newra ticstars['dec'][gd] = newdec # recompute the angular distances with new coordinates c0 = SkyCoord(ra=ticstars['ra'][0] * u.deg, dec=ticstars['dec'][0] * u.deg) for i in range(1, nrstars): # skip the target star itself cs = SkyCoord(ra=ticstars['ra'][i] * u.deg, dec=ticstars['dec'][i] * u.deg) rdist = c0.separation(cs) ticstars['dstArcSec'][i] = rdist.arcsecond # resort table by new radial distance ticstars.sort(['dstArcSec'])
def get_gaia_sources(self): """ Get the source table from gaia""" # Find the center of the first frame: # ra, dec = self.wcs[0].all_pix2world(self.center[:, None].T, 0).T # ra, dec = ra[0], dec[0] # ra2, dec2 = self.wcs[0].all_pix2world(np.asarray([[self.center[0] + self.shape[2]/2, self.center[1] + self.shape[1]/2]]), 0)[0] ra, dec = self.wcs[0].all_pix2world( np.vstack([self.X.ravel(), self.Y.ravel()]).T, 0).T # height = ((np.max(dec) - np.min(dec)) * 1.01)*u.deg # width = ((np.max(ra) - np.min(ra))/4)*u.deg radius = np.hypot((ra.max() - ra.min()) / 2, (dec.max() - dec.min()) / 2) # r = np.hypot(ra - ra2, dec - dec2) # print(ra, dec, r) sources = get_sources( ra.mean(), dec.mean(), radius=radius, #height=height, width=width, epoch=self.time[0], magnitude_limit=self.magnitude_limit).reset_index(drop=True) # Use gaia space motion to correct for any drifts in time dist = Distance(parallax=np.asarray(sources['Plx']) * u.mas, allow_negative=True) coords = SkyCoord( ra=np.asarray(sources['RA_ICRS']) * u.deg, dec=np.asarray(sources['DE_ICRS']) * u.deg, pm_ra_cosdec=np.nan_to_num(sources['pmRA']) * u.mas / u.year, pm_dec=np.nan_to_num(sources['pmDE']) * u.mas / u.year, distance=dist, obstime='J2015.05', radial_velocity=np.nan_to_num(sources['RV']) * u.km / u.s) cs = coords.apply_space_motion(self.time[0]) locs = self.wcs[0].wcs_world2pix( np.atleast_2d((cs.ra.deg, cs.dec.deg)).T, 0) # Trim out any sources that are outside the image lmask = (locs[:, 0] >= -1) & (locs[:, 0] <= self.shape[1] + 1) & ( locs[:, 1] >= -1) & (locs[:, 1] <= self.shape[2] + 1) sources, cs, locs = sources[lmask].reset_index( drop=True), cs[lmask], locs[lmask] return sources, cs, locs
def correct_gaia_to_epoch(gaia_cat: Union[str, table.QTable], new_epoch: time.Time): gaia_cat = load_catalogue(cat_name="gaia", cat=gaia_cat) epochs = list(map(lambda y: f"J{y}", gaia_cat['ref_epoch'])) gaia_coords = SkyCoord(ra=gaia_cat["ra"], dec=gaia_cat["dec"], pm_ra_cosdec=gaia_cat["pmra"], pm_dec=gaia_cat["pmdec"], obstime=epochs) u.debug_print(2, "astrometry.correct_gaia_to_epoch(): new_epoch ==", new_epoch) gaia_coords_corrected = gaia_coords.apply_space_motion( new_obstime=new_epoch) gaia_cat_corrected = copy.deepcopy(gaia_cat) gaia_cat_corrected["ra"] = gaia_coords_corrected.ra gaia_cat_corrected["dec"] = gaia_coords_corrected.dec new_epoch.format = "jyear" gaia_cat_corrected["ref_epoch"] = new_epoch.value return gaia_cat_corrected
def find_in_gaia(ra, dec): # First do a large search using 30 arcsec coord = SkyCoord(ra=ra, dec=dec, unit=(u.degree, u.degree), frame='icrs') radius = u.Quantity(30.0, u.arcsec) q = Gaia.cone_search_async(coord, radius) gaia = q.get_results() gaia = gaia[np.nan_to_num(gaia['parallax']) > 0] warning = (len(gaia) == 0) # Then propagate the Gaia coordinates to 2000, and find the best match to the # input coordinates if not warning: ra2015 = np.array(gaia['ra']) * u.deg dec2015 = np.array(gaia['dec']) * u.deg parallax = np.array(gaia['parallax']) * u.mas pmra = np.array(gaia['pmra']) * u.mas / u.yr pmdec = np.array(gaia['pmdec']) * u.mas / u.yr c2015 = SkyCoord(ra=ra2015, dec=dec2015, distance=Distance(parallax=parallax, allow_negative=True), pm_ra_cosdec=pmra, pm_dec=pmdec, obstime=Time(2015.5, format='decimalyear')) c2000 = c2015.apply_space_motion(dt=-15.5 * u.year) idx, sep, _ = coord.match_to_catalog_sky(c2000) # The best match object best = gaia[idx] gaia_id = best['source_id'] MG = 5 + 5 * np.log10( best['parallax'] / 1000) + best['phot_g_mean_mag'] bprp = best['bp_rp'] gaia_id = np.int(gaia_id) G = np.float(best['phot_g_mean_mag']) MG = np.float(MG) bprp = np.float(bprp) return gaia_id, G, MG, bprp
def get_radec_position_after_pm(self, date_obs): target_pmra = self.simbad[0]['PMRA'] * u.mas / u.yr if np.isnan(target_pmra): target_pmra = 0 * u.mas / u.yr target_pmdec = self.simbad[0]['PMDEC'] * u.mas / u.yr if np.isnan(target_pmdec): target_pmdec = 0 * u.mas / u.yr target_parallax = self.simbad[0]['PLX_VALUE'] * u.mas if target_parallax == 0 * u.mas: target_parallax = 1e-4 * u.mas target_coord = SkyCoord(ra=self.radec_position.ra, dec=self.radec_position.dec, distance=Distance(parallax=target_parallax), pm_ra_cosdec=target_pmra, pm_dec=target_pmdec, frame='icrs', equinox="J2000", obstime="J2000") self.radec_position_after_pm = target_coord.apply_space_motion( new_obstime=Time(date_obs)) return self.radec_position_after_pm
def best_gaia_entry(ra: float, dec: float, mjd: float, entries: Table): """ Using the originally supplied ra and dec choose the closest entries (propagating all entries in time to match current ra and dec at time = 'mjd') :param ra: float, the right ascension in degrees :param dec: float, the declination in degrees :param mjd: float, the modified julien date for observation :param entries: :return: """ # get the original coords in SkyCoord ocoord = SkyCoord(ra, dec, unit='deg') # get gaia time and observation time gaia_time = Time('2015.5', format='decimalyear') obs_time = Time(mjd, format='mjd') # get entries as numpy arrays (with units) ra_arr = np.array(entries['ra']) * uu.deg dec_arr = np.array(entries['dec']) * uu.deg pmra_arr = np.array(entries['pmra']) * uu.mas / uu.yr pmde_arr = np.array(entries['pmde']) * uu.mas / uu.yr plx_arr = np.array(entries['plx']) * uu.mas # propagate all entries ra and dec to mjd coords0 = SkyCoord(ra_arr, dec_arr, pm_ra_cosdec=pmra_arr, pm_dec=pmde_arr, distance=Distance(parallax=plx_arr), obstime=gaia_time) # apply space motion coords1 = coords0.apply_space_motion(obs_time) # crossmatch with ra and dec and keep closest separation = coords1.separation(ocoord) # find the position of the minimum separated value position = np.argmin(separation.value) # return the position return position
def wrapper(*args, _skip_decorator=False, **kwargs): """Wrapper docstring. Other Parameters ---------------- _skip_decorator : bool, optional Whether to skip the decorator. default {_skip_decorator} Notes ----- The wrapper """ # whether to skip decorator or keep going if _skip_decorator: return function(*args, **kwargs) # else: ba = sig.bind_partial_with_defaults(*args, **kwargs) obstime = ba.arguments.get("obstime", None) # print("obstime: ", obstime) # Catalog munging for name, info in catalogs.items(): catalog = ba.arguments[name] # get obstime if not provided or determined if obstime is None and hasattr(catalog, "obstime"): obstime = getattr(catalog, "obstime") # print(obstime) # TODO report this somehow # Step 1, convert to correct output type # TODO use registry for this cat_dtype = info["dtype"] # output type # TODO support passing multiple types if cat_dtype is BCFrame: if isinstance( # note, works if subclass catalog, (BCFrame, SkyCoord)): pass elif isinstance(catalog, Table): catalog = xdg.get_transform(Table, SkyCoord)(catalog) # elif: else: raise TypeError((f"Unknown conversion {type(catalog)} " "-> BaseCoordinateFrame.")) elif isinstance(cat_dtype, SkyCoord): if obstime is None and hasattr(catalog, "obstime"): obstime = getattr(catalog, "obstime") if isinstance(catalog, SkyCoord): # note, works if subclass pass elif isinstance(catalog, BCFrame): catalog = xdg.get_transform(BCFrame, SkyCoord)(catalog) elif isinstance(catalog, Table): catalog = xdg.get_transform(Table, SkyCoord)(catalog) # elif isinstance(cat_dtype, BCFrame): # a specific Frame class else: raise Exception("TODO") # Step 2, evolve observations by `obstime` try: new_catalog = SkyCoord.apply_space_motion(catalog, obstime) except Exception as e: # can't evolve # need to check that this is not a problem if hasattr(catalog, "obstime"): if catalog.obstime != obstime: # Uh oh!, it is raise Exception((f"epoch mismatch: catalog {name}'s " "coordinates cannot be transformed " f"to match the epoch {obstime} " f"(Exception {e}).")) else: pass # the epochs match # TODO, do as warning instead # print(f"Not adjusting catalog {name} ({e})") new_catalog = catalog # reassign ba.arguments[name] = new_catalog # /def # ---------------------------------- return_ = function(*ba.args, **ba.kwargs) # replace_catalogs = {} # TODO implement when have "return" working # for name, info in catalogs.items(): # if info.get("return", False): # get munged data # replace_catalogs[name] = ba.arguments[name] # if replace_catalogs: # it's not empty # return return_, replace_catalogs return return_
def find_object_in_catalog(image, db_address, gaia_class, simbad_class): """ Find the object in external catalogs. Update the ra and dec if found. Also add an initial classification if found. :return: """ # Assume that the equinox and input epoch are both j2000. # Gaia uses an equinox of 2000, but epoch of 2015.5 for the proper motion coordinate = SkyCoord(ra=image.ra, dec=image.dec, unit=(units.deg, units.deg), frame='icrs', pm_ra_cosdec=image.pm_ra * units.mas / units.year, pm_dec=image.pm_dec * units.mas / units.year, equinox='j2000', obstime=Time(2000.0, format='decimalyear')) transformed_coordinate = coordinate.apply_space_motion( new_obstime=Time(2015.5, format='decimalyear')) with warnings.catch_warnings(): warnings.simplefilter("ignore") # 10 arcseconds should be a large enough radius to capture bright objects. gaia = import_utils.import_attribute(gaia_class) gaia_connection = gaia() gaia_connection.ROW_LIMIT = 200 results = gaia_connection.query_object( coordinate=transformed_coordinate, radius=10.0 * units.arcsec) # Filter out objects fainter than r=12 and brighter than r = 5. # There is at least one case (gamma cas) that is in gaia but does not have a complete catalog record like proper # motions and effective temperatures. results = results[np.logical_and(results['phot_rp_mean_mag'] < 12.0, results['phot_rp_mean_mag'] > 5.0)] if len(results) > 0: # convert the luminosity from the LSun units that Gaia provides to cgs units results[0]['lum_val'] *= constants.L_sun.to('erg / s').value image.classification = dbs.get_closest_HR_phoenix_models( db_address, results[0]['teff_val'], results[0]['lum_val']) # Update the ra and dec to the catalog coordinates as those are basically always better than a user enters # manually. image.ra, image.dec = results[0]['ra'], results[0]['dec'] if results[0]['pmra'] is not np.ma.masked: image.pm_ra, image.pm_dec = results[0]['pmra'], results[0]['pmdec'] # If nothing in Gaia fall back to simbad. This should only be for stars that are brighter than mag = 3 else: # IMPORTANT NOTE: # During e2e tests we do not import astroquery.simbad.Simbad. We import a mocked simbad call # which can be found in banzai_nres.tests.utils.MockSimbad . This returns a simbad table that is # truncated. If you add a new votable field, you will need to add it to the mocked table as well. simbad = import_utils.import_attribute(simbad_class) simbad_connection = simbad() simbad_connection.add_votable_fields('pmra', 'pmdec', 'fe_h', 'otype') try: results = simbad_connection.query_region(coordinate, radius='0d0m10s') except astroquery.exceptions.TableParseError: response = simbad_connection.last_response.content logger.error( f'Error querying SIMBAD. Response from SIMBAD: {response}', image=image) results = [] if results: results = remove_planets_from_simbad(results) results = results[0] # get the closest source. image.classification = dbs.get_closest_phoenix_models( db_address, results['Fe_H_Teff'], results['Fe_H_log_g'])[0] # note that we always assume the proper motions are in mas/yr... which they should be. if results['PMRA'] is not np.ma.masked: image.pm_ra, image.pm_dec = results['PMRA'], results['PMDEC'] # Update the ra and dec to the catalog coordinates as those will be consistent across observations. # Simbad always returns h:m:s, d:m:s, for ra, dec. If for some reason simbad does not, these coords will be # very wrong and barycenter correction will be very wrong. coord = SkyCoord(results['RA'], results['DEC'], unit=(units.hourangle, units.deg)) image.ra, image.dec = coord.ra.deg, coord.dec.deg
def match_kc19_to_2rxs(max_sep=12 * u.arcsec): """ 2RXS has RA/dec of ROSAT sources in J2000. Cut on the ROSAT detection likelihood to be >=9. Following the Boller et al (2016) abstract, this is "conservative". Cut on the "Sflag' screening flag (0=good, not equal to 0 = screening flag set). https://ui.adsabs.harvard.edu/abs/2016A%26A...588A.103B/abstract """ csvpath = '../data/kc19_to_2rxs_within_{}_arcsec.csv'.format(max_sep.value) if not os.path.exists(csvpath): # # Get 2RXS sources. From Boller+2016, they were taken with the PSPC between # June 1990 and Aug 1991. # with fits.open("../data/Boller_2016_2RXS_ROSAT_sources.fits") as hdul: t = Table(hdul[1].data) sel = (t['ExiML'] >= 9) & (t['Sflag'] == 0) t = t[sel] rosat_coord = SkyCoord(nparr(t['_RAJ2000']), nparr(t['_DEJ2000']), frame='icrs', unit=(u.deg, u.deg), equinox=Time(2000.0, format='jyear'), obstime=Time(1991.0, format='jyear')) # # Get Kounkel & Covey 2019 sources with gaia info crossmatched. Set an # annoyingly high 1" absolute tolerance on the ra/dec match between what I # got from gaiadr2.source table, and what was listed in KC19 table 1. Use # the gaiadr2.source ra/dec (in this case, "_x", not "_y"). rtol is set as # what it needed to be to pass. Set the correct equinox and estimate of # observing time by the Gaia satellite, and include the proper motions. # df = pd.read_csv('../data/kounkel_table1_sourceinfo.csv') for k_x, k_y in zip(['ra_x', 'dec_x'], ['ra_y', 'dec_y']): np.testing.assert_allclose(nparr(df[k_x]), nparr(df[k_y]), rtol=3e-2, atol=1 / 3600) kc19_coord = SkyCoord(nparr(df.ra_x), nparr(df.dec_x), frame='icrs', unit=(u.deg, u.deg), pm_ra_cosdec=nparr(df.pmra) * u.mas / u.yr, pm_dec=nparr(df.pmdec) * u.mas / u.yr, equinox=Time(2015.5, format='jyear'), obstime=Time(2015.5, format='jyear')) kc19_coord_rosat_epoch = kc19_coord.apply_space_motion( new_obstime=Time(1991.0, format='jyear')) # # For each ROSAT coordinate, get the nearest KC2019 match. Ensure they are # each in the same equinox system (J2000.0). Examine the separation # distribution of the match. # idx, d2d, _ = rosat_coord.match_to_catalog_sky( kc19_coord_rosat_epoch.transform_to(rosat_coord)) sel = (d2d < 1000 * u.arcsec) # ~1/3rd of a degree bins = np.logspace(-2, 3, 11) plt.close('all') f, ax = plt.subplots(figsize=(4, 3)) ax.hist(d2d[sel].to(u.arcsec).value, bins=bins, cumulative=False, color='black', fill=False, linewidth=0.5) format_ax(ax) ax.set_xlabel('2RXS to nearest KC19 source [arcsec]') ax.set_ylabel('Number per bin') ax.set_xscale('log') ax.set_yscale('log') f.tight_layout(pad=0.2) outpath = '../results/crossmatching/kc19_to_2rxs_separation.png' savefig(f, outpath) # # The above shows a hint of a bump at like ~10 arcsec. This is roughly the # level of positional uncertainty expected from ROSAT (Ayres+2004, # https://ui.adsabs.harvard.edu/abs/2004ApJ...608..957A/abstract). 6" was # the design specification accuracy. Seems like 12" (2x the expected # positional uncertainty) is a decent place to set the cut. It will give # ~1.5k matches. # sep_constraint = (d2d < max_sep) rosat_matches = t[sep_constraint] kc19_match_df = df.iloc[idx[sep_constraint]] kc19_match_df['has_2RXS_match'] = 1 kc19_match_df['2RXS_match_dist_arcsec'] = (d2d[sep_constraint].to( u.arcsec).value) kc19_match_df['2RXS_name'] = rosat_matches['_2RXS'] kc19_match_df['2RXS_ExiML'] = rosat_matches['ExiML'] kc19_match_df.to_csv(csvpath, index=False) print('made {}'.format(csvpath)) else: kc19_match_df = pd.read_csv(csvpath) df = pd.read_csv('../data/kounkel_table1_sourceinfo.csv') return kc19_match_df, df
def run_astrometry(image2d, mask2d, saturpix, header, no_reuse_gaia, maxfieldview_arcmin, fieldfactor, pvalues, nightdir, output_fname, setupdata, interactive, logfile, debug=False): """ Compute astrometric solution of image. Note that the input parameter header is modified in output. Parameters ---------- image2d : numpy 2D array Image to be calibrated. mask2d : numpy 2D array Useful region mask. saturpix : numpy 2D array Array storing the location of saturated pixels in the raw data. header: astropy header Initial header of the image prior to the astrometric calibration. no_reuse_gaia : bool If True, previous GAIA data is not reused to perform the initial astrometric calibration with Astrometry.net. maxfieldview_arcmin : float Maximum field of view. This is necessary to retrieve the GAIA data for the astrometric calibration. fieldfactor : float Multiplicative factor to enlarge the required field of view in order to facilitate the reuse of the downloaded GAIA data. pvalues : list of int Possible P values for build-astrometry-index (scale number) in the order to be employed (if one fails, the next one is used). See help of build-astrometry-index for details. nightdir : str or None Directory where the reduced images will be stored. output_fname : str or None Output file name. setupdata : dict Setup data stored as a Python dictionary. interactive : bool or None If True, enable interactive execution (e.g. plots,...). logfile : instance of ToLogFile Logfile to store reduction information. debug : bool or None Display additional debugging information. Returns ------- ierr_astr : int Error status value. 0: no error. 1: error while performing astrometric calibration. astrsumm1 : instance of AstrSumm Summary of the astrometric calibration with Astronomy.net. astrsumm2 : instance of AstrSumm Summary of the astrometric calibration with AstrOmatic.net. """ ierr_astr = 0 astrsumm1 = None astrsumm2 = None # creating work subdirectory workdir = nightdir + '/work' if not os.path.isdir(workdir): os.makedirs(workdir) else: filelist = glob.glob('{}/*'.format(workdir)) logfile.print('\nRemoving previous files: {}'.format(filelist)) for filepath in filelist: try: os.remove(filepath) except: logfile.print("Error while deleting file : ", filepath) # define ToLogFile object logfile.print('\nAstrometric calibration of {}'.format(output_fname)) # define CmdExecute object cmd = CmdExecute(logfile) # generate myastrometry.cfg cfgfile = '{}/myastrometry.cfg'.format(workdir) with open(cfgfile, 'wt') as f: f.write('add_path .\nindex index-image') logfile.print('Creating configuration file {}'.format(cfgfile)) # remove deprecated WCS keywords: for kwd in ['pc001001', 'pc001002', 'pc002001', 'pc002002']: if kwd in header: del header[kwd] # rename deprecated RADECSYS as RADESYS if 'RADECSYS' in header: header.rename_keyword('RADECSYS', 'RADESYS') # RA, DEC, and DATE-OBS from the image header ra_initial = header['ra'] dec_initial = header['dec'] dateobs = header['date-obs'] c_fk5_dateobs = SkyCoord(ra=ra_initial * u.degree, dec=dec_initial * u.degree, frame='fk5', equinox=Time(dateobs)) c_fk5_j2000 = c_fk5_dateobs.transform_to(FK5(equinox='J2000')) logfile.print('Central coordinates:') logfile.print(str(c_fk5_dateobs)) logfile.print(str(c_fk5_j2000)) ra_center = c_fk5_j2000.ra.deg * np.pi / 180 dec_center = c_fk5_j2000.dec.deg * np.pi / 180 xj2000 = np.cos(ra_center) * np.cos(dec_center) yj2000 = np.sin(ra_center) * np.cos(dec_center) zj2000 = np.sin(dec_center) # read JSON file with central coordinates of fields already calibrated jsonfname = '{}/central_pointings.json'.format(nightdir) if os.path.exists(jsonfname): with open(jsonfname) as jfile: ccbase = json.load(jfile) else: ccbase = dict() # decide whether new GAIA data is needed retrieve_new_gaia_data = True indexid = None if no_reuse_gaia: logfile.print( '-> Forcing downloading of GAIA catalogue close the field pointing' ) else: nindices = len(ccbase) if nindices > 0: dist_arcmin_min = None for i, ikey in enumerate(ccbase): x = ccbase[ikey]['x'] y = ccbase[ikey]['y'] z = ccbase[ikey]['z'] search_radius_arcmin = ccbase[ikey]['search_radius_arcmin'] # angular distance (radians) dotprodcut = x * xj2000 + y * yj2000 + z * zj2000 if abs( dotprodcut ) > 1: # avoid RuntimeWarning when dotproduct = 1.0000000000000002 dist_rad = 0.0 else: dist_rad = np.arccos(x * xj2000 + y * yj2000 + z * zj2000) # angular distance (arcmin) dist_arcmin = dist_rad * 180 / np.pi * 60 if (maxfieldview_arcmin / 2) + dist_arcmin < search_radius_arcmin: if dist_arcmin_min is None: dist_arcmin_min = dist_arcmin indexid = int(ikey[-6:]) else: if dist_arcmin < dist_arcmin_min: dist_arcmin_min = dist_arcmin indexid = int(ikey[-6:]) if indexid is not None: logfile.print( '-> Reusing previously downloaded GAIA catalogue (indexid={})'. format(indexid)) retrieve_new_gaia_data = False else: logfile.print( '-> No previous GAIA catalogue found close the field pointing') if retrieve_new_gaia_data: indexid = len(ccbase) + 1 # create index subdir subdir = 'index{:06d}'.format(indexid) # create path to subdir newsubdir = nightdir + '/' + subdir if retrieve_new_gaia_data: # check that directory for the new index does not exist if not os.path.isdir(newsubdir): logfile.print( 'Subdirectory {} not found. Creating it!'.format(newsubdir)) os.makedirs(newsubdir) else: msg = 'ERROR: subdirectory {} already exists'.format(newsubdir) raise SystemError(msg) # generate additional logfile for retrieval of GAIA data loggaianame = '{}/gaialog.log'.format(newsubdir) loggaia = open(loggaianame, 'wt') logfile.print('-> Creating {}'.format(loggaianame)) loggaia.write('Querying GAIA data...\n') # generate query for GAIA search_radius_arcmin = fieldfactor * (maxfieldview_arcmin / 2) search_radius_degree = search_radius_arcmin / 60 # loop in phot_g_mean_mag # --- mag_minimum = 0 gaia_query_line, tap_result = retrieve_gaia(c_fk5_j2000.ra.deg, c_fk5_j2000.dec.deg, search_radius_degree, mag_minimum, loggaia) if tap_result is None: nobjects_mag_minimum = 0 else: nobjects_mag_minimum = len(tap_result) logfile.print('-> Gaia data: magnitude, nobjects: {:.3f}, {}'.format( mag_minimum, nobjects_mag_minimum)) if nobjects_mag_minimum >= NMAXGAIA: raise SystemError('Unexpected') # --- mag_maximum = 30 gaia_query_line, tap_result = retrieve_gaia(c_fk5_j2000.ra.deg, c_fk5_j2000.dec.deg, search_radius_degree, mag_maximum, loggaia) if tap_result is None: nobjects_mag_maximum = 0 else: nobjects_mag_maximum = len(tap_result) logfile.print('-> Gaia data: magnitude, nobjects: {:.3f}, {}'.format( mag_maximum, nobjects_mag_maximum)) if nobjects_mag_maximum < NMAXGAIA: loop_in_gaia = False else: loop_in_gaia = True # --- niter = 0 nitermax = 50 while loop_in_gaia: niter += 1 loggaia.write('Iteration {}\n'.format(niter)) mag_medium = (mag_minimum + mag_maximum) / 2 gaia_query_line, tap_result = retrieve_gaia( c_fk5_j2000.ra.deg, c_fk5_j2000.dec.deg, search_radius_degree, mag_medium, loggaia) if tap_result is None: msg = 'WARNING: unable to retrieve GAIA data (tap_result is None)' logfile.print(msg) else: nobjects = len(tap_result) logfile.print( '-> Gaia data: magnitude, nobjects: {:.3f}, {}'.format( mag_medium, nobjects)) if nobjects < NMAXGAIA: if mag_maximum - mag_minimum < 0.1: loop_in_gaia = False else: mag_minimum = mag_medium else: mag_maximum = mag_medium if niter > nitermax: loggaia.write( 'ERROR: nitermax reached while retrieving GAIA data') loop_in_gaia = False if tap_result is None: raise SystemError( 'FATAL ERROR: unable to retrieve GAIA data (tap_result is None; check http connection)' ) loggaia.write(str(tap_result.to_table()) + '\n') loggaia.close() logfile.print('Querying GAIA data: {} objects found'.format( len(tap_result))) # proper motion correction logfile.print('-> Applying proper motion correction...') source_id = [] ra_corrected = [] dec_corrected = [] phot_g_mean_mag = [] for irecord, record in enumerate(tap_result): source_id.append(record['source_id']) phot_g_mean_mag.append(record['phot_g_mean_mag']) ra, dec = record['ra'], record['dec'] pmra, pmdec = record['pmra'], record['pmdec'] ref_epoch = record['ref_epoch'] if not np.isnan(pmra) and not np.isnan(pmdec): t0 = Time(ref_epoch, format='decimalyear') c = SkyCoord(ra=ra * u.degree, dec=dec * u.degree, pm_ra_cosdec=pmra * u.mas / u.yr, pm_dec=pmdec * u.mas / u.yr, obstime=t0) dt = Time(dateobs) - t0 c_corrected = c.apply_space_motion(dt=dt.jd * u.day) if debug: print(irecord, ra, c_corrected.ra.value, dec, c_corrected.dec.value) ra_corrected.append(c_corrected.ra.value) dec_corrected.append(c_corrected.dec.value) else: ra_corrected.append(ra) dec_corrected.append(dec) # save GAIA objects in FITS binary table hdr = fits.Header() hdr.add_history('GAIA objets selected with following query:') hdr.add_history(gaia_query_line) hdr.add_history('---') hdr.add_history( 'Note that RA and DEC have been corrected from proper motion') primary_hdu = fits.PrimaryHDU(header=hdr) col1 = fits.Column(name='source_id', format='K', array=source_id) col2 = fits.Column(name='ra', format='D', array=ra_corrected) col3 = fits.Column(name='dec', format='D', array=dec_corrected) col4 = fits.Column(name='phot_g_mean_mag', format='E', array=phot_g_mean_mag) hdu = fits.BinTableHDU.from_columns([col1, col2, col3, col4]) hdul = fits.HDUList([primary_hdu, hdu]) outfname = nightdir + '/' + subdir + '/GaiaDR2-query.fits' hdul.writeto(outfname, overwrite=True) logfile.print('-> Saving {}'.format(outfname)) # update JSON file with central coordinates of fields already calibrated ccbase[subdir] = { 'ra': c_fk5_j2000.ra.degree, 'dec': c_fk5_j2000.dec.degree, 'x': xj2000, 'y': yj2000, 'z': zj2000, 'search_radius_arcmin': search_radius_arcmin } with open(jsonfname, 'w') as outfile: json.dump(ccbase, outfile, indent=2) else: # check that directory with the old index does exist if os.path.isdir(newsubdir): logfile.print('Subdirectory {} found'.format(newsubdir)) else: msg = 'ERROR: subdirectory {} does not exist!' raise SystemError(msg) command = 'cp {}/{}/GaiaDR2-query.fits {}/work/'.format( nightdir, subdir, nightdir) cmd.run(command) # image dimensions naxis2, naxis1 = image2d.shape # save temporary FITS file tmpfname = '{}/xxx.fits'.format(workdir) header.add_history('--Computing Astrometry.net WCS solution--') hdu = fits.PrimaryHDU(image2d.astype(np.float32), header) hdu.writeto(tmpfname, overwrite=True) logfile.print('\nGenerating reduced image {}/xxx.fits (after bias ' 'subtraction and flatfielding)\n'.format(workdir)) logfile.print('\n*** Using Astrometry.net tools ***') ip = 0 loop = True while loop: # generate index file with GAIA data command = 'build-astrometry-index -i GaiaDR2-query.fits' command += ' -o index-image.fits' command += ' -A ra -D dec -S phot_g_mean_mag' command += ' -P {}'.format(pvalues[ip]) command += ' -E -I {}'.format(indexid) cmd.run(command, cwd=workdir) # solve fieldmormo command = 'solve-field -p' command += ' --config myastrometry.cfg --overwrite' command += ' --ra ' + str(c_fk5_j2000.ra.degree) command += ' --dec ' + str(c_fk5_j2000.dec.degree) command += ' --radius {}'.format(maxfieldview_arcmin / 120) command += ' xxx.fits' cmd.run(command, cwd=workdir) # check that the field solved if not os.path.isfile('{}/xxx.solved'.format(workdir)): logfile.print('WARNING: field did not solve.') if ip < len(pvalues) - 1: logfile.print('WARNING: trying with new P value.') ip += 1 else: ierr_astr = 1 msg = 'Unable to solve the field with Astrometry.net' logfile.print(msg) header.add_history(msg) hdu = fits.PrimaryHDU(image2d.astype(np.float32), header) hdu.writeto(output_fname, overwrite=True) logfile.print('-> file {} created'.format(output_fname)) save_auxfiles(output_fname=output_fname, nightdir=nightdir, workdir=workdir, logfile=logfile) return ierr_astr, astrsumm1, astrsumm2 else: loop = False # check for saturated objects with fits.open('{}/xxx.axy'.format(workdir), 'update') as hdul_table: tbl = hdul_table[1].data isaturated = [] for i in range(tbl.shape[0]): ix = int(tbl['X'][i] + 0.5) iy = int(tbl['Y'][i] + 0.5) if saturpix[iy - 1, ix - 1]: isaturated.append(i) logfile.print('Checking file: {}/xxx.axy'.format(workdir)) logfile.print('Number of saturated objects found: {}/{}'.format( len(isaturated), tbl.shape[0])) if len(isaturated) > 0: for i in isaturated: logfile.print('Saturated object: {}'.format(tbl[i])) if len(isaturated) > 0: hdul_table[1].data = np.delete(tbl, isaturated) logfile.print('File: {}/xxx.axy updated\n'.format(workdir)) if len(isaturated) > 0: # rerun code command = 'solve-field -p' command += ' --config myastrometry.cfg --continue' command += ' --width {} --height {}'.format(naxis1, naxis2) command += ' --x-column X --y-column Y --sort-column FLUX' command += ' --ra ' + str(c_fk5_j2000.ra.degree) command += ' --dec ' + str(c_fk5_j2000.dec.degree) command += ' --radius {}'.format(maxfieldview_arcmin / 120) command += ' xxx.axy' cmd.run(command, cwd=workdir) # check that the field solved if not os.path.isfile('{}/xxx.solved'.format(workdir)): ierr_astr = 1 msg = 'Unable to solve the field with Astrometry.net' logfile.print(msg) header.add_history(msg) hdu = fits.PrimaryHDU(image2d.astype(np.float32), header) hdu.writeto(output_fname, overwrite=True) logfile.print('-> file {} created'.format(output_fname)) save_auxfiles(output_fname=output_fname, nightdir=nightdir, workdir=workdir, logfile=logfile) return ierr_astr, astrsumm1, astrsumm2 # insert new WCS into image header command = 'new-wcs -i xxx.fits -w xxx.wcs -o xxx.new -d' cmd.run(command, cwd=workdir) # read GaiaDR2 table and convert RA, DEC to X, Y # (note: the same result can be accomplished using the command-line program: # $ wcs-rd2xy -w xxx.wcs -i GaiaDR2-query.fits -o gaia-xy.fits) with fits.open('{}/GaiaDR2-query.fits'.format(workdir)) as hdul_table: gaiadr2 = hdul_table[1].data with fits.open('{}/xxx.new'.format(workdir)) as hdul: w = WCS(hdul[0].header) try: xgaia, ygaia = w.all_world2pix(gaiadr2.ra, gaiadr2.dec, 1) except NoConvergence: msg = 'WARNING: NoConvergence exception in WCS.all_world2pix() call (using quiet=True instead)' print(msg) xgaia, ygaia = w.all_world2pix(gaiadr2.ra, gaiadr2.dec, 1, quiet=True) # compute pixel scale (mean in both axis) in arcsec/pix pixel_scales_arcsec_pix = proj_plane_pixel_scales(w) * 3600 logfile.print('astrometry.net> pixel scales (arcsec/pix): {}'.format( pixel_scales_arcsec_pix)) # load corr file corrfname = '{}/xxx.corr'.format(workdir) with fits.open(corrfname) as hdul_table: tcorr = hdul_table[1].data # generate plots astrsumm1 = plot_astrometry( output_fname=output_fname, image2d=image2d, mask2d=mask2d, peak_x=tcorr.field_x, peak_y=tcorr.field_y, pred_x=tcorr.index_x, pred_y=tcorr.index_y, xcatag=xgaia, ycatag=ygaia, pixel_scales_arcsec_pix=pixel_scales_arcsec_pix, workdir=workdir, interactive=interactive, logfile=logfile, suffix='net') # open result and update header result_fname = '{}/xxx.new'.format(workdir) with fits.open(result_fname) as hdul: newheader = hdul[0].header # copy configuration files for astrometric.net logfile.print('\n*** Using AstrOmatic.net tools ***') conffiles = ['default.param', 'config.sex', 'config.scamp'] for fname in conffiles: keyfname = fname.replace('.', '_') if keyfname in setupdata: initfname = setupdata[keyfname] if initfname[0] != '/': initfname = os.getcwd() + '/' + initfname if os.path.exists(initfname): command = 'cp {} {}/'.format(initfname, workdir) cmd.run(command) else: raise SystemError( 'The file {} given in setup_filabres.yaml does not exist!'. format(initfname)) else: dumdata = pkgutil.get_data('filabres.astromatic', fname) txtfname = '{}/{}'.format(workdir, fname) logfile.print('Generating {}'.format(txtfname)) with open(txtfname, 'wt') as f: f.write(str(dumdata.decode('utf8'))) logfile.print(' ') # run sextractor command = 'sex xxx.new -c config.sex -CATALOG_NAME xxx.ldac' cmd.run(command, cwd=workdir) # run scamp command = 'scamp xxx.ldac -c config.scamp' cmd.run(command, cwd=workdir) # check there is a useful result if os.path.exists('{}/xxx.head'.format(workdir)): with open('{}/xxx.head'.format(workdir)) as fdum: singleline = fdum.read() if 'PV2_10' not in singleline: ierr_astr = 2 else: ierr_astr = 2 if ierr_astr == 2: msg = 'Unable to solve the field with AstrOmatic.net' logfile.print(msg) newheader.add_history(msg) newheader[ 'history'] = '-------------------------------------------------------' newheader[ 'history'] = 'Summary of astrometric calibration with Astrometry.net:' newheader['history'] = '- pixscale: {}'.format(astrsumm1.pixscale) newheader['history'] = '- ntargets: {}'.format(astrsumm1.ntargets) newheader['history'] = '- meanerr: {}'.format(astrsumm1.meanerr) newheader[ 'history'] = '-------------------------------------------------------' hdu = fits.PrimaryHDU(image2d.astype(np.float32), newheader) hdu.writeto(output_fname, overwrite=True) logfile.print('-> file {} created'.format(output_fname)) save_auxfiles(output_fname=output_fname, nightdir=nightdir, workdir=workdir, logfile=logfile) return ierr_astr, astrsumm1, astrsumm2 # remove SIP parameters in newheader newheader['history'] = '--Deleting SIP from Astrometry.net WCS solution--' newheader.add_comment('--Deleted SIP from Astrometry.net WCS solution--') sip_param = [] for p in ['', 'P']: for c in ['A', 'B']: sip_param += ['{}{}_ORDER'.format(c, p)] sip_param += [ '{}{}_{}_{}'.format(c, p, i, j) for i in range(3) for j in range(3) if i + j < 3 ] for kwd in sip_param: kwd_value = newheader[kwd] kwd_comment = newheader.comments[kwd] newheader['comment'] = 'deleted {:8} = {:20} / {}'.format( kwd, kwd_value, kwd_comment) del newheader[kwd] # remove HISTORY and COMMENT entries from astrometry.net with fits.open('{}/xxx.wcs'.format(workdir)) as hdul: oldheader = hdul[0].header for kwd in ['HISTORY', 'COMMENT']: for itemval in oldheader[kwd]: try: idel = list(newheader.values()).index(itemval) except ValueError: idel = -1 if idel > -1: del newheader[idel] # delete additional comment lines tobedeleted = [ 'Original key: "END"', '--Start of Astrometry.net WCS solution--', '--Put in by the new-wcs program--', '--End of Astrometry.net WCS--', '--(Put in by the new-wcs program)--' ] for item in tobedeleted: try: idel = list(newheader.values()).index(item) except ValueError: idel = -1 if idel > -1: del newheader[idel] # remove blank COMMENTS idel = 0 while idel > -1: try: idel = list(newheader.values()).index('') except ValueError: idel = -1 if idel > -1: del newheader[idel] # set the TPV solution obtained with sextractor+scamp newheader['history'] = '--Computing new solution with SEXTRACTOR+SCAMP--' with open('{}/xxx.head'.format(workdir)) as tpvfile: tpvheader = tpvfile.readlines() for line in tpvheader: kwd = line[:8].strip() if kwd.find('END') > -1: break if kwd == 'COMMENT': pass # Avoid problem with non-standard ASCII characters elif kwd == 'HISTORY': newheader[kwd] = line[10:].rstrip() else: # note the blank spaces to avoid problem with "S/N" kwd_value, kwd_comment = line[11:].split(' / ') try: value = float(kwd_value.replace('\'', ' ')) except ValueError: value = kwd_value.replace('\'', ' ') newheader[kwd] = (value, kwd_comment.rstrip()) # set CTYPE1 and CTYPE2 from 'RA---TAN' and 'DEC--TAN' to 'RA---TPV' and 'DEC--TPV' newheader['CTYPE1'] = 'RA---TPV' newheader['CTYPE2'] = 'DEC--TPV' # load WCS computed with SCAMP w = WCS(newheader) # compute pixel scale (mean in both axis) in arcsec/pix pixel_scales_arcsec_pix = proj_plane_pixel_scales(w) * 3600 logfile.print('astrometry> pixel scales (arcsec/pix): {}'.format( pixel_scales_arcsec_pix)) # load peak location from catalogue peak_x, peak_y = load_scamp_cat('full', workdir, logfile) peak_ra, peak_dec = load_scamp_cat('merged', workdir, logfile) pred_x, pred_y = w.wcs_world2pix(peak_ra, peak_dec, 1) # predict expected location of GAIA data with fits.open('{}/GaiaDR2-query.fits'.format(workdir)) as hdul_table: gaiadr2 = hdul_table[1].data xgaia, ygaia = w.wcs_world2pix(gaiadr2.ra, gaiadr2.dec, 1) # generate plots astrsumm2 = plot_astrometry( output_fname=output_fname, image2d=image2d, mask2d=mask2d, peak_x=peak_x, peak_y=peak_y, pred_x=pred_x, pred_y=pred_y, xcatag=xgaia, ycatag=ygaia, pixel_scales_arcsec_pix=pixel_scales_arcsec_pix, workdir=workdir, interactive=interactive, logfile=logfile, suffix='scamp') # store astrometric summaries in history newheader[ 'history'] = '-------------------------------------------------------' newheader[ 'history'] = 'Summary of astrometric calibration with Astrometry.net:' newheader['history'] = '- pixscale: {}'.format(astrsumm1.pixscale) newheader['history'] = '- ntargets: {}'.format(astrsumm1.ntargets) newheader['history'] = '- meanerr: {}'.format(astrsumm1.meanerr) newheader[ 'history'] = '-------------------------------------------------------' newheader[ 'history'] = 'Summary of astrometric calibration with AstrOmatic.net:' newheader['history'] = '- pixscale: {}'.format(astrsumm2.pixscale) newheader['history'] = '- ntargets: {}'.format(astrsumm2.ntargets) newheader['history'] = '- meanerr: {}'.format(astrsumm2.meanerr) newheader[ 'history'] = '-------------------------------------------------------' # save result hdu = fits.PrimaryHDU(image2d.astype(np.float32), newheader) hdu.writeto(output_fname, overwrite=True) logfile.print('-> file {} created'.format(output_fname)) # storing relevant files in corresponding subdirectory save_auxfiles(output_fname=output_fname, nightdir=nightdir, workdir=workdir, logfile=logfile) return ierr_astr, astrsumm1, astrsumm2
def get_nearby_offset_stars(source_ra, source_dec, source_name, how_many=3, radius_degrees=2 / 60., mag_limit=18.0, mag_min=10.0, min_sep_arcsec=5, starlist_type='Keck', obstime=None, use_source_pos_in_starlist=True, allowed_queries=2, queries_issued=0): """Finds good list of nearby offset stars for spectroscopy and returns info about those stars, including their offsets calculated to the source of interest Parameters ---------- source_ra : float Right ascension (J2000) of the source source_dec : float Declination (J2000) of the source source_name : str Name of the source how_many : int, optional How many offset stars to try to find radius_degrees : float, optional Search radius from the source position in arcmin mag_limit : float, optional How faint should we search for offset stars? mag_min : float, optional What is the brightest offset star we will allow? min_sep_arcsec : float, optional What is the closest offset star allowed to the source? starlist_type : str, optional What starlist format should we use? obstime : str, optional What datetime (in isoformat) should we assume for the observation (to calculate proper motions)? use_source_pos_in_starlist : bool, optional Return the source itself for in starlist? allowed_queries : int, optional How many times should we query (with looser and looser criteria) before giving up on getting the number of offset stars we desire? queries_issued : int, optional How many times have we issued a query? Bookkeeping parameter. Returns ------- (list, str, int, int) Return a tuple which contains: a list of dictionaries for each object in the star list, the query issued, the number of queries issues, and the length of the star list (not including the source itself) """ if queries_issued >= allowed_queries: raise Exception( 'Number of offsets queries needed exceeds what is allowed') if not obstime: source_obstime = Time(datetime.datetime.utcnow().isoformat()) else: # TODO: check the obstime format source_obstime = Time(obstime) gaia_obstime = "J2015.5" center = SkyCoord(source_ra, source_dec, unit=(u.degree, u.degree), frame='icrs', obstime=source_obstime) # get three times as many stars as requested for now # and go fainter as well fainter_diff = 2.0 # mag search_multipler = 10 query_string = f""" SELECT TOP {how_many*search_multipler} DISTANCE( POINT('ICRS', ra, dec), POINT('ICRS', {source_ra}, {source_dec})) AS dist, source_id, ra, dec, ref_epoch, phot_rp_mean_mag, pmra, pmdec, parallax FROM gaiadr2.gaia_source WHERE 1=CONTAINS( POINT('ICRS', ra, dec), CIRCLE('ICRS', {source_ra}, {source_dec}, {radius_degrees})) AND phot_rp_mean_mag < {mag_limit + fainter_diff} AND phot_rp_mean_mag > {mag_min} AND parallax < 250 ORDER BY phot_rp_mean_mag ASC """ # TODO possibly: save the offset data (cache) job = Gaia.launch_job(query_string) r = job.get_results() queries_issued += 1 catalog = SkyCoord.guess_from_table(r) # star needs to be this far away # from another star min_sep = min_sep_arcsec * u.arcsec good_list = [] for source in r: c = SkyCoord(ra=source["ra"], dec=source["dec"], unit=(u.degree, u.degree), pm_ra_cosdec=(np.cos(source["dec"] * np.pi / 180.0) * source['pmra'] * u.mas / u.yr), pm_dec=source["pmdec"] * u.mas / u.yr, frame='icrs', distance=min(abs(1 / source["parallax"]), 10) * u.kpc, obstime=gaia_obstime) d2d = c.separation(catalog) # match it to the catalog if sum(d2d < min_sep) == 1 and source["phot_rp_mean_mag"] <= mag_limit: # this star is not near another star and is bright enough # precess it's position forward to the source obstime and # get offsets suitable for spectroscopy # TODO: put this in geocentric coords to account for parallax cprime = c.apply_space_motion(new_obstime=source_obstime) dra, ddec = cprime.spherical_offsets_to(center) good_list.append((source["dist"], c, source, dra.to(u.arcsec), ddec.to(u.arcsec))) good_list.sort() # if we got less than we asked for, relax the criteria if (len(good_list) < how_many) and (queries_issued < allowed_queries): return get_nearby_offset_stars( source_ra, source_dec, source_name, how_many=how_many, radius_degrees=radius_degrees * 1.3, mag_limit=mag_limit + 1.0, mag_min=mag_min - 1.0, min_sep_arcsec=min_sep_arcsec / 2.0, starlist_type=starlist_type, obstime=obstime, use_source_pos_in_starlist=use_source_pos_in_starlist, queries_issued=queries_issued, allowed_queries=allowed_queries) # default to keck star list sep = ' ' # 'fromunit' commentstr = "#" giveoffsets = True maxname_size = 16 # truncate the source_name if we need to if len(source_name) > 10: basename = source_name[0:3] + ".." + source_name[-6:] else: basename = source_name if starlist_type == 'Keck': pass elif starlist_type == 'P200': sep = ':' # 'fromunit' commentstr = "!" giveoffsets = False maxname_size = 20 # truncate the source_name if we need to if len(source_name) > 15: basename = source_name[0:3] + ".." + source_name[-11:] else: basename = source_name else: print("Warning: Do not recognize this starlist format. Using Keck.") basename = basename.strip().replace(" ", "") space = " " star_list_format = ( f"{basename:{space}<{maxname_size}} " + f"{center.to_string('hmsdms', sep=sep, decimal=False, precision=2, alwayssign=True)[1:]}" + f" 2000.0 {commentstr}") star_list = [] if use_source_pos_in_starlist: star_list.append({ "str": star_list_format, "ra": float(source_ra), "dec": float(source_dec), "name": basename }) for i, (dist, c, source, dra, ddec) in enumerate(good_list[:how_many]): dras = f"{dra.value:<0.03f}\" E" if dra > 0 else f"{abs(dra.value):<0.03f}\" W" ddecs = f"{ddec.value:<0.03f}\" N" if ddec > 0 else f"{abs(ddec.value):<0.03f}\" S" if giveoffsets: offsets = \ f"raoffset={dra.value:<0.03f} decoffset={ddec.value:<0.03f}" else: offsets = "" name = f"{basename}_off{i+1}" star_list_format = ( f"{name:{space}<{maxname_size}} " + f"{c.to_string('hmsdms', sep=sep, decimal=False, precision=2, alwayssign=True)[1:]}" + f" 2000.0 {offsets} " + f" {commentstr} dist={3600*dist:<0.02f}\"; {source['phot_rp_mean_mag']:<0.02f} mag" + f"; {dras}, {ddecs} " + f" ID={source['source_id']}") star_list.append({ "str": star_list_format, "ra": float(source["ra"]), "dec": float(source["dec"]), "name": name, "dras": dras, "ddecs": ddecs, "mag": float(source["phot_rp_mean_mag"]) }) # send back the starlist in return (star_list, query_string.replace("\n", " "), queries_issued, len(star_list) - 1)
def make_catalog(ra: float, dec: float, radius: float, year: float, outfile=None, return_data=False ) -> Union[int, Tuple[int, Table]]: """ Make a catalogue of Gaia/2MASS point sources based on a circle of center "ra" and "dec" of "radius" [arcsec] Output table has following columns index, ra, dec, kmag, kmag, kmag and is saved in "outfile" :param ra: float, the Right Ascension in degrees :param dec: float, the Declination in degrees :param radius: float, the field radius (in arc seconds) :param year: float, the decimal year (to propagate ra/dec with proper motion/plx) :return: returns position of target in source list table (if return_data is False else returns a tuple 1. the position of target in source list table, 2. the Table of sources centered on the ra/dec """ print('='*50) print('Field Catalog Generator') print('='*50) # log input parameters print('\nMaking catalog for field centered on:') print('\t RA: {0}'.format(ra)) print('\t DEC: {0}'.format(ra)) print('\n\tradius = {0} arcsec'.format(radius)) print('\tObservation date: {0}'.format(year)) # get observation time with warnings.catch_warnings(record=True) as _: obs_time = Time(year, format='decimalyear') # get center as SkyCoord coord_cent = SkyCoord(ra * uu.deg, dec * uu.deg) # ------------------------------------------------------------------------- # Query Gaia - need proper motion etc # ------------------------------------------------------------------------- # construct gaia query gaia_query = GAIA_QUERY.format(RA=ra, DEC=dec, RADIUS=radius/3600.0, GAIA_TWOMASS_ID=GAIA_TWOMASS_ID) # define gaia time gaia_time = Time('2015.5', format='decimalyear') # run Gaia query print('\nQuerying Gaia field\n') gaia_table = tap_query(GAIA_URL, gaia_query) # ------------------------------------------------------------------------- # Query 2MASS - need to get J, H and Ks mag # ------------------------------------------------------------------------- jmag, hmag, kmag, tmass_id = [], [], [], [] # now get 2mass magnitudes for each entry for row in range(len(gaia_table)): # log progress pargs = [row + 1, len(gaia_table)] print('Querying 2MASS source {0} / {1}'.format(*pargs)) # query 2MASS for magnitudes tmass_query = TWOMASS_QUERY.format(ID=gaia_table[GAIA_TWOMASS_ID][row], TWOMASS_ID=TWOMASS_ID) # run 2MASS query tmass_table = tap_query(TWOMASS_URL, tmass_query) # deal with no entry if tmass_query is None: jmag.append(np.nan) hmag.append(np.nan) kmag.append(np.nan) tmass_id.append('NULL') else: jmag.append(tmass_table['jmag'][0]) hmag.append(tmass_table['hmag'][0]) kmag.append(tmass_table['kmag'][0]) tmass_id.append(tmass_table[TWOMASS_ID][0]) # add columns to table gaia_table['JMAG'] = jmag gaia_table['HMAG'] = hmag gaia_table['KMAG'] = kmag gaia_table[TWOMASS_ID] = tmass_id # ------------------------------------------------------------------------- # Clean up table - remove all entries without 2MASS # ------------------------------------------------------------------------- # remove rows with NaNs in 2MASS magnitudes mask = np.isfinite(gaia_table['JMAG']) mask &= np.isfinite(gaia_table['HMAG']) mask &= np.isfinite(gaia_table['KMAG']) # mask table cat_table = gaia_table[mask] # ------------------------------------------------------------------------- # Apply space motion # ------------------------------------------------------------------------- # get entries as numpy arrays (with units) ra_arr = np.array(cat_table['ra']) * uu.deg dec_arr = np.array(cat_table['dec']) * uu.deg pmra_arr = np.array(cat_table['pmra']) * uu.mas/uu.yr pmde_arr = np.array(cat_table['pmde']) * uu.mas/uu.yr plx_arr = np.array(cat_table['plx']) * uu.mas # Get sky coords instance coords0 = SkyCoord(ra_arr, dec_arr, pm_ra_cosdec=pmra_arr, pm_dec=pmde_arr, distance=Distance(parallax=plx_arr), obstime=gaia_time) # apply space motion with warnings.catch_warnings(record=True) as _: coords1 = coords0.apply_space_motion(obs_time) # find our target source (closest to input) separation = coord_cent.separation(coords0) # sort rest by brightness order = np.argsort(cat_table['KMAG']) # get the source position (after ordering separation) # assume our source is closest to the center source_pos = int(np.argmin(separation[order])) # ------------------------------------------------------------------------- # make final table # ------------------------------------------------------------------------- # start table instance final_table = Table() # index column final_table['index'] = np.arange(len(coords1)) # ra column final_table[RA_OUTCOL] = coords1.ra.value[order] # dec column final_table[DEC_OUTCOL] = coords1.dec.value[order] # mag columns final_table[F380M_OUTCOL] = cat_table['KMAG'][order] final_table[F430M_OUTCOL] = cat_table['KMAG'][order] final_table[F480M_OUTCOL] = cat_table['KMAG'][order] # ------------------------------------------------------------------------- # deal with return data if return_data: return source_pos, final_table # ------------------------------------------------------------------------- # write file write_catalog(final_table, outfile) # return the position closest to the input coordinates return source_pos
if np.abs(starPmTot) / starPmTotE < 1.5: pmOK = False if pmOK: print('TIC has proper motion data. Getting 2015.5 position for GAIA') ctic = SkyCoord( ra=starRa * u.deg, dec=starDec * u.deg, pm_ra_cosdec=starPmRa * u.mas / u.yr, pm_dec=starPmDec * u.mas / u.yr, obstime=Time('J2000.0'), distance=1000.0 * u.pc, # Use fake distance just for skycoord functionality radial_velocity=0.0 * u.km / u.s) # Use fake rv " # convert position to GAIA DR2 2015.5 gaiaEpc = Time('J2015.5') ctic_gaia_epoch = ctic.apply_space_motion(gaiaEpc) gaiaPredRa = ctic_gaia_epoch.ra.degree gaiaPredDec = ctic_gaia_epoch.dec.degree else: gaiaPredRa = starRa gaiaPredDec = starDec gaiaPos = 'Predicted GAIA position {0:.6f} {1:.6f} [J2000.0; epoch 2015.5]'.format( gaiaPredRa, gaiaPredDec) # We are go for GAIA Cone search ADSQL_Str = "SELECT \ DISTANCE( POINT('ICRS', ra, dec),\ POINT('ICRS', {0}, {1}) ) AS dist, phot_g_mean_mag, teff_val, teff_percentile_lower, \ teff_percentile_upper, radius_val, radius_percentile_lower, radius_percentile_upper,\ astrometric_gof_al, astrometric_excess_noise_sig, phot_bp_mean_mag, phot_rp_mean_mag, bp_rp, \ a_g_val, e_bp_min_rp_val, parallax, \
print(len(result[0])) ############################################################################### # Now we load all stars into an array coordinate. The reference epoch for the # star positions is J2015.5, # so we update these positions to the date of the # COR2 observation using :meth:`astropy.coordinates.SkyCoord.apply_space_motion`. tbl_crds = SkyCoord(ra=result[0]['RA_ICRS'], dec=result[0]['DE_ICRS'], distance=Distance(parallax=u.Quantity(result[0]['Plx'])), pm_ra_cosdec=result[0]['pmRA'], pm_dec=result[0]['pmDE'], radial_velocity=result[0]['RV'], frame='icrs', obstime=Time(result[0]['Epoch'], format='jyear')) tbl_crds = tbl_crds.apply_space_motion(new_obstime=cor2.date) ############################################################################### # One of the bright features is actually Mars, so let's also get that coordinate. mars = get_body_heliographic_stonyhurst('mars', cor2.date, observer=cor2.observer_coordinate) ############################################################################### # Let's plot the results. The coordinates will be transformed automatically # when plotted using :meth:`~astropy.visualization.wcsaxes.WCSAxes.plot_coord`. ax = plt.subplot(projection=cor2) # Let's tweak the axis to show in degrees instead of arcsec lon, lat = ax.coords lon.set_major_formatter('d.dd')
def __init__(self, filename, search_radius = 5, target_ra = np.nan, target_dec = np.nan, dist = np.nan, boxsize = 13, hires_scale = 1, rotate_to = np.nan, normalize = False, query_simbad = False): """Load in an image, store some important parameters and perform initial image processing.""" with fits.open(filename) as fitsfile: self.image = fitsfile['image'].data * 1000 #image in mJy/pixel self.pfov = fitsfile['image'].header['CDELT2'] * 3600 #pixel FOV in arcsec self.wav = int(fitsfile['PRIMARY'].header['WAVELNTH']) #wavelength of observations self.level = int(fitsfile['PRIMARY'].header['LEVEL']) #processing level (20 or 25) self.name = fitsfile['PRIMARY'].header['OBJECT'] #target name self.angle = fitsfile['PRIMARY'].header['POSANGLE'] #pointing position angle #extract the obsid; the appropriate keyword seemingly depends on the processing level try: self.obsid = fitsfile['PRIMARY'].header['OBSID001'] #this works for level 2.5 except KeyError: self.obsid = fitsfile['PRIMARY'].header['OBS_ID'] #and this for level 2 #get the expected star coordinates in pixels, if RA and dec were provided; #otherwise, assume it's at the centre of the image if np.isnan(target_ra) or np.isnan(target_dec): star_expected = [i / 2 for i in self.image.shape] else: wcs = WCS(fitsfile['image'].header) star_expected = np.flip(wcs.wcs_world2pix([[target_ra, target_dec]], 0)[0]) #extract coverage level, so that we can estimate the rms flux in a suitable region cov = fitsfile['coverage'].data #refuse to analyse 160 micron data (70/100 is always available and generally at higher S/N) if self.wav != 70 and self.wav != 100: raise Exception(f"Please provide a 70 or 100 μm image ({filename} is at {self.wav} μm)") #factors to correct for flux lost during high-pass filtering (see Kennedy et al. 2012) if self.wav == 70: self.flux_factor = 1.16 elif self.wav == 100: self.flux_factor = 1.19 #if no distance is supplied, simply set d = 1 pc so that separations will be in arcsec, not au; #in_au can be stored in any saved output for future reference, and plots can be annotated with sep_unit, #which is intended to be embedded in a LaTeX string if np.isnan(dist): distance_provided = False dist = 1 self.sep_unit = r'^{\prime\prime}' self.in_au = False else: distance_provided = True self.sep_unit = r'\mathrm{au}' self.in_au = True #au per pixel at the distance of the target self.aupp = self.pfov * dist #clean up NaN pixels self.image[np.isnan(self.image)] = 0 cov[np.isnan(cov)] = 0 #find the coordinates of the brightest pixel within search_radius arcsec #of the specified RA and dec (or simply the centre) brightest_pix = self._find_brightest(search_radius, star_expected) #estimate the rms flux in a region defined by two conditions: coverage is above a #specified level, and projected separation from the brightest pixel is above a certain level. #NOTE: if the provided RA/dec are far from the image centre, the region defined by these #conditions may not be the most appropriate (however, it's unlikely that we will be trying #to fit a source near the edge of a map) cov_threshold_rms = 0.6 #fraction of max coverage sep_threshold_rms = 15 #arcsec sky_separation = self._projected_sep_array(brightest_pix) self.rms = self._estimate_background((cov > cov_threshold_rms * np.max(cov)) & (sky_separation > sep_threshold_rms)) #need to scale up uncertainties since noise is correlated natural_pixsize = 3.2 #always the case for PACS 70/100 micron images self.uncert = self.rms * self._correlated_noise_factor(natural_pixsize) if np.isnan(rotate_to): #no rotation requested; simply crop down to the requested size self._crop_image(brightest_pix, boxsize) else: #cut out a portion of the image with the brightest pixel at the centre - this step is necessary #because after the rotation the brightest_pix coordinates will no longer be correct self._crop_image(brightest_pix, 2 * boxsize) #rotate to the requested position angle (necessary if using image as a PSF) self.image = rotate(self.image, self.angle - rotate_to) #now cut down to the requested size; note that we again look for the brightest pixel #and put this in the centre, since the rotation may have introduced a small offset self._crop_image(self._find_brightest(2 * self.pfov, [i / 2 for i in self.image.shape]), boxsize) #normalize if requested if normalize: self.image /= np.sum(self.image) #rebin to a higher resolution if requested self.hires_scale = hires_scale if self.hires_scale >= 1: self.image_hires = congrid(self.image, [i * self.hires_scale for i in self.image.shape], minusone = True) #ensure that flux is conserved self.image_hires *= np.sum(self.image) / np.sum(self.image_hires) else: raise Exception(f"hires_scale should be an integer >= 1") if query_simbad: if not np.isnan(rotate_to): warnings.warn(f"SIMBAD source overplotting for rotated images is" " not supported. Skipping query.", stacklevel = 2) else: self.source_coords=[] self.source_names=[] Simbad.add_votable_fields('pm','plx') with fits.open(filename) as fitsfile: qra = fitsfile['PRIMARY'].header['RA'] if np.isnan(target_ra) else target_ra qdec = fitsfile['PRIMARY'].header['DEC'] if np.isnan(target_dec) else target_dec coord = SkyCoord(ra = qra, dec = qdec, unit = (u.degree, u.degree), frame = 'icrs') #find sources within a circle whose radius is half the cutout side length r = Simbad.query_region(coordinates = coord, radius = boxsize * self.pfov * u.arcsec) if len(r) > 0: wcs = WCS(fitsfile['image'].header) for i in range(len(r)): #assume a distance of 50pc if none is available qdist = 50 * u.pc #preferentially use the supplied distance if distance_provided: qdist = dist * u.pc #otherwise, try to get one from SIMBAD elif r[i]['PLX_VALUE'] > 0: distance = (1e3 / r[i]['PLX_VALUE']) * u.pc if np.isfinite(r[i]['PMRA']) and np.isfinite(r[i]['PMDEC']): #J2000 sky coordinates s2000 = SkyCoord(r[i]['RA'].replace(' ',':')+' '+r[i]['DEC'].replace(' ',':'), unit = (u.hour, u.degree), distance = qdist, pm_ra_cosdec = r[i]['PMRA'] * u.mas / u.yr, pm_dec = r[i]['PMDEC'] * u.mas / u.yr, obstime = Time(2451545.0,format='jd')) #apply proper motion correction to observation date s = s2000.apply_space_motion(new_obstime = Time(fitsfile['PRIMARY'].header['DATE-OBS'])) else: s = SkyCoord(r[i]['RA'].replace(' ',':')+' '+r[i]['DEC'].replace(' ',':'), unit = (u.hour, u.degree)) #find the pixel corresponding to source i coord = np.flip(wcs.wcs_world2pix([[s.ra.deg, s.dec.deg]], 0)[0]) #translate into image cutout coordinates coord -= np.array(brightest_pix) - boxsize #store the coordinates and source name as an attribute append = True #don't append duplicate coordinates (i.e. planets) for c in self.source_coords: if np.isclose(c, coord).all(): append = False if append: self.source_coords.append(coord) self.source_names.append(r[i]['MAIN_ID'].decode())
def get_nearby_offset_stars( source_ra, source_dec, source_name, how_many=3, radius_degrees=2 / 60.0, mag_limit=18.0, mag_min=10.0, min_sep_arcsec=2, starlist_type='Keck', obstime=None, use_source_pos_in_starlist=True, allowed_queries=2, queries_issued=0, use_ztfref=True, required_ztfref_source_distance=60, ): """Finds good list of nearby offset stars for spectroscopy and returns info about those stars, including their offsets calculated to the source of interest Parameters ---------- source_ra : float Right ascension (J2000) of the source source_dec : float Declination (J2000) of the source source_name : str Name of the source how_many : int, optional How many offset stars to try to find radius_degrees : float, optional Search radius from the source position in arcmin mag_limit : float, optional How faint should we search for offset stars? mag_min : float, optional What is the brightest offset star we will allow? min_sep_arcsec : float, optional What is the closest offset star allowed to the source? starlist_type : str, optional What starlist format should we use? obstime : str, optional What datetime (in isoformat) should we assume for the observation (to calculate proper motions)? use_source_pos_in_starlist : bool, optional Return the source itself for in starlist? allowed_queries : int, optional How many times should we query (with looser and looser criteria) before giving up on getting the number of offset stars we desire? queries_issued : int, optional How many times have we issued a query? Bookkeeping parameter. use_ztfref : boolean, optional Use the ZTFref catalog for offset star positions if possible required_ztfref_source_distance : float, optional If there are zero ZTF ref stars within this distance in arcsec, then do not use the ztfref catalog even if asked. This probably means that the source is at the end of the ref catalog. Returns ------- (list, str, int, int, bool) Return a tuple which contains: a list of dictionaries for each object in the star list, the query issued, the number of queries issues, the length of the star list (not including the source itself), and whether the ZTFref catalog was used for source positions or not. """ if queries_issued >= allowed_queries: raise Exception( 'Number of offsets queries needed exceeds what is allowed') if not obstime: source_obstime = Time(datetime.datetime.utcnow().isoformat()) else: # TODO: check the obstime format source_obstime = Time(obstime) center = SkyCoord( source_ra, source_dec, unit=(u.degree, u.degree), frame='icrs', obstime=source_obstime, ) # get three times as many stars as requested for now # and go fainter as well fainter_diff = 1.5 # mag search_multipler = 20 min_distance = 5.0 / 3600.0 # min distance from source for offset star source_in_catalog_dist = 0.5 / 3600.0 # min distance from source for offset star query_string = f""" SELECT TOP {how_many*search_multipler} DISTANCE( POINT('ICRS', ra, dec), POINT('ICRS', {source_ra}, {source_dec})) AS dist, source_id, ra, dec, ref_epoch, phot_rp_mean_mag, pmra, pmdec, parallax FROM {{main_db}}.gaia_source WHERE 1=CONTAINS( POINT('ICRS', ra, dec), CIRCLE('ICRS', {source_ra}, {source_dec}, {radius_degrees})) AND phot_rp_mean_mag < {mag_limit + fainter_diff} AND phot_rp_mean_mag > {mag_min} AND parallax < 250 ORDER BY dist ASC """ g = GaiaQuery() r = g.query(query_string) # get brighter stars at top: r.sort("phot_rp_mean_mag") potential_source_in_gaia_query = r[r["dist"] < source_in_catalog_dist] if len(potential_source_in_gaia_query) > 0: # try to find offset stars brighter than the catalog brightness of the # source. source_catalog_mag = potential_source_in_gaia_query["phot_rp_mean_mag"] offset_brightness_limit = source_catalog_mag for _ in range(3): temp_r = r[r["phot_rp_mean_mag"] <= offset_brightness_limit] if len(temp_r) > how_many + 2: r = temp_r break offset_brightness_limit += 0.5 # filter out stars near the source (and the source itself) # since we do not want waste an offset star on very nearby sources r = r[r["dist"] > min_distance] queries_issued += 1 catalog = SkyCoord.guess_from_table(r) if use_ztfref: ztfcatalog = get_ztfcatalog(source_ra, source_dec) if ztfcatalog is None: log('Warning: Could not find the ZTF reference catalog' f' at position {source_ra} {source_dec}') else: if (sum( center.separation(ztfcatalog) < required_ztfref_source_distance * u.arcsec) == 0): ztfcatalog = None log('Warning: The ZTF reference catalog is empty near' f' position {source_ra} {source_dec}. This probably means' ' that the source is at the edge of the ref catalog.') use_ztfref = False # star needs to be this far away # from another star min_sep = min_sep_arcsec * u.arcsec good_list = [] for source in r: c = SkyCoord( ra=source["ra"], dec=source["dec"], unit=(u.degree, u.degree), pm_ra_cosdec=source['pmra'] * u.mas / u.yr, pm_dec=source["pmdec"] * u.mas / u.yr, frame='icrs', distance=min(abs(1 / source["parallax"]), 10) * u.kpc, obstime=Time(source['ref_epoch'], format='jyear'), ) d2d = c.separation(catalog) # match it to the catalog if sum(d2d < min_sep) == 1 and source["phot_rp_mean_mag"] <= mag_limit: # this star is not near another star and is bright enough # if there's a close match to ZTF reference position then use # ZTF position for this source instead of the gaia/motion data if use_ztfref and ztfcatalog is not None: idx, ztfdist, _ = c.match_to_catalog_sky(ztfcatalog) if ztfdist < 0.5 * u.arcsec: cprime = SkyCoord( ra=ztfcatalog[idx].ra.value, dec=ztfcatalog[idx].dec.value, unit=(u.degree, u.degree), frame='icrs', obstime=source_obstime, ) dra, ddec = cprime.spherical_offsets_to(center) pa = cprime.position_angle(center).degree # use the RA, DEC from ZTF here source["ra"] = ztfcatalog[idx].ra.value source["dec"] = ztfcatalog[idx].dec.value good_list.append(( source["dist"], cprime, source, dra.to(u.arcsec), ddec.to(u.arcsec), pa, )) else: # precess it's position forward to the source obstime and # get offsets suitable for spectroscopy # TODO: put this in geocentric coords to account for parallax cprime = c.apply_space_motion(new_obstime=source_obstime) dra, ddec = cprime.spherical_offsets_to(center) pa = cprime.position_angle(center).degree good_list.append(( source["dist"], cprime, source, dra.to(u.arcsec), ddec.to(u.arcsec), pa, )) good_list.sort() # if we got less than we asked for, relax the criteria if (len(good_list) < how_many) and (queries_issued < allowed_queries): return get_nearby_offset_stars( source_ra, source_dec, source_name, how_many=how_many, radius_degrees=radius_degrees * 1.3, mag_limit=mag_limit + 1.0, mag_min=mag_min - 1.0, min_sep_arcsec=min_sep_arcsec / 2.0, starlist_type=starlist_type, obstime=obstime, use_source_pos_in_starlist=use_source_pos_in_starlist, queries_issued=queries_issued, allowed_queries=allowed_queries, use_ztfref=use_ztfref, required_ztfref_source_distance=required_ztfref_source_distance, ) starlist_format = starlist_formats.get(starlist_type) if starlist_format is None: log("Warning: Do not recognize this starlist format. Using Keck.") starlist_format = starlist_formats["Keck"] sep = starlist_format["sep"] commentstr = starlist_format["commentstr"] giveoffsets = starlist_format["giveoffsets"] maxname_size = starlist_format["maxname_size"] first_line = starlist_format["first_line"] basename = source_name.strip().replace(" ", "") if len(basename) > maxname_size: basename = basename[3:] abrev_basename = source_name.strip().replace(" ", "") if len(abrev_basename) > maxname_size - 3: abrev_basename = basename[3:maxname_size] space = " " star_list_format = ( f"{basename:{space}<{maxname_size}} " + f"{center.to_string('hmsdms', sep=sep, decimal=False, precision=2, alwayssign=True)[1:]}" + f" 2000.0 {commentstr} source_name={source_name}") star_list = [{"str": first_line}] if first_line else [] if use_source_pos_in_starlist: star_list.append({ "str": star_list_format, "ra": float(source_ra), "dec": float(source_dec), "name": basename, }) for i, (dist, c, source, dra, ddec, pa) in enumerate(good_list[:how_many]): dras = f"{dra.value:<0.03f}\" E" if dra > 0 else f"{abs(dra.value):<0.03f}\" W" ddecs = (f"{ddec.value:<0.03f}\" N" if ddec > 0 else f"{abs(ddec.value):<0.03f}\" S") if giveoffsets: offsets = f"raoffset={dra.value:<0.03f} decoffset={ddec.value:<0.03f}" else: offsets = "" name = f"{abrev_basename}_o{i+1}" star_list_format = ( f"{name:{space}<{maxname_size}} " + f"{c.to_string('hmsdms', sep=sep, decimal=False, precision=2, alwayssign=True)[1:]}" + f" 2000.0 {offsets}" + f" {commentstr} dist={3600*dist:<0.02f}\"; {source['phot_rp_mean_mag']:<0.02f} mag" + f"; {dras}, {ddecs} PA={pa:<0.02f} deg" + f" ID={source['source_id']}") star_list.append({ "str": star_list_format, "ra": c.ra.value, "dec": c.dec.value, "name": name, "dras": dras, "ddecs": ddecs, "mag": float(source["phot_rp_mean_mag"]), "pa": pa, }) # send back the starlist in return ( star_list, query_string.replace("\n", " "), queries_issued, len(star_list) - 1, use_ztfref, )
# Then propagate the Gaia coordinates to 2000, and find the best match to the # input coordinates if not warning: ra2015 = np.array(gaia['ra']) * u.deg dec2015 = np.array(gaia['dec']) * u.deg parallax = np.array(gaia['parallax']) * u.mas pmra = np.array(gaia['pmra']) * u.mas / u.yr pmdec = np.array(gaia['pmdec']) * u.mas / u.yr c2015 = SkyCoord(ra=ra2015, dec=dec2015, distance=Distance(parallax=parallax, allow_negative=True), pm_ra_cosdec=pmra, pm_dec=pmdec, obstime=Time(2015.5, format='decimalyear')) c2000 = c2015.apply_space_motion(dt=-15.5 * u.year) idx, sep, _ = coord.match_to_catalog_sky(c2000) # All objects id_all = gaia['source_id'] plx_all = np.array(gaia['parallax']) g_all = np.array(gaia['phot_g_mean_mag']) MG_all = 5 + 5 * np.log10(plx_all / 1000) + g_all bprp_all = np.array(gaia['bp_rp']) id_all = np.array(id_all) g_all = np.array(gaia['phot_g_mean_mag']) MG_all = np.array(MG_all) bprp_all = np.array(bprp_all)