class STELA(): """ Class that governs the alignment of telescope, star positions, catalogs, and star identification Quantities and objects: STELA.naked: catalog of nearest stars, brightes in sky STELA.ard_pos: last stored position of arduino STELA.ard_targ: last stored arduino target Functions: STELA.setup_cats: Setup catalog files. STELA.setup_serial: Initiate serial connection with Arduino. STELA.get_ref_stars:ls Given an estimated latitude and longitude, returns the three brightest stars in the sky to align. STELA.gen_mock_obs: For testing triangulation STELA.triangulate: After calling get_ref_stars, and measuring the differences in alt-az coordinates of the three points, STELA.triangulate can locate the new latitude and longitude that accounts for the error in telescope positioning. STELA.set_targ: Sends target alt azimuth coordinate to arduino. STELA.get_pos: Reads current arduino poisition. """ def __init__(self): self.DATA_DIREC = DATA_PATH self.simbad = Simbad() self.simbad.TIMEOUT = 1000000 self.simbad.remove_votable_fields('coordinates') self.simbad.add_votable_fields('id(NAME)', 'ids', 'id(NGC)', 'id(M)', 'id(HD)', 'ra', 'dec', 'otype(V)', 'sp', 'plx', 'z_value', 'flux(V)', 'distance') self.reset_cats = False self.__triangulation_class = Triangulate() self.Transform = Transform() def setup_cats(self, reset_cats=False): """ Sets up the necessary catalogs, prints them to a file. (No parameters) """ self.__online = self.connect() if self.__online == False: print 'WARNING: No internet connection. Running offline mode.' print "" # Download necessary catalogs catlabels = ["GJ", "New Galactic Catalog", "Messier", "Henry Draper"] cats = ["gj", "ngc", "m", "hr"] catobjs = [] # If user requests fresh import, do it if reset_cats == True and self.__online == True: print "Deleting old catalog..." for c in cats: os.system('rm ' + os.path.join(self.DATA_DIREC, c.lower() + ".dat")) elif reset_cats == True: print "Requested refresh of catalogs but no internet connection. Find one!" for i in range(len(cats)): fp = os.path.join(self.DATA_DIREC, cats[i].lower() + '.dat') if os.path.exists(fp) == False: if self.__online == False: raise RuntimeError( "No saved catalogs, run once with internet") print "Downloading " + catlabels[i] + " data..." cat = self.simbad.query_catalog(cats[i].upper()) cat.remove_columns([ "Distance_merr", "Distance_Q", "Distance_perr", "Distance_bibcode" ]) select = np.array(cat["RA"] != '') * np.array(cat["DEC"] != '') catobjs += [cat[select]] catobjs[-1].write(fp, format='ascii') print "Done!" else: print catlabels[i] + " catalog file found." catobjs += [Table.read(fp, format='ascii')] class cats(): pass self.catalogs = cats() [ self.catalogs.gj, self.catalogs.ngc, self.catalogs.m, self.catalogs.hd ] = catobjs catobjs = None # Create catalog of naked eye stars (used in calibration) naked_fp = os.path.join(self.DATA_DIREC, 'naked.dat') if os.path.exists(naked_fp) == False: print "Setting up naked eye catalogs" # remove objects with no recorded magnitude select = np.ones(len(self.catalogs.gj), dtype='bool') select[np.where(np.isnan(np.array( self.catalogs.gj['FLUX_V'])))[0]] = False select[np.where(self.catalogs.gj['FLUX_V'] > 5)[0]] = False naked = self.catalogs.gj[select] print len(naked) self.catalogs.naked = Table(np.unique(naked[:800])) print len(self.catalogs.naked) self.catalogs.naked.sort("FLUX_V") # Write it to the catalogs folder self.catalogs.naked.write(naked_fp, format='ascii') else: self.catalogs.naked = Table.read(naked_fp, format='ascii') print "Done. \n" def setup_serial(self): """ Searches for a serial connection on the coms. ttyS* ports corresponding to usb COM numbers in /dev/ folder must be fully read/write/ex permissable. If failed, returns a Runtime Error. Some ports just don't work so switching USB ports might solve any problems.""" ports_closed = [] portsopen = 0 # First, try the expected filepath for a pi try: self.__ser = serial.Serial( "/dev/serial/by-id/usb-Arduino_LLC_Arduino_Micro-if00") print "Found the Arduino Micro." return except: pass # Then, try all of the ttyS* paths (useful on windows) for i in range(20): # New path to try path = "/dev/ttyS" + str(i) try: # Send message, if recieve correct response, keep serial information ser = serial.Serial(path) ser.write('setup') if ser.readline()[:5] == 'STELA': print 'Found STELA arduino running on on COM' + str(i) self.__ser = ser portsopen += 1 break # SerialException could be a sign that permissions need to be changed except serial.SerialException as err: if err.args[0] == 13: ports_closed += [i] # IOError means there was no response from the port. except IOError: pass # Next, check the pi directory where serial ports are stored if os.path.exists("/dev/serial/by-id"): for i in os.listdir("/dev/serial/by-id"): print "trying other paths..." try: ser = serial.Serial("/dev/serial/by-id/" + i) ser.write("setup") if ser.readline()[:5] == "STELA": print "Found connection with: " + i self.__ser = ser portsopen = 1 break except: pass # If no serial port, raise error and if permission issues were found. if portsopen == 0: if len(ports_closed) > 0: msg = ("Connection Failed. Unable to access ports: " + str(ports_closed) + ". Try changing permissions.") else: msg = "Connection Failed. Try different usb port." raise RuntimeError(msg) def search(self, string, list_results=False): """ Search stored catologs for a string. Looks for hits in the IDS column. Input ------ string: string The search string. Case does not matter, but any misspellings will not fly. Optional ------ list_results: bool Whether or not to return a list of all hits. If not, returns the best match. Output ------ data: dictionary or list of dictionaries A dictionary containing all of the information found in the catologs. If list_results == True, a list of dictionaries for each hit. """ print 'Searching for: "' + string + '"\n' # Check if online or not self.__online = self.connect(verbose=False) matches = Table(self.catalogs.m[0]) matches.remove_row(0) scores = [] for cat in [ self.catalogs.m, self.catalogs.gj, self.catalogs.ngc, self.catalogs.hd ]: # Load string of all names names = cat["IDS"] arr = names.view(type=np.recarray) # Look for hits in each item of the catalog string_low = string.lower() low = np.array([s.lower() for s in arr]) score_l = [] for l in low: allnames = np.array(l.split("|")) sc = [] for a in allnames: if a[:4] == "name": sc += [utils.score(string_low, a[5:])] elif a[:2] == "m " or a[:2] == "hd" or a[:3] == "ngc": sc += [utils.score(string_low, a)] else: sc += [-10.] score_l += [max(sc)] where = np.where(np.array(score_l) > 0.5)[0] if len(where) > 0: [matches.add_row(cat[i]) for i in where] scores += [score_l[i] for i in where] if len(matches) > 0: ind = scores.index(max(scores)) result = matches[ind] else: result = None """ NEED PLANET SEARCHING """ # Check connectivity if self.__online == True and type(result) == type(None): result = self.simbad.query_object(string) if isinstance(result, type(None)): return {"Error": "No object found"} if list_results == False or len(result) == 1: return self.__parse_result(result) else: info = [] for i in range(len(result)): info += [self.__parse_result(result[i])] return {"list": info} def __parse_result(self, row): """ Used to turn the catolog entries into dictionaries of information. Input ------ row: astropy row or single entry table The table to convert. Output ------ data: dic [keys=("Name","Otype","ra","dec","Distance","Mag","Sptype","Redshift","Luminosity") """ # Attempt to convert a row to a table (tables can be converted to arrays, but rows not) as_table = Table(row) # Extract information to single variables [ main, name, ids, idngc, idm, idhd, ra, dec, otype, sptype, plx, redshift, mag, dist, distu, distmeth ] = as_table.as_array()[0] # Make a string of all valid names usenames = "" for i in [name, idm, idngc, idhd]: if i != '' and i != 0: usenames += i + ", " data = {"Name": usenames[:-2]} # Go through the list, check if variables have information, and add them to dictionary if # information is found if mag != None: data["Mag"] = "%.3f" % mag if redshift > 0.001: data["Redshift"] = "%.3f" % redshift if otype != None: data["Otype"] = otype else: data["Otype"] = "Unknown" if sptype != '': data["Sptype"] = str(sptype) if plx > 0: # Distance calculations from parallax data["Plx"] = str(plx) distpc = 1000. / plx distly = distpc * 3.2616 data["Distance"] = "%.2f pc, %.2f ly" % (distpc, distly) if redshift != None: # Distance estimates from redshift data["redshift"] = redshift if plx > 0 and mag != None: # Luminosity estimates Msun = 4.74 absmag = mag - 5 * (np.log10(distpc) - 1) L = 10**(1. / 2.5 * (Msun - absmag)) data["Luminosity"] = "%.2f" % L if dist > 0: data["Distance"] = "%.2f %s" % (dist, distu) # Add right ascencion and declination information data["ra"] = "%s hrs" % ra data["dec"] = "%s deg" % dec data["celcoor"] = [utils.hourangle(ra), utils.degree(dec)] if "location" in dir(self): loc = self.location if "home" in dir(self): loc = self.home altaz = self.Transform.cel2altaz(data["celcoor"], loc)[0] data["altaz"] = altaz data["az"] = "%.2f deg" % altaz[0] data["alt"] = "%.2f deg" % altaz[1] # This is for a string with all of the information. Goes down the list, if a key exists, adds # infomation to string. Useful for displaying everything easily. order = [ "Name", "Otype", "ra", "dec", "az", "alt", "Mag", "Distance", "Sptype", "Luminosity", "Redshift" ] outstring = '' for key in order: if key in data.keys(): outstring += key + ": " + data[key] + "\n" data["String"] = outstring return data def set_time(self, datetime): """ Sets the system time. Input ------ datetime: string (format) """ l = datetime.split("-") date_str = l[1] + l[2] + l[3] + l[4] + l[0] + "." + l[5] if os.environ["USER"] == 'pi': os.system("sudo date " + date_str) else: print "Not setting time." def set_targ(self, targ): """ Sends the target coordinates in the arduino. Input ------ targ: ndarray [args, shape=(2), dtype=float] New target coordinates for the arduino. """ msg = 'set_targ:' + str(targ) self.__ser.write(msg) def get_pos(self, return_targ=False): """ Gets the current arduino position and target. Optional ------ return_targ: bool If true, returns both the arduino postion and the current arduino target. Output ------ ard_pos: ndarray [args, shape=(2), dtype=float] The postion sent back from the arduino """ now = ephem.now() # Send request to arduino through serial, read response self.__ser.write('info') msg = self.__ser.readline() # Find alt-az information, break message string into sections pos_str = msg[msg.find('[') + 1:msg.find(']')] targ_str = msg[msg.find('[', 11) + 1:msg.find(']', msg.find(']') + 1)] # Set class objects using parsed string self.ard_pos = np.fromstring(pos_str, sep=', ') self.ard_targ = np.fromstring(targ_str, sep=', ') # Save time of update self.update_time = now # Return if return_targ == True: return self.ard_pos, self.ard_targ return self.ard_pos def set_pos(self, newpos): """ Resets the position of the arduino to specified coordinates. Inputs ------ newpos: ndarray or list [args, shape=(2), dtype=float] The coordinates to use to reset the position. """ # create message and send it msg = 'set_pos:' + str(newpos) self.__ser.write(msg) def get_ref_stars(self): """ Given an estimation of longitude and latitude, identifies 3 target stars to use as triangulation coordinates Input: ------ lon_est: float The current longitude at which the telescope is set up lat_est: float The current latitude at which the telescope is set up Optional: ------ representation: string The preferred representation of the coordinates ('SkyCoord' or 'String') Output: ------ altaz_calib: list [args, shape=(3,2), dtype=float] The estimated positions in the altitude azimuth coordinate frame. Can be used to point telescope to approximate position of stars. """ [lon_est, lat_est] = self.location # Coordinates of all visible stars ra = self.catalogs.naked["RA"].data dec = self.catalogs.naked["DEC"].data # One by one, compares catalog entries to the earth normal vector for any that should be # visible in the night sky. After this check, compares entry to other accepted reference stars. # If it's too close to others, reject it. coors = [] earthnorm = self.Transform.earthnorm2icrs(self.location) for i in range(len(self.catalogs.naked)): obj = [utils.hourangle(ra[i]), utils.degree(dec[i])] if self.Transform.separation(earthnorm, obj) < 60: # If star is 30 degrees above horizon... if sum([self.Transform.separation(c, obj) < 20 for c in coors]) == 0: # Check if star is at least 20 degrees away from the other reference stars... coors += [obj] if len(coors) >= 3: # If we already now have 3 reference stars, we are good. break # Convert to earth altaz frame altaz_calib = [ self.Transform.cel2altaz(c, self.location)[0] for c in coors ] # set class objects self.altaz_calib = altaz_calib self.cel_calib = coors return altaz_calib def triangulate(self, v2_v1, v3_v2, iterations=5, verbose=True): """ Used to triangulate the true latitude and longitude corresponding to the norm of the telescope position. Input ------ v2_v1: list [args,len=2,dtype=float] The difference in [azimuth,altitude] between object 2 and object 1 v3_v2: list [args,len=2,dtype=float] The difference in [azimuth,anow haveude] between object 3 and object 2 """ # Get reference star coordinates v = np.array(self.cel_calib) # Triangulate using the triangulation class (nested in try:except for safety) loc, self.loc_errs = self.__triangulation_class.triangulate( v[0], v[1], v[2], v2_v1, v3_v2, iterations=iterations, verbose=verbose) zero = self.Transform.earthnorm2icrs([0, 0]) [self.lon, self.lat] = loc - zero self.home_coors = [self.lon, self.lat] self.tel_pos = self.Transform.cel2altaz(v[2], self.home_coors) try: self.set_pos(v[2]) except: pass self.calibrated = True def save(self): """Saves some of the data to a file.""" [lon, lat] = self.location savedata = { 'location': [lon.value, lat.value], 'location_units': [lon.unit.to_string(), lat.unit.to_string()] } with open(self.savefile, 'w') as file: file.write(json.dumps(savedata)) def load(self): """Loads data from the file.""" with open(self.savefile, 'r') as file: data = json.loads(file.read()) loc = data['location'] loc_u = data['location_units'] self.location = [loc[0] * Unit(loc_u[0]), loc[1] * Unit(loc_u[1])] def connect(self, verbose=True): """Tests the internet connection. Optional ------ verbose: bool If true, prints out process. """ if verbose == True: print "Testing internet connection..." try: urllib2.urlopen('http://216.58.192.142', timeout=1) if verbose == True: print "Connection succeeded!" return True except urllib2.URLError as err: if verbose == True: print "Connection failed." return False except Exception as e: print e return False