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