Tuesday, May 31, 2011

Automated Astrophotography with Python - Part 2a

THE cMOUNT CLASS
The next piece of code we will look at is the 'cMount' class. This class will interact with various objects exposed by The Sky6 planetarium and telescope control software. The Python code for 'cMount' is shown below. (Note: Docstrings have been removed from this listing for brevity and readability purposes.)
import time
import win32com.client

ERROR = True
NOERROR = False

##------------------------------------------------------------------------------
## Class: cMount
##------------------------------------------------------------------------------
class cMount:
    def __init__(self):
        self.__info = {}
        self.__objectName = ''
        print "Connecting to The Sky6..."
        self.__SKYCHART = win32com.client.Dispatch("TheSky6.StarChart")
        self.__SKYINFO = win32com.client.Dispatch("TheSky6.ObjectInformation")

    def findObject(self,objToFind):
        self.__objectName = objToFind
        if len(self.__objectName) > 0:
            try:
                self.__SKYCHART.Find(self.__objectName)
                for index in range(189):
                    skyProperty = self.__SKYINFO.Property(index)
                    if self.__SKYINFO.PropertyApplies(index):
                        self.__info[self.__SKYINFO.PropertyName(index)] = \
                                    skyProperty
                return NOERROR     
            except:
                return ERROR
        
    def getCoordinates(self):
        raDec = {}
        if self.__info.has_key('RA (current epoch)'):
            raDec['RA Now'] = self.__info['RA (current epoch)']
        if self.__info.has_key('Dec (current epoch)'):
            raDec['DEC Now'] = self.__info['Dec (current epoch)']
        if self.__info.has_key('RA (epoch 2000)'):
            raDec['RA J2000'] = self.__info['RA (epoch 2000)']
        if self.__info.has_key('Dec (epoch 2000)'):
            raDec['DEC J2000'] = self.__info['Dec (epoch 2000)']
        return raDec
    
    def getAltAzimuth(self):
        altAzimuth = {}
        if self.__info.has_key('Altitude'):
            altAzimuth['Altitude'] = self.__info['Altitude']
        if self.__info.has_key('Azimuth'):
            altAzimuth['Azimuth'] = self.__info['Azimuth']
        return altAzimuth
        
    def getTimes(self):
        objTimes = {}
        if self.__info.has_key('Date'):
            objTimes['Date Now'] = self.__info['Date']
        if self.__info.has_key('Time'):
            objTimes['Time Now'] = self.__info['Time']
        if self.__info.has_key('Transit time'):
            objTimes['Transit time'] = self.__info['Transit time']
        if self.__info.has_key('Rise time'):
            objTimes['Rise time'] = self.__info['Rise time']
        if self.__info.has_key('Set time'):
            objTimes['Set time'] = self.__info['Set time']
        return objTimes
        
    def getImagingLocation(self):
        return self.__SKYCHART.DocumentProperty(63)

    def checkObjectInWest(self):
        azimuth = self.getAltAzimuth()['Azimuth']
        if azimuth >= 180.00 and azimuth < 360.00:
            return True
        else:
            return False

##
##    END OF 'cMount' Class
##
The first two lines of the listing are the same 'import' statements that we saw in the 'cCamera' class. The listing for the 'cMount' class follows the 'import' statements and consists of seven methods. The '__init__' method is the constructor for 'cMount' and is executed whenever an instance of the 'cMount' object is created. This method creates a class attribute '__info' which is initialized as an empty dictionary data structure and '__objName' which will hold a string representation of the object that The Sky6 will find. This method also creates a '__SKYCHART' object that is bound to the Sky6's 'Skychart' object and a '__SKYINFO' object that is bound to the Sky6's 'Skyinfo' object. (See help for The Sky6 under scripting for more information regarding attributes and methods relating to these two objects.)

The next method is 'findObject' which takes as an argument the string representation of the object to find. After making sure the string 'objToFind' is not empty, the Sky6's 'Find' method is used to actually locate the object in its database. If the object is found, the remainder of the method loads all of the properties relating to that object into the '__info' dictionary. The contents of this dictionary will be used by most of the remaining methods of the 'cMount' class.

The next method, 'getCoordinates', strips out the RA and Declination information from the '__info' dictionary and builds a new dictionary called 'raDec' to hold this data. Likewise, the 'getAltAzimuth' and 'getTimes' methods similarly strip relevant information from the '__info' dictionary and build new dictionaries called 'altAzimuth' and 'objTimes' respectively.

Finally, the 'getImagingLocation' method extracts the current location in use by the Sky6's planetarium and returns it to the calling routine. Also, the 'checkObjectInWest' method uses the 'getAltAzimuth' method to determine if the located object is in the eastern or western sky. It returns TRUE if the object is in the western sky and FALSE if the object is in the eastern sky.

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 an object for The Sky6 to locate. The test then prints out the current location for The Sky6 then locates the object in its database (or returns an error if it cannot be located.) Next, the script checks whether the object is in the eastern or western sky then prints out the current system time and date before printing out the object's rise, transit, and set times (or transit time only for circumpolar objects). The object's RA and Declination for the current epoch and for the year 2000 epoch are then printed. Finally, the 'getAltAzimuth' method is called and the object's current altitude and azimuth are printed to screen.
if __name__ == "__main__":
    
    # create an instance of the cMount object
    testMount = cMount()

    # prompt for name of object to locate
    print
    obj = raw_input("Enter object to find: ")
    
    # test getImagingLocation method
    print
    print "The Sky6 Location: %s" % testMount.getImagingLocation()

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

        # test getTimes function
        times = testMount.getTimes()
        print "System Date = %s" % times['Date Now']
        print "System Time = %s" % times['Time Now']
        if times.has_key('Rise time'):
            print "%s rise time    = %0.3f" % (obj,times['Rise time'])
        if times.has_key('Transit time'):
            print "%s transit time = %0.3f" % (obj,times['Transit time'])
        if times.has_key('Set time'):
            print "%s set time     = %0.3f" % (obj,times['Set time'])
        
        # test getCoordinates function
        coords = testMount.getCoordinates()
        print
        print "%s RA  (current epoch) = %0.3f" % (obj,coords['RA Now'])
        print "%s Dec (current epoch) = %0.3f" % (obj,coords['DEC Now'])
        print "%s RA  (epoch 2000)    = %0.3f" % (obj,coords['RA J2000'])
        print "%s Dec (epoch 2000)    = %0.3f" % (obj,coords['DEC J2000'])
        
        # test getAltAzimuth function
        altazi = testMount.getAltAzimuth()
        print
        print "%s Altitude = %0.3f" % (obj,altazi['Altitude'])
        print "%s Azimuth  = %0.3f" % (obj,altazi['Azimuth'])
        print
    else:
        print "%s could not be found." % obj
RUNNING THE SCRIPT
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_2a.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 continue to expand the 'cMount' class by introducing more methods and properties that utilize planetarium and telescope control features of the Sky6. 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

Saturday, May 28, 2011

Automated Astrophotography with Python - Part 1d

AUTOGUIDING
The last major functionality to add to the 'cCamera' class is the ability to perform autoguiding during long exposures. Instead of showing the complete 'cCamera' class listing as I have in previous posts, here I show the new attributes and methods that must be added to the 'cCamera' class that I've built up so far. The first listing shows the new class attributes.
self.__guideStarXPos = 0 # x-coordinate of guide star
self.__guideStarYPos = 0 # y-coordinate of guide star
self.__guideExposure = 1.0 # default guide exposure in seconds
self.__guideSettleLimit = 0.40 # max pixel error before imaging can occur
self.__guideSettleMaxTime = 120 # max time for autoguider to settle
The first two attributes contain the x- and y-coordinates of the guide star that will be used for autoguiding and the attribute '__guideExposure' specifies the length of exposure for each guide image. The attribute '__guideSettleLimit' sets the limit which the guide errors must fall under before an image exposure will start. (ex. In this case, guide errors for both axes must settle under 0.40 pixels before an image exposure will begin.) The '__guideSettleMaxTime' attribute holds the maximum number of seconds allowed for the guide errors to settle under '__guideSettleLimit'.

The next listing shows the new methods added to the 'cCamera' class.
def autoGuide(self,autoGuideStar,exposure):
    if autoGuideStar:
        self.__CAMERA.GuiderAutoSelectStar = True
        if self.__guideStarYPos == 0 or self.__guideStarYPos == 0:
            self.__guideExposure = exposure
            if self.exposeGuider(self.__guideExposure):
                return ERROR
            self.__guideStarXPos = self.__CAMERA.GuiderXStarPosition
            self.__guideStarYPos = self.__CAMERA.GuiderYStarPosition
            print
            print "Guider Setup:"
            print "Guider:                %s" % self.__CAMERA.GuiderName
            print "Guide star selection:  Auto"
            print "Guide star exposure:   %0.2f" % self.__guideExposure
            print "Aggressiveness X-Axis: %0.2f" % \
                   self.__CAMERA.GuiderAggressivenessX
            print "Aggressiveness Y-Axis: %0.2f" % \
                   self.__CAMERA.GuiderAggressivenessY
            print "Max Move X-Axis:       %0.2f" % self.__CAMERA.GuiderMaxMoveX
            print "Max Move Y-Axis:       %0.2f" % self.__CAMERA.GuiderMaxMoveY
            print "Min Move X-Axis:       %0.2f" % self.__CAMERA.GuiderMinMoveX
            print "Min Move Y-Axis:       %0.2f" % self.__CAMERA.GuiderMinMoveY
    else:
        self.__CAMERA.GuiderAutoSelectStar = False
        ## if necessary, set up the guider for autoguiding
        if self.__guideStarXPos == 0 or self.__guideStarYPos == 0:
            # prompt operator to manually select a guide star
            print
            print " *** INPUT NEEDED ***"
            print " 1. In MaxIm, manually expose the guide camera."
            print " 2. Click on a guide star and enter a guide exposure value."
            print " 3. Verify that MaxIm correctly tracks on the guide star."
            raw_input(" 4. Press ENTER key when ready to proceed: ")
            self.__guideStarXPos = self.__CAMERA.GuiderXStarPosition
            self.__guideStarYPos = self.__CAMERA.GuiderYStarPosition
            print
            exposure = raw_input(" Enter Guide Star Exposure (sec): ")
            try:
                self.__guideExposure = float(exposure)
            except:
                print " ERROR: Invalid input. Expecting float value...try again"
                exposure = raw_input(" Enter Guide Star Exposure: ")
                try:
                    self.__guideExposure = float(exposure)
                except:
                    print "ERROR: Invalid input for guide star exposure"
                    return ERROR
            print
            print "Guider Setup:"
            print "Guider:                %s" % self.__CAMERA.GuiderName
            print "Guide star selection:  Manual"
            print "Guide star exposure:   %0.2f" % self.__guideExposure
            print "Aggressiveness X-Axis: %0.2f" % \
                   self.__CAMERA.GuiderAggressivenessX
            print "Aggressiveness Y-Axis: %0.2f" % \
                   self.__CAMERA.GuiderAggressivenessY
            print "Max Move X-Axis:       %0.2f" % self.__CAMERA.GuiderMaxMoveX
            print "Max Move Y-Axis:       %0.2f" % self.__CAMERA.GuiderMaxMoveY
            print "Min Move X-Axis:       %0.2f" % self.__CAMERA.GuiderMinMoveX
            print "Min Move Y-Axis:       %0.2f" % self.__CAMERA.GuiderMinMoveY

    self.__CAMERA.GuiderBinning = 1
    self.__CAMERA.GuiderSetStarPosition(self.__guideStarXPos,
                                        self.__guideStarYPos)
    print "Guider Declination = %d" % self.__CAMERA.GuiderDeclination
    print "Tracking on guide star at X = %d, Y = %d" % \
           (self.__guideStarXPos,self.__guideStarYPos)
    # start autoguiding
    try:
        guideStatus = self.__CAMERA.GuiderTrack(self.__guideExposure)
    except:
        print "ERROR: While attempting to start the autoguider"
        return ERROR
    else:
        if guideStatus:
            print "Start autoguiding..."
        else:
            print "ERROR: Autoguider did not start successfully"
            return ERROR
    print "Waiting for guider to settle below %0.2f px (max wait %d sec)" % \
           (self.__guideSettleLimit,self.__guideSettleMaxTime)
    started = time.time()
    cnt = 0
    while True:
        if (time.time() - started) > self.__guideSettleMaxTime:
            print "ERROR: Guider not settled within the max allowable time"
            return ERROR
        if self.__CAMERA.GuiderNewMeasurement:
            recentErrorX = self.__CAMERA.GuiderXError
            recentErrorY = self.__CAMERA.GuiderYError
            # ignore the first reading
            if cnt != 0:
                print "X-Error: %7.3f  Y-Error: %7.3f" % \
                       (recentErrorX,recentErrorY)
                if (abs(recentErrorX) < self.__guideSettleLimit and
                    abs(recentErrorY) < self.__guideSettleLimit):
                    break
        cnt += 1
        time.sleep(0.5)
    return NOERROR

def stopAutoGuide(self):
    try:
        self.__CAMERA.GuiderStop()
    except TypeError:
        print "Stop Autoguiding..."
        time.sleep(2)
    except:
        print "ERROR: Unexpected error while attempting to stop autoguider"

def checkGuiderRunning(self):
    return self.__CAMERA.GuiderRunning
    
def resetGuideStar(self):
    self.__guideStarXPos = 0
    self.__guideStarYPos = 0
Not surprisingly, 'autoGuide' is the new method that initiates all autoguiding operations. This method takes a boolean argument called 'autoGuideStar' to determine if autoguiding will use automatic or manual guide star selection. Additionally, the 'exposure' argument specifies the guide exposure length to use when automatic guide star selection is chosen and is not relevant when manual guide star selection is specified. If 'True' is passed in to the 'autoGuideStar' argument, the MaxIm DL attribute 'GuiderAutoSelectStar' is set to 'True', and, if a guide star has not been previously selected, a guide image is taken, and class attributes are populated with the position of the brightest guide star that MaxIm DL finds. All guide settings are then printed to the screen. If the 'autoGuideStar' argument is passed in as 'False', the MaxIm DL attribute 'GuiderAutoSelectStar' is set to 'False', and if a guide star has not previously been identified, the user is prompted to manually take a guide exposure, click on a potential guide star, then test tracking to make sure it is suitable. After pressing the 'ENTER' key, the user is then prompted to enter a length of exposure for all guide images and then all guide settings are printed to the screen.

After the guide star has been identified and set up, the guider binning is set then messages are printed to screen that show the current declination of the guider and the x- and y-coordinates of the chosen guide star. At this point, the autoguider is started with the MaxIm DL 'GuiderTrack' method inside a 'try', 'except', 'else' construct. The 'autoGuide' method then enters a loop that repeats until the guide errors fall under the pixel value defined by the class attribute '__guideSettleLimit'. If the errors do not settle within '__guideSettleMaxTime' seconds an error is returned to the calling routine. Assuming autoguiding starts okay and guide errors settle out, imaging can now begin.

The other two new methods added to this class are a procedure to stop the autoguider and a procedure to check to see if the autoguider is running. The 'checkGuiderRunning' method simply returns the result of MaxIm DL's 'GuiderRunning' attribute to the calling routine. The 'stopAutoGuide' method is complicated by a minor bug in MaxIm DL that causes a call to MaxIm's 'GuiderStop' method to always throw a 'TypeError' exception even though the autoguider is successfully stopped. This expected error is trapped in a 'except TypeError' block and any other exception generates an error statement to the screen.

cCAMERA UNIT TEST
The following listing shows the unit test code for this version of the 'cCamera' class. The script sets the simulator imaging camera up for full-frame mode with 1x1 binning then sets a CCD temperature and waits for the temperature to stabilize. As a first test, the script initiates autoguiding with automatic guide star selection and uses 1-second exposures for guide images. After making sure that autoguiding is in progress, it begins taking three 10-second images through the blue filter. When all images are complete, autoguiding is stopped. After resetting the guide star's x- and y-coordinates to zero, the process is started again with auto guide star selection and with 2.5-second guide images. This time, three 20-second images through the blue filter are taken before autoguiding is stopped. As a last test, the guide star cooridinates are zeroed out and autoguiding is started with manual guide star selection. After the user switches to MaxIm DL, takes a guide exposure, clicks on a guide star, and tests tracking for that star, autoguiding is started and three 15-second images through the blue filter are initiated. After all images are finished, autoguiding is stopped and the CCD is warmed to ambient temperature.
if __name__ == "__main__":

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

    # Setup Maxim DL to take a full frame image 
    testCamera.setFullFrame()
    # Setup binning for 1x1
    testCamera.setBinning(1)

    # Set CCD Temperature
    testCamera.setCCDTemp('-15C')
    # Goto CCD Temperature
    testCamera.gotoCCDTemp()
    
    # Start camera autoguiding with auto guide star select
    # Guide exposure = 1.0 second
    if testCamera.autoGuide(True,1.0):
        testCamera.stopAutoGuide()
    # Make sure autoguider is running
    if testCamera.checkGuiderRunning():
        # Take 3 images
        for i in range(3):
            # Expose filter slot 2 (Blue) for 10 seconds
            testCamera.exposeLight(10,2,'m51_B')
        # Stop autoguider after all images complete
        testCamera.stopAutoGuide()    
    else:
        print "ERROR - Autoguider not running as expected"

    # Reset guide star positions for next test
    testCamera.resetGuideStar()
    # Start camera autoguiding with auto guide star select
    # Guide exposure = 2.5 seconds
    if testCamera.autoGuide(True,2.5):
        testCamera.stopAutoGuide()
    # Make sure autoguider is running 
    if testCamera.checkGuiderRunning():
        # Take 3 images
        for i in range(3):
            # Expose filter slot 2 (Blue) for 20 seconds
            testCamera.exposeLight(20,2,'m51_B')
        # Stop autoguider after all images complete
        testCamera.stopAutoGuide()    
    else:
        print "ERROR - Autoguider not running as expected"
    
    # Reset guide star positions for next test
    testCamera.resetGuideStar()
    # Start camera autoguiding with manual guide star select
    if testCamera.autoGuide(False,0):
        testCamera.stopAutoGuide()
    if testCamera.checkGuiderRunning():
        # Take 3 images
        for i in range(3):
            # Expose filter slot 2 (Blue) for 15 seconds
            testCamera.exposeLight(15,2,'m51_B')
        # Stop autoguider after all images complete
        testCamera.stopAutoGuide()    
    else:
        print "ERROR - Autoguider not running as expected"

    # Warm the CCD to ambient
    testCamera.warmCCD()
RUNNING THE SCRIPT
Testing this version of the 'cCamera' class using the camera simulator follows the same procedure described in Part 1a of this series. The new listing for 'cCamera' can be downloaded from cCamera_1d.zip. Experiment with the script by changing guide star selection mode (automatic or manual) and/or guide image exposure time to verify the script operates as expected.

WHAT'S NEXT?
This is the final post that will cover the 'cCamera' class (although occasionally new attributes or methods may be added to the class, as needed). In the next post I will begin building the 'cMount' class. This class will interface with methods and attributes provided by the Sky's telescope control and planetarium classes.

CLICK HERE FOR NEXT POST IN THIS SERIES

Tuesday, May 24, 2011

Automated Astrophotography with Python - Part 1c

CCD TEMPERATURE CONTROL
A feature of most serious astronomical CCD cameras is the ability to set and regulate the temperature of the CCD chip via use of thermo-electric cooling (TEC). To this point, the 'cCamera' class has not addressed this feature. The listing show below now includes new constants, a class attribute, and three new methods that add TEC control to the script. (Python code added to this version of the script is shown in light cyan text; existing code is shown in yellow text.)
import time
import os
import win32com.client

LIGHT_PATH = r"c:\\astro_images\\"
SETTLE_TIME = 120  # minimum time for stable temp at set point 
SETTLE_MAX  = 480  # maximum time for temp to stabilize
ERROR = True
NOERROR = False

##------------------------------------------------------------------------------
## Class: cCamera
##------------------------------------------------------------------------------
class cCamera:
    def __init__(self):
        self.__CCDTemp = 'Skip' # CCD Temperature (Skip = don't change temp)

        print "Connecting to MaxIm DL..."
        self.__CAMERA = win32com.client.Dispatch("MaxIm.CCDCamera")
        self.__CAMERA.DisableAutoShutdown = True
        try:
            self.__CAMERA.LinkEnabled = True
        except:
            print "... cannot connect to camera"
            print "--> Is camera hardware attached?"
            print "--> Is some other application already using camera hardware?"
            raise EnvironmentError, 'Halting program'
        if not self.__CAMERA.LinkEnabled:
            print "... camera link DID NOT TURN ON; CANNOT CONTINUE"
            raise EnvironmentError, 'Halting program'

    def setCCDTemp(self,strtemp):
        if strtemp.upper() == 'SKIP':
            print "No CCD Cooling Specified"
            self.__CCDTemp = -99
            return NOERROR
        if strtemp.endswith("C"):
            try:
                flttemp = float(strtemp[:-1])
            except:
                print "ERROR: Specified CCD Temperature - Invalid format"
                return ERROR
        else:
            try:
                flttemp = float(strtemp)
            except:
                print "ERROR: Specified CCD Temperature - Invalid format"
                return ERROR
        self.__CCDTemp = flttemp
        return NOERROR
        
    def gotoCCDTemp(self):
        if self.__CCDTemp > -90:
            # set the CCD temperature set-point
            self.__CAMERA.TemperatureSetpoint = self.__CCDTemp
            print "CCD temperature setpoint: %0.2fC" % self.__CCDTemp
            # make sure the cooler is on, just in case
            if not self.__CAMERA.CoolerOn:
                print "Turning CCD cooler on"
                self.__CAMERA.CoolerOn = True
            print "Waiting for CCD temperature to stabilize"
            started = time.time()
            cnt = 0
            # Check CCD temp to stabilize
            while cnt < SETTLE_TIME and (time.time() - started) < SETTLE_MAX:
                currentTemp = self.__CAMERA.Temperature
                if (currentTemp < self.__CCDTemp - 0.5 or
                    currentTemp > self.__CCDTemp + 0.5):
                    cnt = 0
                time.sleep(1)
                cnt += 1
            if cnt == SETTLE_TIME and (time.time() - started) < SETTLE_MAX:
                print "CCD Temperature Stable at %0.2fC" % self.__CAMERA.Temperature
                try:
                    power = self.__CAMERA.CoolerPower
                    print "CCD Cooler Power: %d%%" % power
                except:
                    print "CCD cooler power could not be read"
                    return NOERROR
                return NOERROR
            else:
                print "CCD Temperature Did Not Stabilize"
                return ERROR
        else:
            print "Skipping temp stabilization."
            return NOERROR

    def warmCCD(self):
        if self.__CAMERA.CoolerOn:
            print "Starting to gradually warm CCD temperature to ambient"
            power = 100
            setTemp = self.__CAMERA.Temperature
            while power > 3:
                setTemp = setTemp + 5.0
                self.__CAMERA.TemperatureSetpoint = setTemp
                print "CCD temperature setpoint: %0.2fC" % setTemp
                print "Waiting 2.5 minutes for temperature to rise"
                time.sleep(150)
                print "CCD Cooler Temp : %0.2fC" % self.__CAMERA.Temperature
                try:
                    power = self.__CAMERA.CoolerPower
                    print "CCD Cooler Power: %d%%" % power
                except:
                    print "CCD cooler power could not be read"
                    power = 0
            print "CCD warming complete. Turning CCD cooler off"
            self.__CAMERA.CoolerOn = False
        else:
            print "CCD Cooler is off. CCD warming not necessary."

    def generateFilename(self,path,baseName):
        # path is the path to where the file will be saved
        baseName.replace(':', '_')      # colons become underscores
        baseName.replace(' ', '_')      # blanks become underscores
        baseName.replace('\\', '_')     # backslash becomes underscore
        # make sure the base filename has an '_' at the end
        if not baseName.endswith("_"):
            baseName = baseName + "_"
        # add 1 to use next available number
        seqMax = self.getSequenceNumber(path,baseName) 
        seqNext = seqMax + 1
        filename = "%s%s%05d.fit" % (path,baseName,seqNext)
        return filename

    def getSequenceNumber(self,path,baseName):
        # get a list of files in the image directory
        col = os.listdir(path)
        # Loop over these filenames and see if any match the basename
        retValue = 0
        for name in col:
            front = name[0:-9]
            back = name[-9:]
            if front == baseName:
                # baseName match found, now get sequence number for this file
                seqString = name[-9:-4]  # get last 5 chars of name (seq number)
                try:
                    seqInt = int(seqString)
                    if seqInt > retValue:
                        retValue = seqInt    # store greatest sequence number
                except:
                    pass
        return retValue

    def exposeLight(self,length,filterSlot,name):
        print "Exposing light frame..."
        self.__CAMERA.Expose(length,1,filterSlot)
        while not self.__CAMERA.ImageReady:
            time.sleep(1)
        print "Light frame exposure and download complete!"
        # save image
        filename = self.generateFilename(LIGHT_PATH,name)
        print "Saving light image -> %s" % filename
        self.__CAMERA.SaveImage(filename)

    def setFullFrame(self):
        self.__CAMERA.SetFullFrame()
        print "Camera set to full-frame mode"
        
    def setBinning(self,binmode):
        tup = (1,2,3)
        if binmode in tup:
            self.__CAMERA.BinX = binmode
            self.__CAMERA.BinY = binmode
            print "Camera binning set to %dx%d" % (binmode,binmode)
            return NOERROR
        else:
            print "ERROR: Invalid binning specified"
            return ERROR

##
##    END OF 'cCamera' Class
##
The first new method is called 'setCCDTemp' and takes a string (ex. '-15.5C') as its argument. This method first checks to see if the string is 'skip' (meaning don't change the currently set TEC regulated temperature) and if it is, sets the new class attribute '__CCDTemp' to -99. If the string is not 'skip', it strips off the trailing 'C' (if it exists) and then attempts to convert the string to a float. If successful, '__CCDTemp' is set to the float value, and if not successful, the method ends with an invalid format error message and returns an error flag back to the calling routine.

The second new method, 'gotoCCDTemp', sets the MaxIm DL property 'TemperatureSetpoint' to the desired temperature and sets the 'CoolerOn' property to TRUE to activate the TEC. The remainder of the method reads the CCD temperature and looks for it to be stable (setpoint temp plus or minus 0.5C) for a two minute period (SETTLE_TIME) before reading the TEC power and continuing. The method also imposes a maximum of 8 minutes (SETTLE_MAX) for the temperature to become stable before returning an error flag back to the calling routine.

The third method is a simple CCD warming routine that raises the chip's temperature by 5C, waits 150 seconds, then reads the TEC power. If the TEC power is above 3%, the temperature setpoint is raised another 5C and another 2.5 minute wait is initiated. This procedure repeats until the TEC power falls below 3% at which point the 'CoolerOn' property is set to FALSE. (The cooler is also turned off if the method is unable to read the TEC power - as is the case with the MaxIm DL autoguider simulator.) NOTE: This method is probably not really needed since the risk of damaging the CCD by thermal shock from turning off the cooler without warming is minimal or nonexistent.

cCAMERA UNIT TEST
The following listing shows the unit test code for this version of the 'cCamera' class. The script sets the simulator camera up for full-frame mode with 1x1 binning. It then sets a CCD temperature and takes three images then sets a different CCD temperature and takes three more images. The test script then sets the CCD temp to 'Skip' and takes more images to verify that the temperature doesn't change before warming the CCD back to ambient.
if __name__ == "__main__":

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

    # Setup Maxim DL to take a full frame image 
    testCamera.setFullFrame()
    # Setup binning for 1x1
    testCamera.setBinning(1)

    # Set CCD Temperature
    testCamera.setCCDTemp('-20C')
    # Goto CCD Temperature
    testCamera.gotoCCDTemp()
 
    # Take 3 images
    for i in range(3):
        # Expose filter slot 1 (Green) for 10 seconds
        testCamera.exposeLight(10,1,'m51_G')
    
    # Set CCD Temperature
    testCamera.setCCDTemp('-15C')
    # Goto CCD Temperature
    testCamera.gotoCCDTemp()
 
    # Take 3 images
    for i in range(3):
        # Expose filter slot 1 (Green) for 10 seconds
        testCamera.exposeLight(10,1,'m51_G')
    
    # Set CCD Temperature
    testCamera.setCCDTemp('skip')
    # Goto CCD Temperature
    testCamera.gotoCCDTemp()
 
    # Take 3 images
    for i in range(3):
        # Expose filter slot 1 (Green) for 10 seconds
        testCamera.exposeLight(10,1,'m51_G')
    
    # Warm the CCD to ambient
    testCamera.warmCCD()
RUNNING THE SCRIPT
Testing this version of the 'cCamera' class follows the same procedure described in Part 1a of this series. The new listing for 'cCamera' can be downloaded from cCamera_1c.zip. Experiment with the script by running it multiple times and by changing the CCD temperature, binning, filter, exposure length, or file base name in the unit test section to verify the script operates as expected.

WHAT'S NEXT?
In the next post I will continue to expand the 'cCamera' class by introducing more methods and properties that allow for autoguide tracking during imaging. I will also expand the unit test listing to verify correct operation of the features added to this class.

CLICK HERE FOR NEXT POST IN THIS SERIES

Monday, May 23, 2011

Automated Astrophotography with Python - Part 1b

SAVING IMAGES
The script of the 'cCamera' class presented in Part 1a of this series suffered from one major problem - it only provided for exposing and downloading an image but did not provide for saving the image to storage media. The script shown below addresses that deficiency. (Python code added to this version of the script is shown in light cyan text; existing code is shown in yellow text.)
import time
import os
import win32com.client

LIGHT_PATH = r"c:\\astro_images\\"
ERROR = True
NOERROR = False

##------------------------------------------------------------------------------
## Class: cCamera
##------------------------------------------------------------------------------
class cCamera:
    def __init__(self):
        print "Connecting to MaxIm DL..."
        self.__CAMERA = win32com.client.Dispatch("MaxIm.CCDCamera")
        self.__CAMERA.DisableAutoShutdown = True
        try:
            self.__CAMERA.LinkEnabled = True
        except:
            print "... cannot connect to camera"
            print "--> Is camera hardware attached?"
            print "--> Is some other application already using camera hardware?"
            raise EnvironmentError, 'Halting program'
        if not self.__CAMERA.LinkEnabled:
            print "... camera link DID NOT TURN ON; CANNOT CONTINUE"
            raise EnvironmentError, 'Halting program'

    def generateFilename(self,path,baseName):
        # path is the path to where the file will be saved
        baseName.replace(':', '_')      # colons become underscores
        baseName.replace(' ', '_')      # blanks become underscores
        baseName.replace('\\', '_')     # backslash becomes underscore
        # make sure the base filename has an '_' at the end
        if not baseName.endswith("_"):
            baseName = baseName + "_"
        # add 1 to use next available number
        seqMax = self.getSequenceNumber(path,baseName) 
        seqNext = seqMax + 1
        filename = "%s%s%05d.fit" % (path,baseName,seqNext)
        return filename

    def getSequenceNumber(self,path,baseName):
        # get a list of files in the image directory
        col = os.listdir(path)
        # Loop over these filenames and see if any match the basename
        retValue = 0
        for name in col:
            front = name[0:-9]
            back = name[-9:]
            if front == baseName:
                # baseName match found, now get sequence number for this file
                seqString = name[-9:-4]  # get last 5 chars of name (seq number)
                try:
                    seqInt = int(seqString)
                    if seqInt > retValue:
                        retValue = seqInt    # store greatest sequence number
                except:
                    pass
        return retValue

    def exposeLight(self,length,filterSlot,name):
        print "Exposing light frame..."
        self.__CAMERA.Expose(length,1,filterSlot)
        while not self.__CAMERA.ImageReady:
            time.sleep(1)
        print "Light frame exposure and download complete!"
        # save image
        filename = self.generateFilename(LIGHT_PATH,name)
        print "Saving light image -> %s" % filename
        self.__CAMERA.SaveImage(filename)

    def setFullFrame(self):
        self.__CAMERA.SetFullFrame()
        print "Camera set to full-frame mode"
        
    def setBinning(self,binmode):
        tup = (1,2,3)
        if binmode in tup:
            self.__CAMERA.BinX = binmode
            self.__CAMERA.BinY = binmode
            print "Camera binning set to %dx%d" % (binmode,binmode)
            return NOERROR
        else:
            print "ERROR: Invalid binning specified"
            return ERROR

##
##    END OF 'cCamera' Class
##
As you can see, two new methods have been added to the script. The 'generateFilename' script takes as arguments a path and a base name that usually should include the object name, filter, and binning used during the exposure (ex. m106_R_2x2). The first thing this method does is to take the 'baseName' parameter and strip out any characters that should not be in a filename and replace them with the '_' (underscore) character. It also appends an underscore character to 'baseName'. This method then calls the other new method, 'getSequenceNumber', and provides it with the path and new name arguments. The 'getSequenceNumber' method then uses the 'listdir' method of the 'os' object (hence, the new 'import os' at the top of the listing) to generate a list of all files in the directory specified by 'path'. The method then iterates through the list of directory files looking for one with a matching base name. If one is found, it checks the last five characters before '.fit' and tries to convert these characters into an integer. If successful, it returns the highest integer it can find back to the calling method. The 'generateFilename' method then uses this number to assign the next image with a similar base name the next number in the sequence.

Additional new code has been added to the existing 'exposeLight' method. This new code makes the call to the 'generateFilename' method, prints a message to the screen showing the path and filename of the saved image, then calls the MaxIm DL 'SaveImage' method to actually transfer the image to storage.

cCAMERA UNIT TEST
The following listing shows the unit test code for this version of the 'cCamera' class. The main difference is the requirement for the new 'name' parameter in the 'exposeLight' method.
if __name__ == "__main__":

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

    # Setup MaxIm DL to take a full frame image 
    testCamera.setFullFrame()
    # Setup binning for 2x2
    if not testCamera.setBinning(2):
        for i in range(4):
            # Expose filter slot 0 (Red) for 15 seconds
            testCamera.exposeLight(15,0,'m51_R_2x2')
    else:
        print "Images not taken due to previous error"
RUNNING THE SCRIPT
Testing this version of the 'cCamera' class follows the same procedure described in Part 1a of this series. Note: Remember that the path specified by the constant 'LIGHT_PATH' must exist prior to running the script. The new listing for 'cCamera' can be downloaded from cCamera_1b.zip. Experiment with the script by running it multiple times and by changing the binning, filter, exposure length, or file base name in the unit test section to verify the script operates as expected.

WHAT'S NEXT?
In the next post I will continue to expand the 'cCamera' class by introducing more methods and properties that allow for control of the camera's thermo-electric cooler (TEC). I will also expand the unit test listing to verify correct operation of the features added to this class.

CLICK HERE FOR NEXT POST IN THIS SERIES

Sunday, May 22, 2011

Automated Astrophotography with Python - Part 1a

GETTING THINGS TO WORK
The very first step in this process is to verify that Python and Python Extension for Windows are properly communicating with a suitable Windows application. In this case, we will use MaxIm DL and its built-in camera simulator.

THE cCAMERA CLASS
The first piece of code that we will look at is the 'cCamera' class. The Python code for 'cCamera' is shown below. (Note: Docstrings have been removed from this listing for brevity and readability purposes.)
import time
import win32com.client

ERROR = True
NOERROR = False

##------------------------------------------------------------------------------
## Class: cCamera
##------------------------------------------------------------------------------
class cCamera:
    def __init__(self):
        print "Connecting to MaxIm DL..."
        self.__CAMERA = win32com.client.Dispatch("MaxIm.CCDCamera")
        self.__CAMERA.DisableAutoShutdown = True
        try:
            self.__CAMERA.LinkEnabled = True
        except:
            print "... cannot connect to camera"
            print "--> Is camera hardware attached?"
            print "--> Is some other application already using camera hardware?"
            raise EnvironmentError, 'Halting program'
        if not self.__CAMERA.LinkEnabled:
            print "... camera link DID NOT TURN ON; CANNOT CONTINUE"
            raise EnvironmentError, 'Halting program'

    def exposeLight(self,length,filterSlot):
        print "Exposing light frame..."
        self.__CAMERA.Expose(length,1,filterSlot)
        while not self.__CAMERA.ImageReady:
            time.sleep(1)
        print "Light frame exposure and download complete!"

    def setFullFrame(self):
        self.__CAMERA.SetFullFrame()
        print "Camera set to full-frame mode"
        
    def setBinning(self,binmode):
        tup = (1,2,3)
        if binmode in tup:
            self.__CAMERA.BinX = binmode
            self.__CAMERA.BinY = binmode
            print "Camera binning set to %dx%d" % (binmode,binmode)
            return NOERROR
        else:
            print "ERROR: Invalid binning specified"
            return ERROR
            
##
##    END OF 'cCamera' Class
##
The first two lines of the listing are 'import' statements that identify and allow use of the 'time' and 'win32com.client' library objects in the script. The listing for the 'cCamera' class follows the 'import' statements and consists of four methods. The '__init__' method is the constructor method for 'cCamera' and is executed whenever an instance of the cCamera object is created. This method creates a '__CAMERA' object that is an instance of MaxIm DL's 'CCDCamera' object. The '__CAMERA' object then can be used to access all the properties and methods found in the scripting section of MaxIm DL's help file (for CCDCamera object). This method also uses 'try' and 'except' statements to determine if a link has been successfully established to the program. If MaxIm DL has not already been loaded and connection to the camera established manually, the script will load MaxIm DL and make the camera connection automatically.

Next in the listing is the 'exposeLight' method that takes as arguments a length of exposure (float) and an integer that specifies the filter to use for the exposure (zero-based). This method uses MaxIm DL's 'Expose' method to start the exposure and polls the 'ImageReady' parameter to determine when the exposure and image download have completed (indicated when 'ImageReady' goes True).

The third method of 'cCamera' calls the MaxIm DL method 'SetFullFrame' and the last method sets the 'BinX' and 'BinY' properties. For more details regarding 'SetFullFrame()', 'BinX', and 'BinY' refer to MaxIm DL help under 'Scripting' and under 'CCDCamera' methods or properties.

cCAMERA UNIT TEST
The listing given below is inserted onto the end of the source listing and allows unit testing the 'cCamera' class. The first step is to create an instance of the 'cCamera' object called 'testCamera'. In turn, the 'setFullFrame' and 'setBinning' methods of 'testCamera' are called to set MaxIm to full-frame mode and to set binning to 1x1. If 'setBinning' finished without error, the 'exposeLight' method is called and initiates a 12.5 second exposure through the simulator's blue filter (slot 2).
if __name__ == "__main__":

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

    # Setup MaxIm DL to take a full frame image 
    testCamera.setFullFrame()
    # Setup binning for 2x2
    if not testCamera.setBinning(2):
        # Expose filter slot 2 (Blue) for 12.5 seconds
        testCamera.exposeLight(12.5,2)
    else:
        print "Image not taken due to previous error"
RUNNING THE SCRIPT
Follow the steps below to execute the script described in this post:
  1. Download and extract the script from cCamera_1a.zip.
  2. Start MaxIm DL and open the Camera Control window. Under 'Setup Camera' for Camera 1, choose 'Simulator' and under 'Setup Filter' for Camera 1 choose 'Simulator' also. For Camera 2, choose 'Simulator' and under 'Setup Filter' for Camera 2 leave unselected.
  3. Change focus to the IDLE window containing your script listing, click the 'Run' menu item and pull down to 'Run Module', or simply press the F5 key.
  4. If everything is working correctly, you should see MaxIm DL kick off a 12.5 second exposure through the blue filter and eventually display an image from the simulated camera. You can inspect the FITS header to verify the binning, exposure, and filter for this image.
  5. Modify the parameters in the unit test section of the listing to take exposures of different lengths, through different filters, using different binnings to verify that all work as expected.
WHAT'S NEXT?
In the next post I will continue to expand the 'cCamera' class by introducing methods that allow saving light images to disk. I will also expand the unit test listing to verify correct operation of the new features added to this class.

CLICK HERE FOR NEXT POST IN THIS SERIES

Saturday, May 21, 2011

Automated Astrophotography with Python - Introduction

INTRODUCTION
As anyone past the beginner stage of astronomical imaging quickly finds out, an executive software application that controls all aspects of the imaging process becomes a desirable tool. There are various applications available (CCD Commander, ACP, CCD Autopilot) that I highly recommend and that perform very well but that also come with relatively high price tags. (Although the price is typically a fraction of the costs tied up in equipment and other software for the average imager.) However, an alternative to using an application that someone else has created and maintains is to create your own control software that directs the imaging process EXACTLY how you would wish it to be executed. This introductory post is the first in a series of posts that will detail how you can easily create and maintain your own executive application at no cost (other than your time learning and programming in Python).

WHY PYTHON?
Python is a agile programming language that is freely available, well supported, and easy to learn for beginners. Python syntax is very easy to read and understand, and the language supports object-oriented programming practices. Python is also an interpreted language which means there is no compilation process. You simply execute the script and it runs! Most importantly, Python supports the COM (Common Object Model) architecture under which most astronomical imaging software exposes their properties and methods to the outside world. This means that Python can seamlessly communicate with and control all major astronomical software applications like CCDSoft, MaxIm DL, the Sky, etc. For more information regarding the question of "Why Python?" click here.

WHERE TO GET PYTHON?
Python can be downloaded from this web page. Simply choose the installer that corresponds with your operating system and follow the prompts. (Although Python is written for Windows, Mac, and Unix platforms, you will need the Windows 32-bit or 64-bit OS installer since virtually all of the major astronomical imaging software packages you'll hook into are Windows-based.) The installer will also install IDLE which is the Python IDE (Integrated Development Environment) which you can use to write, test, and debug your scripts. Click here for a beginner's guide to downloading Python.

NOTE: All scripts presented in this series have been tested using the latest stable version of Python which is 2.7.1 as of the date of this post. The scripts should also work under the latest version of Python 3 but this claim has not been verified. Additionally, in order to have COM support, you must also load the Python Extensions for Windows package that corresponds to the version of Python that you have chosen to use. The latest package (Build 216 as of the date of this post) can be found here.

HOW TO LEARN PYTHON?
An on-line search of "Learning Python" or "Python tutorial" will return hundreds of on-line tutorials and PDF documents to help you get started. Another list of learning resources can be found here. One on-line PDF document that I found useful, even though it is somewhat dated, can be found here and the official Python tutorial can be found here.

BENEFITS OF CREATING AND RUNNING YOUR OWN SCRIPT
  • No Cost (other than your time)
  • Great opportunity to learn computer programming if you've never done it before or to learn a new programming language if you're already an experienced programmer
  • COMPLETE control of all aspects of the automated imaging process
  • Fine-tuned to the particular set of software that YOU use for imaging
  • You maintain the application...no waiting for someone else to get around to fixing a bug or implementing an improvement
  • Implement only the features that you want in exactly the way you want them to function

MY IMAGING ENVIRONMENT
The computer that I use for astronomical imaging is a 6-year old Toshiba laptop running Windows XP Home Edition. My Python script accesses the following imaging applications:
  • MaxIm DL 5 (For Camera Control)
  • The Sky6 (For Mount Control)
  • FocusMax (For Automated Focusing)
  • CCDSoft (For Plate Solving)
My imaging equipment is all portable. Since I don't have a suitable dark location near my home, I perform all image acquisition at a dark site that is approximately 200 miles away. Consequently, I don't have any script examples for dome control, weather monitoring equipment, or any other hardware that you would typically find with a permanently mounted setup. These features can be easily added to your Python script if required.

MY SCRIPT
The following is a very top-level description of my script's operation during acquisition of normal, light frames of astronomical objects. I will be describing in detail how each of these steps is implemented in the course of these posts.
  1. Read in the input text file that specifies the object to image and contains a list of images to take that includes filter, exposure, and number of frames. (An example of an input text file can be downloaded here.) Also, read in other options such as CCD temp, minimum altitude, warm CCD after all images etc. that apply to this set of images.
  2. Find the object in the Sky and wait for object to cross the meridian (since I only image on the west side of the sky). If the object has already crossed the meridian, continue immediately with the next step.
  3. Locate a suitable focus star (from a preselected list of candidate stars) that is close to the object, slew to the star, plate solve, then resync and reslew if needed. Using FocusMax's focus method, auto-focus through the specified filter then check focus using FocusMax's HFD measurement method.
  4. Slew to the object, plate solve, then resync and reslew if needed to reduce the pointing error.
  5. Expose the guide camera, identify a guide star, begin autoguiding, then wait until guide errors fall below a specified threshold.
  6. Begin acquiring images until the specified number of images have been captured or until it is time to perform the periodic focus check (required after 'x' number of images have be captured).
  7. When the periodic focus check is required, slew to the same focus star used in step 3 (with plate solve, resync, and reslew if required) and check focus using FocusMax's HFD measurement method. If the HFD has changed by a specified threshold, repeat FocusMax's focus method. Otherwise, continue directly to the next step.
  8. Slew to the object and plate solve, then resync and reslew if required.
  9. Return to step 6 until all images have been acquired for the current set of images.
  10. For the next set of images return to step 3.
  11. When all image sets have been acquired or when the object drops below the specified altitude, end the script. If required, warm the CCD to ambient temperature and/or slew the mount to a safe position.

DISCLAIMER
The scripts contained in the following posts have been written, debugged, and verified as working by me FOR MY PARTICULAR SETUP only. There are no guarantees that they will work, as is, with anyone else's setup. Therefore, I assume no responsibility for damage to equipment, lost time, or other losses anyone may incur. Furthermore, the scripts are not guaranteed to be without bugs that I haven't found yet. Indeed, they probably DO have bugs that I haven't found yet. The message is: Use the information contained in these posts at your own risk. The scripts in these posts are meant only as a STARTING POINT for your own scripts and are not meant to be used as is.

ACKNOWLEDGMENT
For some time I had been thinking about writing my own scripts even while using CCD Commander on a regular basis. It wasn't until I ran across the series of three articles in the on-line magazine "AstroPhoto Insight" titled "Automated Imaging on a Budget" by Joe Ulowetz that I found the jump-start that I needed. Specifically, at the end of the article, Joe graciously provided a link to allow a download of the complete Python code that controlled the automation described in his article. By pouring through Joe's code and using it as a starting point, I was eventually able to drop CCD Commander and begin the process of writing, testing, debugging, and, eventually, using my own executive software application.

WHAT'S NEXT?
In the next post, I will begin diving into the Python code. Specifically, I'll introduce and begin describing methods contained in the Camera class. I'll also show how these methods can be easily tested using only the camera simulator built into MaxIm DL.

CLICK HERE FOR NEXT POST IN THIS SERIES

Friday, May 20, 2011

M5 - Globular Cluster

(Click image to go to info page)

M5 is a large, bright globular cluster located in the constellation Serpens (Caput). This naked eye object can be found about 20 arc-minutes to the northwest of the 5th magnitude star 5 Serpentis. It's core is extremely compressed and slightly elliptical in shape. At magnitude 5.7, M5 lies at a distance of approximately 25,000 light years and was discovered in 1702 by Gottfried Kirch.

The data for this luminance-only photo was captured on 1 May and 2 May 2011. All images were taken at the Chiefland Astronomy Village in Chiefland, Florida. Complete details regarding this image and more images of M5 at various resolutions can be found by clicking on the image above or by clicking here.

Thursday, May 19, 2011

M106 - Spiral Galaxy

(Click image to go to info page)

The spiral galaxy M106 (NGC 4258) is a very large and bright galaxy located in the constellation Canes Venatici. M106 has a magnitude of approximately 8.3, lies at a distance of around 22 million light years, and was first discovered by Pierre Mechain in 1781.

The luminance data for the photo above was captured on 4 March 2011 and the color information was collected between 30 April and 2 May 2011. All images were taken at the Chiefland Astronomy Village in Chiefland, Florida. Complete details regarding this image and more images of M106 at various resolutions can be found by clicking on the image above or by clicking here.

Wednesday, May 18, 2011

STS-134 Endeavour SRB Separation

(Click image to enlarge)

Approximately one minute thirty seconds after launch the space shuttle SRBs (solid rocket boosters) are separated from the external fuel tank and fall back to the ocean to be recovered and reused.  The photo above has been enlarged and enhanced from this photo to show the back-end of Endeavour, the two SRBs falling away from the external fuel tank, and the exhaust trail that extends backwards along the flight path.  Click on the image above to view a larger version of this photo.

STS-134 Launch of Space Shuttle Endeavour

(Click image above to enlarge)

Here's a collection of photos of the May 16, 2011 launch of Endeavour made from my house which is approximately 30 miles south of the launch pad. Despite low clouds at the launch pad, the sky was mostly clear at my location. Click on the links below for other photos of the climb to orbit.

Photo 1
Photo 2
Photo 3
Photo 4
Photo 5
Photo 6
Photo 7
Photo 8
Photo 9
Photo 10
Photo 11
Photo 12

M63 - The Sunflower Galaxy

(Click image to go to info page)

M63, also cataloged as NGC 5055, is a prominent spiral galaxy in the constellation Canes Venatici. Due to its flowery appearance in photographs, it is also commonly referred to as the Sunflower galaxy. M63 shines with the light of over 10 billion suns and sports a disk approximately 86,000 light years in diameter. This magnitude 8.6 galaxy is located approximately 23.5 million light years away and was first discovered by Pierre Mechain in 1779.

The photo above was captured between 30 April and 2 May 2011 at the Chiefland Astronomy Village in Chiefland, Florida. Complete details regarding this image and more images of M63 at various resolutions can be found by clicking on the image above or by clicking here.