Tuesday, June 28, 2011

M21 - Open Cluster

(Click image to go to info page)

The open cluster M21 is a loose collection of about 50 stars located less than one degree northeast of the Trifid nebula (M20) in Sagittarius. The cluster lies approximately 4000 light years away and has a diameter of about 15 arc-minutes. Together, M20 and M21 make up an asterism known as "Webb's Cross" with M21 at the northern end and M20 at the southern end of the cross. M21 was first observed by Charles Messier in 1764. [Source: O'Meara, Stephen James. "The Messier Objects". Cambridge: University of Cambridge and Sky Publishing, 1998. Print.]

The data for this LRGB photo was captured between 3 June and 5 June 2011. All images were taken at the Chiefland Astronomy Village in Chiefland, Florida. Complete details regarding this image can be found by clicking on the image above or by clicking here.

Monday, June 27, 2011

M20 - Trifid Nebula

(Click image to go to info page)

The Trifid Nebula is a large, bright nebula and open cluster located in the constellation Sagittarius. It consists of four bright lobes separated by dark lanes of obscuring matter. As seen in the photo above and in the full-frame photo found here, M20 is located in a densely populated region of the Milky Way. At a magnitude of 5.9 and a distance of approximately 5000 light-years, the Trifid was first observed by Charles Messier in 1764. [Source: O'Meara, Stephen James. "The Messier Objects". Cambridge: University of Cambridge and Sky Publishing, 1998. Print.]

The data for this LRGB photo was captured between 3 June and 5 June 2011. All images were taken at the Chiefland Astronomy Village in Chiefland, Florida. Complete details regarding this image can be found by clicking on the image above or by clicking here.

Sunday, June 26, 2011

Automated Astrophotography with Python - Part 6

PLATE SOLVING WITH CCDSOFT AND THE SKY6
One of the most important features of any automated imaging script includes the ability to determine exactly where the imaging system is pointing at any given time. This is accomplished by astrometric plate solves that correlates positions of stars in an image with predetermined, known star positions. The combination of CCDSoft and the Sky6 (or MaxIm DL Pro and Pinpoint) provide a software path to implement this feature. For my astro-imaging purposes, I've chosen a more convoluted path to achieving plate solves. Specifically, I acquire my plate solve images using MaxIm DL (which pulls approximate RA and Dec coordinates of the image's center from the Sky6 via an ASCOM driver), then load the image into CCDSoft to perform the actual plate solve operation.

In order to capture the initial image to be plate solved it is necessary to add some code to the 'cCamera' class. A new class constant that specifies the save directory of the plate solve images is added at the top of the module:
PLATESOLVE_PATH = r"c:\\imaging scripts\\plate_solve_images\\"
Also, two new methods are added to the 'cCamera' class. These new methods are shown below:
def exposePlateSolve(self,length,filterSlot,name,printlog):
    self.__CAMERA.Expose(length,1,filterSlot)
    while not self.__CAMERA.ImageReady:
        time.sleep(0.5)
    # save exposure into plate solve folder
    printlog.log(0,"Saving plate solve image...")
    self.__CAMERA.SaveImage(PLATESOLVE_PATH + name + "_platesolve.fit")

def setQuarterSubFrame(self):
    self.__CAMERA.StartX = self.CAMERA.CameraXSize / 4
    self.__CAMERA.StartY = self.CAMERA.CameraYSize / 4
    self.__CAMERA.NumX = self.CAMERA.CameraXSize / 2
    self.__CAMERA.NumY = self.CAMERA.CameraYSize / 2
The method 'exposePlateSolve()' is used to capture the image, generate a filename, and save the image to the directory specified by PLATESOLVE_PATH. (Typically, all my plate solve images are unbinned, 10-second exposures taken through the luminance filter.) Since plate solves using CCDSoft and the Sky6 do not execute reliably on large images, I always use sub-framing to reduce the image size to one-fourth the area of a full-size frame. The 'setQuarterSubFrame()' method is responsible for setting up the reduced sub-frame so that the center of the sub-frame and the center of the full-size frame coincide.

The next listing shows a new class named 'cPlateSolver'. This class is responsible for initiating the plate solve operation through CCDSoft. CCDSoft, in turn, calls on the Sky6 to assist with the matching of stars in the database with the stars in the plate solve image. The listing for 'cPlateSolver' is shown below:
import pythoncom
import win32com.client

IMAGESCALE = 1.808
PLATESOLVE_PATH = r"c:\\Imaging_Scripts\\plate_solve_images\\"

class cPlateSolver:
    def __init__(self,printlog):
        printlog.log(0,"Connecting to CCDSoft...")
        self.__IMAGE = win32com.client.Dispatch("CCDSoft.Image")

    def insertWCS(self,name,targetRA,targetDec,mount,printlog):
        plateSolveResults = []
        # open platesolve.fit in CCDSoft and perform plate solve
        self.__IMAGE.Path = PLATESOLVE_PATH + name + "_platesolve.fit"
        self.__IMAGE.Open()
        # set image scale in CCDSoft
        self.__IMAGE.ScaleInArcsecondsPerPixel = IMAGESCALE
        try:
            self.__IMAGE.InsertWCS()
        except pythoncom.com_error, (hr, msg, exc, arg):
            errorstr = exc[2]
            errorstr = errorstr.replace('\n',' ')
            printlog.log(0,"Plate Solve %s" % errorstr)
            self.__IMAGE.Close()
            plateSolveResults = [0.0,0.0,-1,-1]
        else:
            # if no errors, report results
            printlog.log(0,"Plate Solve Results:")
            printlog.log(1,"Image Scale: %0.3f asp" % IMAGESCALE)
            printlog.log(1,"North Angle: %0.2f deg" % self.__IMAGE.NorthAngle)
            # get 2000 Epoch RA and Dec of center of plate solved image
            RADec = self.__IMAGE.XYToRADec(self.__IMAGE.Width/2.0,
                                         self.__IMAGE.Height/2.0)
            plateSolveResults.append(RADec[0])
            plateSolveResults.append(RADec[1])
            J2000RA = mount.getAngleToDMS(RADec[0])
            printlog.log(1,"Solved RA (J2000) : %02dh %02dm %0.2fs" % \
                         (J2000RA[0],J2000RA[1],J2000RA[2]))
            J2000DEC = mount.getAngleToDMS(RADec[1])
            printlog.log(1,"Solved DEC (J2000): %02d deg %02d' %0.2f\"" % \
                         (J2000DEC[0],J2000DEC[1],J2000DEC[2]))
            # Compute angular separation and position angle from
            # plate solved location to target location
            angSeparation = mount.getAngularSeparation(targetRA,targetDec,
                                                       RADec[0],RADec[1])
            positionAngle = mount.getPositionAngle(targetRA,targetDec,
                                                   RADec[0],RADec[1])
            angSeparation = angSeparation * 3600
            plateSolveResults.append(angSeparation)
            plateSolveResults.append(positionAngle)
            printlog.log(1,"Pointing error = %0.2f arcsec at %0.2f deg" % \
                         (angSeparation,positionAngle))
            self.__IMAGE.Close()
        return plateSolveResults        

    def execPlateSolve(self,camera,mount,name,targetRA,targetDec,printlog,
                       mode="Image"):
        # set binning to unbinned
        printlog.log(0,"Setting imager bin mode to 1x1")
        camera.setBinning(1,printlog)
        # set up camera for quarter-size subframe in center
        camera.setQuarterSubFrame()
        # take 10 second exposure
        printlog.log(0,"Exposing 10 second image for Plate Solve...")
        if mode.upper() == "IMAGE":
            camera.exposePlateSolve(10.0,0,PLATESOLVE_PATH+name,printlog)
        else:
            camera.exposePlateSolve(10.0,0,PLATESOLVE_PATH+name+'_test',
                                    printlog)
        #insert WCS
        printlog.log(0,"Performing plate solve with CCDSoft and The Sky6...")
        return self.insertWCS(name,targetRA,targetDec,mount,printlog)
            
##
## END OF 'cPlateSolver' Class
##
Two constants are declared and initialized at the top of the listing. 'IMAGESCALE' contains the arc-seconds per pixel resolution of the image to be solved and 'PLATESOLVE_PATH' is the path to the directory where the image has been stored. The class constructor creates an instance of the 'Image' object of CCDSoft.

The plate solve operation is initiated by a call to the 'execPlateSolve()' method of 'cPlateSolver'. This method sets up the binning and sub-framing of the plate solve image then calls the 'exposePlateSolveImage()' method of the 'camera' instance of the 'cCamera' object. The image is then exposed and saved under a name that is generated using the 'name' argument of the method (the save filename is modified if the mode is determined to be something other than 'Image'). After the plate solve image is exposed and saved, the plate solve operation is performed by a call to the 'insertWCS()' method.

The 'insertWCS()' method is fairly simple. After opening the plate solve image in CCDSoft and setting the 'ScaleInArcsecondsPerPixel' attribute, the 'insertWCS()' method of CCDSoft's 'Image' object is called from inside a try...except...else construct. If the plate solve fails, an informative error message is printed to the screen and log file. Otherwise, the RA and Dec coordinates of the plate solve image's center is determined using the 'XYToRADec()' method of CCDSoft's 'Image' object. The RA and Declination are reformatted, written to screen and log file, and appended to the 'plateSolveResults' list data structure. Additionally, the separation (in arc-seconds) and position angle from the solved coordinates to the intended coordinates are found, written to screen and log file, and also appended to the 'plateSolveResults' list. This is is then returned to the calling routine ('execPlateSolve()') which, in turn, returns the list to its calling routine.

The unit test for this class demonstrates use of the methods in the 'cPlateSolver' class and is shown below:
if __name__ == '__main__':

    import cLogger_6
    import cCamera_6
    import cMount_6

    # set up path and name of the logfile
    cCameraLogFile = r"C:\Imaging_Scripts\logfiles\cplatesolver.log"

    # instantiate the class with the path and name
    printer = cLogger_6.cLogger(cCameraLogFile)
    printer.logHeader('Unit Test cPlateSolver Class')

    # Create an instance of the cCamera class
    testCamera = cCamera_6.cCamera(printer)

    # Create an instance of the cMount class
    testMount = cMount_6.cMount(printer)

    # Create an instance of the cPlateSolver class
    testPlateSolve = cPlateSolver(printer)

    objects = ['M92','M107']
    for obj in objects:
        printer.log(0,"")
        if not testMount.findObject(obj):
            printer.log(0,"%s located" % (obj,))
            coords = testMount.getCoordinates()
            targetRA = coords['RA J2000']
            targetDec = coords['DEC J2000']
            DMScoords = testMount.getAngleToDMS(targetRA)
            printer.log(0,"%s RA  (J2000) = %02.0fh %02.0fm %02.0fs" % \
                        (obj,DMScoords[0],DMScoords[1],DMScoords[2]))
            DMScoords = testMount.getAngleToDMS(targetDec)
            printer.log(0,"%s Dec (J2000) = %02.0fd %02.0f' %02.0f\"" % \
                        (obj,DMScoords[0],DMScoords[1],DMScoords[2]))
            psResults = testPlateSolve.execPlateSolve(testCamera,testMount,
                                                      obj,targetRA,targetDec,
                                                      printer,'test')
        else:
            printer.log(0,"%s could not be found." % obj)

    printer.logFooter()
    printer.blank()
In the unit test shown above, after importing the 'cLogger', 'cCamera', and 'cMount' modules, an instance of the class contained in each module is created. An instance of the 'cPlateSolver' class is then created and two object names are placed in a list. Next, a loop is entered that iterates through the object list. The first step of the loop is to find the object in the Sky6's database and, if found, extract the object's RA and Declination coordinates which are used as the target or intended coordinates. The 'execPlateSolve()' method of the 'cPlateSolver' object is then called with mode set to something other than 'Image'. This causes the plate solve image's name to be mangled by inserting '_test' in the middle of the file name. This is so as to not overwrite the actual plate solve image that is used to generate a successful plate solve after image acquisition. The process is then repeated for each object in the list.

TESTING THE CLASSES
Like in previous posts, before testing classes in this section, the Sky6 simulator must be manually connected to the telescope control system using the usual procedure. Also, the imaging and guide cameras in MaxIm DL should be first connected to the simulated cameras. The 'cPlateSolver' module can then be executed from the IDE in the normal manner. The source files described in this chapter can be downloaded from chapter_6.zip. Note: the .fit files included in the .zip must be placed in the same directory that the plate solve images are saved to.

As an additional exercise, edit the unit test for the 'cPlateSolver' module and completely remove the 'test' argument in the call to the 'execPlateSolve()' method then run the module. This will cause the plate solve image's name to NOT be mangled and will thus overwrite the real plate solve image included in the .zip file (so, you should make a copy of the actual .fit image prior to this test.). After the plate solve image is taken, the image generated from the simulator will be used for a plate solve. Obviously, this will fail but the manner of the failure is instructive. If the error message returned is 'Plate Solve Error: command failed. Error code = 206 (0xce).' this most likely means the RA and Declination coordinates of the center of the image were not placed into the FITS header of the plate solve image (look for keywords 'OBJCTRA' and 'OBJCTDEC'). Conversely, if the error message is 'Plate Solve Image link failed because there are not enough stars in the image. Try adjusting the image's background and range. Error code = 651 (0x28b).', this indicates that the image just can't be solved (and this is the expected behavior).

WHAT'S NEXT?
In the next post, I will present a new class that demonstrates the use of 'FocusMax' using MaxIm DL Pro. The module will include a unit test which can't be executed from in a simulator mode but must have real stars, real images, and a properly configured 'FocusMax' to execute successfully.

CLICK HERE FOR NEXT POST IN THIS SERIES

Tuesday, June 21, 2011

M5 - Globular Cluster Now In Color

(Click image to go to info page)

As described in a previous post found here, the high-resolution (unbinned) luminance data for this showcase globular cluster was captured in early May 2011 from my favorite dark site at the Chiefland Astronomy Village. On my return trip to CAV in June 2011, I collected the low-resolution (binned 2x2) red, green, and blue data. Normally, I adjust the exposure times for each filter per results of manual G2V calibration that I performed when I first acquired the SBIG ST-8300M camera and then use 1/1/1 weighting when color combining. This time I used the same exposure (5-minutes) for each sub-frame for each filter then used the free application eXcalibrator to determine the RGB weighting for color combine. In short, it worked great! eXcalibrator found 13 G2V stars in the image field and from these determined a RGB weighting of 1.000/1.128/1.301 (very close to the results of my manual G2V calibration).

The color balance of the image above has not been manually adjusted from the initial color combine. Complete details regarding this image and more color images of M5 at various resolutions can be found by clicking on the image above or by clicking here.

Saturday, June 18, 2011

Automated Astrophotography with Python - Part 5

LOGGING TO A TEXT FILE
In previous posts all status messages have been sent only to the standard output (screen) and disappear when Python is closed. In this post I'll describe how to direct status messages to also appear in a permanent log file. In order to accomplish this, it is necessary to generate a new class named 'cLogger'. The listing for the 'cLogger' class is show below:
import time

##----------------------------------------------------------------------------
## Class: cLogger
##----------------------------------------------------------------------------
class cLogger:
    def __init__(self,filename):
        self.__filename = filename
        self.__level = 0
        self.__echo = True

    def log(self,level,value,echo=True):
        self.__level = level
        self.__echo = echo
        
        # echo to terminal with leading space if echo = True (default)
        if self.__echo:
            print " " + value
        # build the filename
        iDateTime = time.localtime()
        sDateStr = time.strftime('_%Y%m%d.log', iDateTime)
        # prepend spaces depending on log level (each additional level has two
        # spaces before text, for indenting)
        for i in range(level):
            value = "   " + value
        # build the full screen line including timestamp
        prefix = time.strftime("%H:%M:%S ", time.localtime(time.time()))
        if value.endswith("\n"):
            value = value[:-1]
        value = prefix + value
        # write all entries to detailed log file
        try:
            f = open (self.__filename[:-4] + sDateStr, "a")
            f.write(value)
            f.write("\n")
            f.close()
        except:
            print "Log: UNABLE TO WRITE TO LOG FILE:"
            print "Filename:", self.__filename[:-4] + sDateStr
            print "Log line:", value        

    def blank(self,echo=True):
        self.__echo = echo
        if self.__echo:
            print " "
        # build the filename
        iDateTime = time.localtime()
        sDateStr = time.strftime('_%Y%m%d.log', iDateTime)
        # write all entries to detailed log file
        try:
            f = open (self.__filename[:-4] + sDateStr, "a")
            f.write("\n")
            f.close()
        except:
            print "Log: UNABLE TO WRITE TO LOG FILE:"
            print "Filename:", self.__filename[:-4] + sDateStr

    def logHeader(self,strTitle):
        self.log(0,"")
        strVal = strTitle + " Log File"
        self.log(0,strVal,False)
        self.log(0,"",False)
        self.log(0,"*** START OF SCRIPT ***")

    def logFooter(self):
        self.log(0,"Script complete!")
        self.log(0,"*** END OF SCRIPT ***")
        self.log(0,"")

##
## END OF 'cLogger' Class
##
The constructor for 'cClass' is a short method that initializes class attributes. (Note: when an instance of the class is created, a complete path and filename must be provided to the class.) The 'log()' method is the routine that writes a status message to the logfile. The contents of the argument 'value' is the string text message that is logged. The 'level' argument determines the indentation level of the message and 'echo' determines whether the status message is also sent to the standard output (screen). The filename that is passed into the class is used as the base of the actual log filename. After the dot and three letter extension (ex. '.log') is removed, the remainder of the filename is appended with the year, month, day, and '.log' extension. Also, the text status message is prefixed with the local hour, minute, and second. The new filename is then opened in 'append' mode and the prefixed text message is written to the file with a carriage return character added to the end of the line. The file is then immediately closed to prevent inadvertent writes.

The 'blank()' method serves only to write a blank line (with no time-stamp on the line) to the log file. This method is used to separate logging of individual runs of a script. The final two methods, 'logHeader()' and 'logFooter()', write out pre-defined header and footer messages to the log file.

The unit test for this class demonstrates use of all methods in the 'cLogger' class and is shown below:
if __name__ == '__main__':

    # set up path and name of the logfile
    cLoggerLogFile = r"C:\Imaging_Scripts\logfiles\clogger.log"

    # instantiate the class with the path and name
    printer = cLogger(cLoggerLogFile)
    
    # print to logfile and to screen
    printer.logHeader('Unit Test for cLogger Class')
    printer.log(0,"Line 1 of status: no indentation")
    printer.log(1,"Line 2 of status: 2 spaces of indentation")
    printer.log(2,"Line 3 of status: 4 spaces of indentation")
    printer.log(3,"Line 4 of status: 6 spaces of indentation")
    # print to logfile only
    printer.log(2,"Line 5 of status: 4 spaces of indentation",False)
    # continue printing to logfile and to screen
    printer.log(1,"Line 6 of status: 2 spaces of indentation")
    printer.log(0,"Line 7 of status: no indentation")
    printer.logFooter()
    printer.blank()
LOGGING FROM WITHIN CLASSES
In order for status messages generated within classes to be logged, it is necessary to modify those classes to include calls to 'cLogger'. For example, shown below is the listing of affected methods in the 'cMount' class. The major change is that a 'cLogger' object is now passed as an argument into the methods that contain status message print statements. Then, in those arguments, the 'print' statement is changed to call the 'log()' method of the passed-in 'cLogger' object. Note that any messages that you wish to only show up on the screen should be left as normal Python 'print' statements. (Changes necessary to implement logging is shown in light cyan text.)
##------------------------------------------------------------------------------
## Class: cMount
##------------------------------------------------------------------------------
class cMount:
    def __init__(self,printlog):
        self.__info = {}
        self.__objectName = ''
        printlog.log(0,"Connecting to The Sky6...")
        self.__SKYCHART = win32com.client.Dispatch("TheSky6.StarChart")
        self.__SKYINFO = win32com.client.Dispatch("TheSky6.ObjectInformation")
        self.__UTIL = win32com.client.Dispatch("TheSky6.Utils")
        self.__MOUNT = win32com.client.Dispatch("TheSky6.RASCOMTele")

    def slewToObject(self,obj,printlog,delay=2.0):
        if not self.findObject(obj):
            coords = self.getCoordinates()
            printlog.log(0,"Slewing telescope to %s..." % obj)
            JnowRA = self.__UTIL.ConvertAngleToDMS(coords['RA Now'])
            printlog.log(0,"JNow RA : %02dh %02dm %0.2fs" % (JnowRA[0],
                                                             JnowRA[1],
                                                             JnowRA[2]))
            JnowDEC = self.__UTIL.ConvertAngleToDMS(coords['DEC Now'])
            printlog.log(0,"JNow DEC: %02d deg %02d' %0.2f\"" % (JnowDEC[0],
                                                                 JnowDEC[1],
                                                                 JnowDEC[2]))
            try:
                self.__MOUNT.SlewToRaDec(coords['RA Now'],coords['DEC Now'],obj)
            except:
                printlog.log(0,"ERROR: During slew to Object")
                return ERROR
            else:
                printlog.log(0,"Done slewing!")
                # delay (default = 2.0 seconds)
                time.sleep(delay)
                return NOERROR
        else:
            printlog.log(0,"%s could not be found." % obj)
            return ERROR

    def slewToAzAlt(self,azimuth,altitude,name,printlog,delay=2.0):
        printlog.log(0,"Slewing to Azimuth: %0.1f and Altitude: %0.1f..." % \
                     (azimuth,altitude))
        try:
            self.__MOUNT.SlewToAzAlt(azimuth,altitude,name)
        except:
            printlog.log(0,"ERROR: During slew to altitude/azimuth position")
            return ERROR
        else:
            printlog.log(0,"Done slewing!")
            # delay (default = 2.0 seconds)
            time.sleep(delay)
            return NOERROR
Additionally, the unit test for 'cMount' must be modified to implement the data logging feature. Here's the listing for the 'cMount' class unit test:
if __name__ == "__main__":
    
    import cLogger_5

    # set up path and name of the logfile
    cMountLogFile = r"C:\Imaging_Scripts\logfiles\cmount.log"

    # instantiate the class with the path and name
    printer = cLogger_5.cLogger(cMountLogFile)
    printer.logHeader('Unit Test for cMount Class')

    # create an instance of the cMount object
    testMount = cMount(printer)

    # prompt for name of object to locate
    print
    obj = raw_input(" Enter first object to slew to: ")
    # test slewToObject()
    testMount.slewToObject(obj,printer)

    # prompt for name of object to locate
    print
    obj = raw_input(" Enter second object to slew to: ")
    # test slewToObject()
    testMount.slewToObject(obj,printer)

    # prompt for name of object for sync
    print
    obj = raw_input(" Enter object to sync to: ")
    if not testMount.findObject(obj):
        coords = testMount.getCoordinates()
        # test syncToObject()
        printer.log(0,"Syncing control system to %s" % obj)
        testMount.syncToObject(coords['RA Now'],coords['DEC Now'],obj)
    else:
        printer.log(0,"%s could not be found." % obj)

    # prompt for azimuth and altitude for slew
    print
    azimuth = raw_input(" Enter azimuth for slew: ")
    azimuth = float(azimuth)
    altitude = raw_input(" Enter altitude for slew: ")
    altitude = float(altitude)

    # test slewToAzAlt()
    testMount.slewToAzAlt(azimuth,altitude,"park",printer)

    printer.logFooter()
    printer.blank()
In the unit test shown above, after importing the 'cLogger' module, an instance of the class named 'printer' is created. This object is then used in calls to methods of the 'cMount' class and to generate status messages for the unit test itself. The class listing and unit test for the 'cCamera' class has been similarly modified and included in the download files for this chapter.

TESTING THE CLASSES
Like in previous posts, before testing classes in this section, the Sky6 simulator must be manually connected to the telescope control system using the usual procedure. Also, the imaging and guide cameras in MaxIm DL should be first connected to the simulated cameras. The 'cLogger', 'cMount', and 'cCamera' classes can then be executed from the IDE in the normal manner. The source files described in this chapter can be downloaded from chapter_5.zip. Execute each unit test individually then inspect the generated log files to see that they are written as expected. Next, run each unit test again to verify that the log file is appended with new status messages for each run of the unit test.

WHAT'S NEXT?
In the next post, I will present a new class that implements plate solving using the Sky6 and CCDSoft. The class and unit test will also include a provision to perform a simulated test of the plate solve operation.

CLICK HERE FOR NEXT POST IN THIS SERIES

Wednesday, June 15, 2011

Automated Astrophotography with Python - Part 4

USING AN INPUT TEXT FILE
In the previous post there was much user interaction required to choose all the possible options for setting up a simple imaging session. In this post, I'll show how I use an input text file that eliminates the need to make all the selections at run-time. Here is a sample of a typical input text file:
#
# Object Command Arguments: [0] = 'OBJECT'
#                           [1] = Object Name
Object m34
#
# Guidestar Command Arguments: [0] = 'GUIDESTAR'
#                              [1] = AUTO -or- MANUAL Guide Star Selection
#                              [2] = Exposure Length in sec. (Auto mode only)
Guidestar Auto 1.0
#
# Exposure Command Arguments: [0] = 'LIGHT'
#                             [1] = Filter (L,R,G,B,HA)
#                             [2] = Binning (Positive Integer)
#                             [3] = Exposure Length (Positive Float)
#                             [4] = No. of Images (Positive Integer)
#            
#
Light L 1 10.25 3
# Light B 2 584 1
# Light G 2 30 2
# Light R 2 357 1
# Other Parameters Command Arguments: [0] = 'OTHERS'
#                                     [1] = CCD Temperature (ex. '-20.0C' or 'Skip')
#                                     [2] = Warm CCD After Imaging? (Y/N)
#                                     [3] = Slew to Safe Position After 
#                                           Imaging? (Y/N)
Others -20C Y Y
A new class called 'cParser' has been created to parse the text file and strip out commands and the parameters associated with each command. The code listing of the 'cParser' class follows:
import sys
import string

INPUTPATH = "c:\\Imaging_Scripts\\input_files\\"
COMMANDS = ('OBJECT','LIGHT','OTHERS','GUIDESTAR')

ERROR = True
NOERROR = False

##------------------------------------------------------------------------------
## Class: cParser
##------------------------------------------------------------------------------
class cParser:
    def __init__(self):
        self.__cmdArgument = []

    def missing(self,filename):
        try:
            f = open(INPUTPATH + filename)
            f.close()
            return NOERROR
        except IOError:
            return ERROR

    def parseFile(self,filename):
        if not self.missing(filename):
            f = open(INPUTPATH + filename, "r")
            while True:
                setup = []
                text = f.readline()
                if text == "":
                    f.close()
                    break
                if text[0] == "#":
                    continue
                line = text.split(" ")
                if line[0].upper() not in COMMANDS:
                    print " ERROR: Invalid Command - %s" % line[0].upper()
                    f.close()
                    return None
                else:
                    for i in range(len(line)):
                        line[i] = line[i].replace('\n','')
                        setup.append(line[i].upper())
                self.__cmdArgument.append(setup)
            return self.__cmdArgument
        else:
            print " Error: Input file %s not found" % filename
            return None

##
## END OF 'cParser' Class
##
As shown in the listing, the expected path to the input text files is defined by the constant "INPUTPATH" and indicates I've chosen to create a sub-directory off of the main directory that holds all my Python classes and scripts. The next line defines a constant called 'COMMANDS' that is a tuple of all valid commands that the parser recognizes. The heart of the 'cParser' class is contained in the 'parseFile()' method. This method reads through the input file line-by-line while ignoring any lines that begins with a '#' character. The contents of a line containing a valid command is parsed using the 'split' string function where the splitting character is a space. The command and parameters associated with that command are built into a list which is then appended to the class attribute list '__cmdArgument'. This process repeats for each command that is found until a line is read containing an empty string. The empty string is considered an end-of-file character and terminates the parsing operation. At this point the contents of the class attribute '__cmdArgument' is passed back to the calling routine thus returning a list that contains lists of commands and parameters.

The following code is a listing of the unit test for this module. It simply prompts the user to enter a filename for the input file then parses the file and prints out lists of commands and parameters found in the file.
if __name__ == '__main__':

    # instantiate the class
    cmdFile = cParser()

    # prompt for the filename of the input command file
    print
    filename = raw_input("Enter input command filename: ")
    if not cmdFile.missing(filename):
        cmds = cmdFile.parseFile(filename)
    else:
        print "ERROR: Input command file not found"
        raise EnvironmentError, 'Halting program'
    print
    for cmdsAndParams in cmds:
        print cmdsAndParams
    print
The following is a listing of the script that demonstrates how to make use of the input text file.
import cMount_4
import cCamera_4
import cParser_4

filterDictionary = {'R':0,'G':1,'B':2,'L':3,'HA':4}

# create an instance of the mount and camera class
testMount = cMount_4.cMount()
testCamera = cCamera_4.cCamera()
cmdFile = cParser_4.cParser()

# get imaging location
print
print "The Sky6 location is %s" % testMount.getImagingLocation()

# prompt for the filename of the input command file
print
filename = raw_input("Enter input command filename: ")
if not cmdFile.missing(filename):
    cmds = cmdFile.parseFile(filename)
else:
    print "ERROR: Input command file not found"
    raise EnvironmentError, 'Halting program'

autoGuide = False
warmCCD = False
slewSafePosition = False
autoGuideStarSelect = False
print
if len(cmds) > 0:
    for stringcmd in cmds:
        if stringcmd[0] == 'OBJECT':
            obj = stringcmd[1]
            print "Object:     %s" % obj
        if stringcmd[0] == 'LIGHT':
            filterName = stringcmd[1]
            binning = stringcmd[2]
            exp = float(stringcmd[3])
            noImages = int(stringcmd[4])
            print "Filter:     %s" % filterName
            print "Binning:    %s" % binning
            print "Exposure:   %02.2f" % exp
            print "No. Images: %d" % noImages
        if stringcmd[0] == 'OTHERS':
            temp = stringcmd[1]
            print "CCD Temp:   %s" % temp
            if stringcmd[2] == 'Y':
                print "Warm CCD?:  Yes"
                warmCCD = True
            else:
                print "Warm CCD?:  No"
            if stringcmd[3] == 'Y':
                print "Slew to Safe Position?: Yes"
                slewSafePosition = True
            else:
                print "Slew to Safe Position?: No"
        if stringcmd[0] == 'GUIDESTAR':
            autoGuide = True
            if stringcmd[1] == 'AUTO':
                autoGuideStarSelect = True
                guideExp = float(stringcmd[2])

print
testCamera.setCCDTemp(temp)
testCamera.gotoCCDTemp()

print
if not testMount.findObject(obj):
    if testMount.checkObjectInWest():
        print "%s is west of the meridian" % obj
    else:
        print "%s is east of the meridian" % obj

    print
    times = testMount.getTimes()
    print "System Date = %s" % times['Date Now']
    print "System Time = %s" % times['Time Now']
    if times.has_key('Rise time'):
        hms = testMount.getAngleToDMS(times['Rise time'])
        print "%s rise time    = %02.0f:%02.0f:%02.0f" % (obj,hms[0],hms[1],
                                                          hms[2])
    if times.has_key('Transit time'):
        hms = testMount.getAngleToDMS(times['Transit time'])
        print "%s transit time = %02.0f:%02.0f:%02.0f" % (obj,hms[0],hms[1],
                                                          hms[2])
    if times.has_key('Set time'):
        hms = testMount.getAngleToDMS(times['Set time'])
        print "%s set time     = %02.0f:%02.0f:%02.0f" % (obj,hms[0],hms[1],
                                                          hms[2])
else:
    print "ERROR: Object not found"
    raise EnvironmentError, 'Halting program'

#begin test
print
if not testMount.slewToObject(obj):
    # Reset guide star positions for next test
    testCamera.resetGuideStar()
    print
    testCamera.setBinning(int(binning))
    testCamera.setFullFrame()
    fileName = "%s_%s_%sx%s" % (obj,filterName,binning,binning)
    if autoGuide:
        if autoGuideStarSelect:
            if testCamera.autoGuide(True,guideExp):
                testCamera.stopAutoGuide()
        else:
            if testCamera.autoGuide(False,0.0):
                testCamera.stopAutoGuide()
        # Make sure autoguider is running
        if testCamera.checkGuiderRunning():
            print
            for i in range(noImages):
                testCamera.exposeLight(exp,filterDictionary[filterName.upper()],
                                       fileName)
            # Stop autoguider after all images complete
            testCamera.stopAutoGuide()    
        else:
            print "ERROR - Autoguider not running as expected"
    else:
        print
        for i in range(noImages):
            testCamera.exposeLight(exp,filterDictionary[filterName.upper()],
                                   fileName)
else:
    print "ERROR: During slew to object"
    raise EnvironmentError, 'Halting program'

# warm the CCD to ambient
if warmCCD:
    print
    testCamera.warmCCD()

# slew to safe position
if slewSafePosition:
    print
    testMount.slewToAzAlt(90.0,75.0,"safe")

print
print "Script Complete!"
After importing the 'cCamera', 'cMount', and 'cParser' modules, each class is instantiated. The user is then prompted for the name of the input text file, its existence is verified by the 'missing()' method, and the file is parsed by the 'parseFile()' method. The next block of code strips out the parameters for each command and associates these parameters with local variables to be used throughout the remainder of the script. The next step is to set the CCD temperature and to wait for the CCD temperature to stabilize (unless 'skip' is specified). After finding the object to image in the Sky6 database, the scope is commanded to slew to the object. If the 'GUIDESTAR' command has been specified, autoguiding will then start with either automatic or manual guide star selection. Finally, after tracking errors have converged (if applicable), the images will be taken using the parameters found with the 'LIGHT' command. After all images are collected, the CCD temperature will be warmed back to ambient if the appropriate parameter is set in the 'OTHERS' command. Likewise, if the appropriate parameter is set, the scope will be commanded to slew to a hard-coded safe position (azimuth of 90 degrees, altitude of 75 degrees).

RUNNING THE SCRIPT
Like in the previous post, before executing this script the Sky6 simulator must be manually connected to the telescope control system using the usual procedure. Also, the imaging and guide cameras in MaxIm DL should be first connected to the simulated cameras. The python script can then be executed from the IDE in the normal manner. The source listing for files used by this script can be downloaded from chapter_4.zip. Experiment with the script by running it multiple times by editing and saving the input text file to select different commands and parameters. (Note: Leave off (or comment out using '#' character) the 'GUIDESTAR' command to perform imaging with no autoguiding.)

WHAT'S NEXT?
One deficiency with this script is all status messages only go to the screen. Ideally, it would be good to also echo status of the imaging session to a log file for later analysis. In the next post, I will present a new class that handles data logging with time-stamps on each message. Implementing this capability will also require some changes to the 'cCamera' and 'cMount' class modules.

CLICK HERE FOR NEXT POST IN THIS SERIES

Saturday, June 11, 2011

Automated Astrophotography with Python - Part 3

USING CLASSES
In this post, I'll demonstrate how to use the classes (cCamera and cMount) developed in previous posts to generate a practical script that slews to a given object then starts imaging with or without autoguiding. In order for the classes and scripts to work together correctly, there are a few initial steps to perform.

SETUP
The first step is to set up a local directory that will contain source code for classes and scripts. This directory can be set up anywhere on your hard drive but I prefer to locate this directory just off of the root directory of my C: drive. On my computer I have set up a directory named 'c:\imaging_scripts'. The classes and script that go with with this post (chapter_3.zip) should be copied to this directory. The second step is to set up Python so that it knows to look in this new directory. This is done my setting an environment variable called 'PYTHONPATH' with the path to the directory. These are the steps to set up 'PYTHONPATH' on a Windows 7 machine (setup on a Windows XP computer should be very similar):
  1. Click 'Start' button
  2. Right click on 'Computer' then click on 'Properties'
  3. Click on 'Advanced system settings'
  4. Click on the 'Environment Variables...' button
  5. Under 'User variables for...' click the 'New...' button
  6. In the 'Variable name:' field enter 'PYTHONPATH'
  7. in the 'Variable value:' field enter 'C:\Python27;C:\imaging_scripts' or whatever name you used for your directory
  8. Click the 'OK' button then reboot your computer
  9. Test the path setup by loading the Python IDE. At the '>>>' prompt type 'import cMount_3'. If the IDE comes back with another '>>>' prompt without throwing an exception, the path is set up correctly.

A listing of the complete script that calls both the 'cMount' and 'cCamera' classes is given below:
import cMount_3
import cCamera_3

filterDictionary = {'R':0,'G':1,'B':2,'L':3,'HA':4}

# create an instance of the mount and camera class
testMount = cMount_3.cMount()
testCamera = cCamera_3.cCamera()

# get imaging location
print
print "The Sky6 location is %s" % testMount.getImagingLocation()


# prompt for CCD temperature (type 'skip' if no change to existing temp)
print
temp = raw_input("Enter CCD temperature (ex. '-20C','-5','skip'): ")
testCamera.setCCDTemp(temp)
testCamera.gotoCCDTemp()

# Test 1 - Slew and image with no autoguiding
# prompt for name of object to locate
print
obj = raw_input("Enter name of object to image: ")
if not testMount.findObject(obj):
    if testMount.checkObjectInWest():
        print "%s is west of the meridian" % obj
    else:
        print "%s is east of the meridian" % obj

    times = testMount.getTimes()
    print "System Date = %s" % times['Date Now']
    print "System Time = %s" % times['Time Now']
    if times.has_key('Rise time'):
        hms = testMount.getAngleToDMS(times['Rise time'])
        print "%s rise time    = %02.0f:%02.0f:%02.0f" % (obj,hms[0],hms[1],
                                                          hms[2])
    if times.has_key('Transit time'):
        hms = testMount.getAngleToDMS(times['Transit time'])
        print "%s transit time = %02.0f:%02.0f:%02.0f" % (obj,hms[0],hms[1],
                                                          hms[2])
    if times.has_key('Set time'):
        hms = testMount.getAngleToDMS(times['Set time'])
        print "%s set time     = %02.0f:%02.0f:%02.0f" % (obj,hms[0],hms[1],
                                                          hms[2])

# prompt for length of exposure
exp = raw_input("Enter length of exposure: ")
try:
    exp = float(exp)
except:
    print "ERROR: Invalid exposure length..."
    raise EnvironmentError, 'Halting program'

# prompt for binning of image
binning = raw_input("Enter binning for exposure (1/2/3): ")
if binning in ('1','2','3'):
    try:
        binmode = int(binning)
    except:
        print "ERROR: Invalid binning..."
        raise EnvironmentError, 'Halting program'

# prompt for number of exposures
noImages = raw_input("Enter the number of exposures: ")
try:
    noImages = int(noImages)
except:
    print "ERROR: Invalid number of images..."
    raise EnvironmentError, 'Halting program'

# prompt for filter
filterName = raw_input("Enter filter for exposures (L/R/G/B/Ha): ")
if not filterName.upper() in ('R','G','B','L','HA'):
    print "ERROR: Invalid filter designation..."
    raise EnvironmentError, 'Halting program'

# begin test
print
if not testMount.slewToObject(obj):
    testCamera.setBinning(binmode)
    testCamera.setFullFrame()
    fileName = "%s_%s_%sx%s" % (obj,filterName,binning,binning)
    for i in range(noImages):
        testCamera.exposeLight(exp,filterDictionary[filterName.upper()],
                               fileName)
else:
    print "ERROR: Object not found or error slewing to object"
    raise EnvironmentError, 'Halting program'


# Test 2 - Slew and image with autoguiding and auto guide star selection
# prompt for name of object to locate
print
obj = raw_input("Enter name of object to image: ")
if not testMount.findObject(obj):
    if testMount.checkObjectInWest():
        print "%s is west of the meridian" % obj
    else:
        print "%s is east of the meridian" % obj

    times = testMount.getTimes()
    print "System Date = %s" % times['Date Now']
    print "System Time = %s" % times['Time Now']
    if times.has_key('Rise time'):
        hms = testMount.getAngleToDMS(times['Rise time'])
        print "%s rise time    = %02.0f:%02.0f:%02.0f" % (obj,hms[0],hms[1],
                                                          hms[2])
    if times.has_key('Transit time'):
        hms = testMount.getAngleToDMS(times['Transit time'])
        print "%s transit time = %02.0f:%02.0f:%02.0f" % (obj,hms[0],hms[1],
                                                          hms[2])
    if times.has_key('Set time'):
        hms = testMount.getAngleToDMS(times['Set time'])
        print "%s set time     = %02.0f:%02.0f:%02.0f" % (obj,hms[0],hms[1],
                                                          hms[2])

# prompt for length of exposure
exp = raw_input("Enter length of exposure: ")
try:
    exp = float(exp)
except:
    print "ERROR: Invalid exposure length..."
    raise EnvironmentError, 'Halting program'

# prompt for binning of image
binning = raw_input("Enter binning for exposure (1/2/3): ")
if binning in ('1','2','3'):
    try:
        binmode = int(binning)
    except:
        print "ERROR: Invalid binning..."
        raise EnvironmentError, 'Halting program'

# prompt for number of exposures
noImages = raw_input("Enter the number of exposures: ")
try:
    noImages = int(noImages)
except:
    print "ERROR: Invalid number of images..."
    raise EnvironmentError, 'Halting program'

# prompt for filter
filterName = raw_input("Enter filter for exposures (L/R/G/B/Ha): ")
if not filterName.upper() in ('R','G','B','L','HA'):
    print "ERROR: Invalid filter designation..."
    raise EnvironmentError, 'Halting program'

#begin test
print
if not testMount.slewToObject(obj):
    # Start camera autoguiding with auto guide star select
    # Guide exposure = 2.0 second
    # Reset guide star positions for next test
    testCamera.resetGuideStar()
    if testCamera.autoGuide(True,2.0):
        testCamera.stopAutoGuide()
    # Make sure autoguider is running
    if testCamera.checkGuiderRunning():
        testCamera.setBinning(binmode)
        testCamera.setFullFrame()
        fileName = "%s_%s_%sx%s" % (obj,filterName,binning,binning)
        for i in range(noImages):
            testCamera.exposeLight(exp,filterDictionary[filterName.upper()],
                                   fileName)
        # Stop autoguider after all images complete
        testCamera.stopAutoGuide()    
    else:
        print "ERROR - Autoguider not running as expected"
else:
    print "ERROR: Object not found or error slewing to object"
    raise EnvironmentError, 'Halting program'

# Test 3 - Slew and image with autoguiding and manual guide star selection
# prompt for name of object to locate
print
obj = raw_input("Enter name of object to image: ")
if not testMount.findObject(obj):
    if testMount.checkObjectInWest():
        print "%s is west of the meridian" % obj
    else:
        print "%s is east of the meridian" % obj

    times = testMount.getTimes()
    print "System Date = %s" % times['Date Now']
    print "System Time = %s" % times['Time Now']
    if times.has_key('Rise time'):
        hms = testMount.getAngleToDMS(times['Rise time'])
        print "%s rise time    = %02.0f:%02.0f:%02.0f" % (obj,hms[0],hms[1],
                                                          hms[2])
    if times.has_key('Transit time'):
        hms = testMount.getAngleToDMS(times['Transit time'])
        print "%s transit time = %02.0f:%02.0f:%02.0f" % (obj,hms[0],hms[1],
                                                          hms[2])
    if times.has_key('Set time'):
        hms = testMount.getAngleToDMS(times['Set time'])
        print "%s set time     = %02.0f:%02.0f:%02.0f" % (obj,hms[0],hms[1],
                                                          hms[2])
        
# prompt for length of exposure
exp = raw_input("Enter length of exposure: ")
try:
    exp = float(exp)
except:
    print "ERROR: Invalid exposure length..."
    raise EnvironmentError, 'Halting program'

# prompt for binning of image
binning = raw_input("Enter binning for exposure (1/2/3): ")
if binning in ('1','2','3'):
    try:
        binmode = int(binning)
    except:
        print "ERROR: Invalid binning..."
        raise EnvironmentError, 'Halting program'

# prompt for number of exposures
noImages = raw_input("Enter the number of exposures: ")
try:
    noImages = int(noImages)
except:
    print "ERROR: Invalid number of images..."
    raise EnvironmentError, 'Halting program'

# prompt for filter
filterName = raw_input("Enter filter for exposures (L/R/G/B/Ha): ")
if not filterName.upper() in ('R','G','B','L','HA'):
    print "ERROR: Invalid filter designation..."
    raise EnvironmentError, 'Halting program'

# begin test
print
if not testMount.slewToObject(obj):
    # Start camera autoguiding with manual guide star select
    # Reset guide star positions for next test
    testCamera.resetGuideStar()
    if testCamera.autoGuide(False,0.0):
        testCamera.stopAutoGuide()
    # Make sure autoguider is running
    if testCamera.checkGuiderRunning():
        testCamera.setBinning(binmode)
        testCamera.setFullFrame()
        fileName = "%s_%s_%sx%s" % (obj,filterName,binning,binning)
        for i in range(noImages):
            testCamera.exposeLight(exp,filterDictionary[filterName.upper()],
                                   fileName)
        # Stop autoguider after all images complete
        testCamera.stopAutoGuide()    
    else:
        print "ERROR - Autoguider not running as expected"
else:
    print "ERROR: Object not found or error slewing to object"
    raise EnvironmentError, 'Halting program'

# warm the CCD to ambient
print
testCamera.warmCCD()

print
print "Script Complete!"
The first two lines of the script contain the import statements that reference the filenames (less the .py extension) containing the 'cMount' and 'cCamera' classes. At this point, instances of the two class are created and a dictionary is set up that indexes a letter or string representing the filter to the filter wheel slot number. Next, the current Sky6 location is printed then the user is prompted to enter a CCD temperature (enter 'skip' if no temperature change is desired). After setting the temperature and waiting for it to stabilize, the script begins the first of three tests.

The first test begins by prompting the user for the name of an object. If the object is located in the Sky6 database, the data related to the object (side of meridian, current date/time, rise/transit/set times) are printed to screen. The user is then prompted to enter the length of the exposure, the binning, the number of exposures, and the filter to be used. The testing then begins by commanding a slew to the object then the images are collected per user inputs with no autoguiding enabled. For the second test, the first test is repeated similarly but this time the images are collected with autoguiding and auto guide star selection (2.0 second guide exposures) enabled. The third test is identical to the first two tests except that autoguiding is enable with manual guide star selection.

RUNNING THE SCRIPT
Before executing this script, the Sky6 simulator must be manually connected to the telescope control system using the same procedure as described in the previous post. Also, the imaging and guide cameras in MaxIm DL should be first connected to the simulated cameras. The python script can then be executed from the IDE in the normal manner. The source listing for files used by this script can be downloaded from chapter_3.zip. Experiment with the script by running it multiple times and selecting different objects for The Sky6 to locate, slew to, and image.

WHAT'S NEXT?
One problem with this script is the repetitive and error-prone requirement to have the user manually enter object name, binning, exposure length, etc. for each test. In the next post, I will develop a new class that will allow this information to be read from an input text file. This will be a necessary feature to facilitate full automation of the imaging process.

CLICK HERE FOR NEXT POST IN THIS SERIES

Thursday, June 9, 2011

Automated Astrophotography with Python - Part 2c

TELESCOPE CONTROL
The last features to add to the 'cMount' class are the ability to control pointing of the telescope and to synchronize the control system to a particular point in the sky. All these functions are found in the RASCOMTele class of the Sky6. As in previous posts, instead of showing the complete 'cMount' class listing, here I show only the changes to the listing presented in the previous post. The first listing shows the one new line that must be added to the class constructor ('__init__' method) to create an instance of the Sky6 RASCOMTele class. Here is the listing for that change:
self.__MOUNT = win32com.client.Dispatch("TheSky6.RASCOMTele")
The next listing shows the three new methods added to 'cMount' that call methods from the Sky6 RASCOMTele class:
def slewToObject(self,obj,delay=2.0):
    if not self.findObject(obj):
        coords = self.getCoordinates()
        print "Slewing telescope to %s..." % obj
        JnowRA = self.__UTIL.ConvertAngleToDMS(coords['RA Now'])
        print "JNow RA : %02dh %02dm %0.2fs" % (JnowRA[0],JnowRA[1],
                                                JnowRA[2])
        JnowDEC = self.__UTIL.ConvertAngleToDMS(coords['DEC Now'])
        print "JNow DEC: %02d deg %02d' %0.2f\"" % (JnowDEC[0],JnowDEC[1],
                                                    JnowDEC[2])
        try:
            self.__MOUNT.SlewToRaDec(coords['RA Now'],coords['DEC Now'],obj)
        except:
            print "ERROR: During slew to Object"
            return ERROR
        else:
            print "Done slewing!"
            # delay (default = 2.0 seconds)
            time.sleep(delay)
            return NOERROR
    else:
        print "%s could not be found." % obj
        return ERROR
    

def slewToAzAlt(self,azimuth,altitude,name,delay=2.0):
    print "Slewing to Azimuth: %0.1f and Altitude: %0.1f..." % (azimuth,
                                                                altitude)
    try:
        self.__MOUNT.SlewToAzAlt(azimuth,altitude,name)
    except:
        print "ERROR: During slew to altitude/azimuth position"
        return ERROR
    else:
        print "Done slewing!"
        # delay (default = 2.0 seconds)
        time.sleep(delay)
        return NOERROR

def syncToObject(self,RA,Dec,Obj):
    self.__MOUNT.Sync(RA,Dec,Obj)
The first new method is 'slewToObject()'. This method attempts to locate the object in the Sky6's database. If successful, the current epoch equatorial coordinates are extracted and printed to screen. The method then calls the 'RASCOMTele' method 'SlewToRaDec()' in a try, except, else block to trap any exceptions. After the slew completes, a delay (default of 2 sec) is entered before the method returns to the calling routine. Similarly, the 'slewToAzAlt()' method calls the 'RASCOMTele' method 'SlewToAzAlt()' in a try, except, else block again to trap any exceptions. The final new method is 'syncToObject()' and calls the 'Sync()' method of 'RASCOMTele' to synchronize the control system to the specified coordinates.

cMOUNT UNIT TEST
The following listing shows the unit test code for this version of the 'cMount' class. After creating an instance of the class, the user is prompted to enter the name of an object. If a valid object name is entered, the simulator is commanded to slew to the object. This test is then repeated for a second object. Next, the user is prompted to enter the name of another object. This object is then located in the database and the telescope control system is synced to the location of that object. (The telescope pointing "bull's eye" jumps to the location of the object.) Finally, the user is prompted to enter azimuth and altitude values. Assuming valid values have been entered, the simulator will slew to the location specified by the entered azimuth and altitude.
if __name__ == "__main__":
    
    # create an instance of the cMount object
    testMount = cMount()

    # prompt for name of object to locate
    print
    obj = raw_input("Enter first object to slew to: ")
    # test slewToObject()
    testMount.slewToObject(obj)

    # prompt for name of object to locate
    print
    obj = raw_input("Enter second object to slew to: ")
    # test slewToObject()
    testMount.slewToObject(obj)

    # prompt for name of object for sync
    print
    obj = raw_input("Enter object to sync to: ")
    if not testMount.findObject(obj):
        coords = testMount.getCoordinates()
        # test syncToObject()
        print "Syncing control system to %s" % obj
        testMount.syncToObject(coords['RA Now'],coords['DEC Now'],obj)
    else:
        print "%s could not be found." % obj

    # prompt for azimuth and altitude for slew
    print
    azimuth = raw_input("Enter azimuth for slew: ")
    azimuth = float(azimuth)
    altitude = raw_input("Enter altitude for slew: ")
    altitude = float(altitude)

    # test slewToAzAlt()
    testMount.slewToAzAlt(azimuth,altitude,"park")
RUNNING THE SCRIPT
In order to test this version of the 'cMount' class you must first load the Sky6 and manually connect the telescope control system to the simulator. This is done by clicking on the 'Telescope' pull-down menu tab then selecting the 'Setup...' option. In the 'Telescope Setup' dialog box, select 'Simulator' in the selection box under 'Name:'. Click on the 'Close' button then re-click the 'Telescope' pull-down menu tab then select the 'Server Settings...' option. In the 'Server Settings' dialog box, make sure the 'Allow sync' and 'Allow goto' boxes are checked then click the 'OK' button. (Note: the changes to the 'Telescope Setup' and 'Server Settings' dialog boxes only need to be made the first time the simulator is used.) The connection to the simulator is then made by clicking on the 'Telescope' pull-down menu tab then hovering over the 'Link' option. Move the mouse pointer over to and click on 'Establish' to make the connection. (You can also just click on the green telescope icon on the menu bar.) After a moment, the white bull's eye will appear that indicates the current location of the scope control system. The python script can now be executed from the IDE in the normal manner. The source listing for 'cMount' can be downloaded from cMount_2c.zip. Experiment with the script by running it multiple times and selecting different objects for The Sky6 to locate, slew to, and sync to. (When testing is complete, click on the red telescope icon the menu bar to terminate the connection.)

WHAT'S NEXT?
This post completes discussion of the 'cMount' class (although additional methods may be added to the class in later posts). In the next post, I will create a script that creates an instance of the 'cMount' class and an instance of the 'cCamera' class to demonstrate how these classes can be used in combination to generate practical scripts for slewing and imaging of objects.

CLICK HERE FOR NEXT POST IN THIS SERIES

Wednesday, June 8, 2011

Automated Astrophotography with Python - Part 2b

THE SKY6 UTILITIES
The next major functionality to add to the 'cMount' class is the ability to perform various calculations related to equatorial coordinates. All these functions are found in the UTILS class of the Sky6. As in previous posts, instead of showing the complete 'cMount' class listing, here I show only the changes to the listing presented in the previous post. The first listing shows the one new line that must be added to the class constructor ('__init__' method) to create an instance of the Sky6 UTILS class. Here is the listing for that change:
self.__UTIL = win32com.client.Dispatch("TheSky6.Utils")

The next listing shows the six new methods added to 'cMount' that call methods from the Sky6 UTILS class:
def getAngularSeparation(self,RA1,Dec1,RA2,Dec2):
    """Returns results of "ComputeAngularSeparation" method of UTILS"""
    return self.__UTIL.ComputeAngularSeparation(RA1,Dec1,RA2,Dec2)
    
def getPositionAngle(self,RA1,Dec1,RA2,Dec2):
    """Returns results of "ComputePositionAngle" method of UTILS"""
    return self.__UTIL.ComputePositionAngle(RA1,Dec1,RA2,Dec2)

def getAngleToDMS(self,angle):
    """Returns results of "ConvertAngleToDMS" method of UTILS"""
    return self.__UTIL.ConvertAngleToDMS(angle)

def get2000ToNow(self,RA,Dec):
    """Returns results of "Precess2000ToNow" method of UTILS"""
    return self.__UTIL.Precess2000ToNow(RA,Dec)

def getNowTo2000(self,RA,Dec):
    """Returns results of "PrecessNowTo2000" method of UTILS"""
    return self.__UTIL.PrecessNowTo2000(RA,Dec)

def getRADecToAzAlt(self,RA,Dec):
    """Returns results of "ConvertRADecToAzAlt" method of UTILS"""
    return self.__UTIL.ConvertRADecToAzAlt(RA,Dec)
The first two methods are used to compute the angular separation (in degrees) between two equatorial coordinates and to compute the position angle (measured counter-clockwise from true North) from one coordinate to the other. The method 'getAngleToDMS()' is used to compute the degrees, minutes, and seconds from an angle expressed as a float value. The next two methods are used to precess an equatorial coordinate between the current epoch and the year 2000 epoch or vice-versa. Finally, the 'getRADecToAzAlt()' method is used to generate altitude and azimuth for the Sky6's current time and location from an equatorial coordinate. Many more methods are available in the UTILS class and descriptions can be found in the Sky6's help section under 'Scripted Operation'.

cMOUNT UNIT TEST
The following listing shows the unit test code for this version of the 'cMount' class. After creating an instance of the class, the test attempts to locate 'Pollux' in the database. If successful, the equatorial coordinates for the star are found and the RA and Dec for both the current epoch and for the year 2000 epoch are printed out in Hours or Degrees, minutes, and seconds. This operation is repeated for 'Castor' then the separation and position angle between the two stars is found and printed to screen. Next, the object 'M95' is located and the the RA and DEC (epoch 2000) is found and printed to screen. The current epoch coordinates is then found and printed using the 'get2000ToNow()' method. The object's altitude and azimuth is then computed and printed out. As a final test the object 'M65' is located and the the RA and DEC (current epoch) is found and printed. The year 2000 epoch coordinates is then found and printed using the 'getNowTo2000()' method. The object's altitude and azimuth is then computed and printed out.
if __name__ == "__main__":
    
    # create an instance of the cMount object
    testMount = cMount()

    # test getAngleToDMS()
    obj1 = 'Pollux'
    if not testMount.findObject(obj1):
        coords1 = testMount.getCoordinates()
        print
        DMScoords = testMount.getAngleToDMS(coords1['RA Now'])
        print "%s RA  (current epoch) = %02.0fh %02.0fm %03.1fs" % (obj1,
                DMScoords[0],DMScoords[1],DMScoords[2])
        DMScoords = testMount.getAngleToDMS(coords1['DEC Now'])
        print "%s Dec (current epoch) = %02.0fd %02.0f' %03.1f\"" % (obj1,
                DMScoords[0],DMScoords[1],DMScoords[2])
        DMScoords = testMount.getAngleToDMS(coords1['RA J2000'])
        print "%s RA     (epoch 2000) = %02.0fh %02.0fm %03.1fs" % (obj1,
                DMScoords[0],DMScoords[1],DMScoords[2])
        DMScoords = testMount.getAngleToDMS(coords1['DEC J2000'])
        print "%s Dec    (epoch 2000) = %02.0fd %02.0f' %03.1f\"" % (obj1,
                DMScoords[0],DMScoords[1],DMScoords[2])
    else:
        print "%s could not be found." % obj1

    obj2 = 'Castor'
    if not testMount.findObject(obj2):
        coords2 = testMount.getCoordinates()
        print
        DMScoords = testMount.getAngleToDMS(coords2['RA Now'])
        print "%s RA  (current epoch) = %02.0fh %02.0fm %03.1fs" % (obj2,
                DMScoords[0],DMScoords[1],DMScoords[2])
        DMScoords = testMount.getAngleToDMS(coords2['DEC Now'])
        print "%s Dec (current epoch) = %02.0fd %02.0f' %03.1f\"" % (obj2,
                DMScoords[0],DMScoords[1],DMScoords[2])
        DMScoords = testMount.getAngleToDMS(coords2['RA J2000'])
        print "%s RA     (epoch 2000) = %02.0fh %02.0fm %03.1fs" % (obj2,
                DMScoords[0],DMScoords[1],DMScoords[2])
        DMScoords = testMount.getAngleToDMS(coords2['DEC J2000'])
        print "%s Dec    (epoch 2000) = %02.0fd %02.0f' %03.1f\"" % (obj2,
                DMScoords[0],DMScoords[1],DMScoords[2])
    else:
        print "%s could not be found." % obj2

    # test getAngularSeparation()
    separation = testMount.getAngularSeparation(coords1['RA Now'],
                    coords1['DEC Now'],coords2['RA Now'],coords2['DEC Now'])
    separation = testMount.getAngleToDMS(separation)
    print
    print "The separation between %s and %s is %02.0f deg %02.0f min" % \
          (obj1,obj2,separation[0],separation[1])

    # test getPositionAngle()
    angle = testMount.getPositionAngle(coords2['RA Now'],coords2['DEC Now'],
                                       coords1['RA Now'],coords1['DEC Now'])
    angle = testMount.getAngleToDMS(angle)
    print
    print "The postion angle from %s to %s is %02.0f deg %02.0f min" % \
          (obj1,obj2,angle[0],angle[1])

    obj1 = 'm95'
    if not testMount.findObject(obj1):
        # test get2000ToNow()
        coords1 = testMount.getCoordinates()
        print
        DMScoords = testMount.getAngleToDMS(coords1['RA J2000'])
        print "%s RA     (epoch 2000) = %02.0fh %02.0fm %02.0fs" % (obj1,
                DMScoords[0],DMScoords[1],DMScoords[2])
        DMScoords = testMount.getAngleToDMS(coords1['DEC J2000'])
        print "%s Dec    (epoch 2000) = %02.0fd %02.0f' %02.0f\"" % (obj1,
                DMScoords[0],DMScoords[1],DMScoords[2])


        coords2 = testMount.get2000ToNow(coords1['RA J2000'],coords1['DEC J2000'])
        DMScoords = testMount.getAngleToDMS(coords2[0])
        print "%s RA  (current epoch) = %02.0fh %02.0fm %02.0fs" % (obj1,
                DMScoords[0],DMScoords[1],DMScoords[2])
        DMScoords = testMount.getAngleToDMS(coords2[1])
        print "%s Dec (current epoch) = %02.0fd %02.0f' %02.0f\"" % (obj1,
                DMScoords[0],DMScoords[1],DMScoords[2])

        # test getRADecToAzAlt()
        print
        azAlt = testMount.getRADecToAzAlt(coords1['RA J2000'],
                                          coords1['DEC J2000'])
        azimuth = testMount.getAngleToDMS(azAlt[0])
        print "%s Azimuth  = %02.0fd %02.0fm %02.0fs" % (obj1,
                azimuth[0],azimuth[1],azimuth[2])
        altitude = testMount.getAngleToDMS(azAlt[1])
        print "%s Altitude = %02.0fd %02.0fm %02.0fs" % (obj1,
                altitude[0],altitude[1],altitude[2])
        print
    else:
        print "%s could not be found." % obj1

    obj2 = 'm65'
    if not testMount.findObject(obj2):
        # test getNowTo2000()
        coords1 = testMount.getCoordinates()
        print
        DMScoords = testMount.getAngleToDMS(coords1['RA Now'])
        print "%s RA  (current epoch) = %02.0fh %02.0fm %02.0fs" % (obj2,
                DMScoords[0],DMScoords[1],DMScoords[2])
        DMScoords = testMount.getAngleToDMS(coords1['DEC Now'])
        print "%s Dec (current epoch) = %02.0fd %02.0f' %02.0f\"" % (obj2,
                DMScoords[0],DMScoords[1],DMScoords[2])


        coords2 = testMount.getNowTo2000(coords1['RA Now'],coords1['DEC Now'])
        DMScoords = testMount.getAngleToDMS(coords2[0])
        print "%s RA     (epoch 2000) = %02.0fh %02.0fm %02.0fs" % (obj2,
                DMScoords[0],DMScoords[1],DMScoords[2])
        DMScoords = testMount.getAngleToDMS(coords2[1])
        print "%s Dec    (epoch 2000) = %02.0fd %02.0f' %02.0f\"" % (obj2,
                DMScoords[0],DMScoords[1],DMScoords[2])

        # test getRADecToAzAlt()
        print
        azAlt = testMount.getRADecToAzAlt(coords2[0],coords2[1])
        azimuth = testMount.getAngleToDMS(azAlt[0])
        print "%s Azimuth  = %02.0fd %02.0fm %02.0fs" % (obj2,
                azimuth[0],azimuth[1],azimuth[2])
        altitude = testMount.getAngleToDMS(azAlt[1])
        print "%s Altitude = %02.0fd %02.0fm %02.0fs" % (obj2,
                altitude[0],altitude[1],altitude[2])
        print
    else:
        print "%s could not be found." % obj2
RUNNING THE SCRIPT
As before, testing this version of the 'cMount' class is simply a matter of loading the source code into the Python IDE and running the module by hitting the F5 key. If the Sky6 is not already running, it will be loaded before the unit test executes. The source listing for 'cMount' can be downloaded from cMount_2b.zip. Experiment with the script by running it multiple times and selecting different objects for The Sky6 to locate and display information about.

WHAT'S NEXT?
In the next post I will complete the 'cMount' class by introducing methods that allow the script to control pointing of the telescope and syncing the the scope with a particular point in the sky. I will also create a new unit test listing to verify correct operation of the features added to this class.

CLICK HERE FOR NEXT POST IN THIS SERIES