Skip to content

Commit 55fd32e

Browse files
Merge pull request #110 from CTetford/develop
AutoPA
2 parents 77d698e + 7089273 commit 55fd32e

File tree

4 files changed

+208
-40
lines changed

4 files changed

+208
-40
lines changed

Software/Addons/AutoPA/autopa_modules/indi.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import io
77
from astropy.coordinates import Angle
88
from astropy import units as u
9-
9+
1010
class IndiClient(PyIndi.BaseClient):
1111
def __init__(self):
1212
super(IndiClient, self).__init__()
@@ -34,7 +34,7 @@ def serverConnected(self):
3434
pass
3535
def serverDisconnected(self, code):
3636
pass
37-
37+
3838
def capture(indiclient, ccd, exposure, outputfile, blobEvent):
3939
# Let's take some pictures
4040
device_ccd=indiclient.getDevice(ccd)

Software/Addons/AutoPA/autopa_modules/platesolve.py

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
#!/usr/bin/env python3
22

33
import subprocess
4-
import re, math, os
4+
import re, math, os, time
55
from pathlib import Path
66
from astropy.time import Time
77
from astropy.coordinates import SkyCoord, EarthLocation, AltAz, Angle
88
from astropy import units as u
9+
from astropy.wcs import WCS
910
from autopa_modules import LX200
1011
import contextlib
1112

@@ -38,17 +39,11 @@ def solve_ASTAP (filename, RA, DEC, radius, pixel_resolution):
3839
cmd = f"/opt/astap/astap -r 30 -speed auto -o /tmp/solution -f {filename}"
3940
try:
4041
output = str(subprocess.check_output(cmd, shell=True))
42+
output = WCS("/tmp/solution.wcs")
43+
object = SkyCoord(output.wcs.crval[0], output.wcs.crval[1], frame='fk5', unit='deg')
4144
except:
42-
return ("Failed", "Failed")
43-
textfile = open("/tmp/solution.wcs", 'r')
44-
filetext = textfile.read()
45-
textfile.close()
46-
try:
47-
RA = re.search("CRVAL1\s*=\s*(\d\.\d+E\+\d+)", filetext).group(1)
48-
DEC = re.search("CRVAL2\s*=\s*(\d\.\d+E\+\d+)", filetext).group(1)
49-
except:
50-
return ("Failed", "Failed")
51-
return (Angle(RA, unit=u.deg).to_string(unit=u.hour), Angle(DEC, unit=u.deg).to_string(unit=u.degree))
45+
return ("Failed")
46+
return (object)
5247

5348
def solve_ASTROMETRY (filename, RA, DEC, radius, pixel_resolution):
5449
config_file="/home/astroberry/.local/share/kstars/astrometry/astrometry.cfg"
@@ -71,7 +66,7 @@ def solve_ASTROMETRY (filename, RA, DEC, radius, pixel_resolution):
7166
DEC_S = result.group("DEC_S")
7267
return (f"{RA_H}h{RA_M}m{RA_S}s", f"{DEC_D}d{DEC_M}m{DEC_S}s")
7368

74-
def polarCalc(mylat, mylong, myelev, observing_time, p1RA, p1DEC, p2RA, p2DEC, p3RA, p3DEC):
69+
def polarCalc(mylat, mylong, myelev, observing_time, p1, p2, p3):
7570
#iers.conf.auto_download = False
7671
#iers.conf.auto_max_age = None
7772

@@ -82,17 +77,17 @@ def polarCalc(mylat, mylong, myelev, observing_time, p1RA, p1DEC, p2RA, p2DEC, p
8277
observing_location = EarthLocation(lat=mylat*u.deg, lon=mylong*u.deg, height=myelev*u.m)
8378

8479
#Create coordinate objects for each point
85-
p1 = SkyCoord(p1RA, p1DEC, unit='deg')
86-
p2 = SkyCoord(p2RA, p2DEC, unit='deg')
87-
p3 = SkyCoord(p3RA, p3DEC, unit='deg')
80+
#p1 = SkyCoord(p1RA, p1DEC, unit='deg')
81+
#p2 = SkyCoord(p2RA, p2DEC, unit='deg')
82+
#p3 = SkyCoord(p3RA, p3DEC, unit='deg')
8883
p1X = (90 - p1.dec.degree) * math.cos(p1.ra.radian)
8984
p1Y = (90 - p1.dec.degree) * math.sin(p1.ra.radian)
9085
p2X = (90 - p2.dec.degree) * math.cos(p2.ra.radian)
9186
p2Y = (90 - p2.dec.degree) * math.sin(p2.ra.radian)
9287
p3X = (90 - p3.dec.degree) * math.cos(p3.ra.radian)
9388
p3Y = (90 - p3.dec.degree) * math.sin(p3.ra.radian)
9489

95-
#Calculate center of circle using three points in the complex plane. DEC is treated as unitless for the purposes of the calculation.
90+
#Calculate center of circle using three points in the complex plane. RA/DEC are treated as unitless for the purposes of the calculation.
9691
x, y, z = complex(p1X,p1Y), complex(p2X,p2Y), complex(p3X,p3Y)
9792
w = z-x
9893
w /= y-x
@@ -104,26 +99,30 @@ def polarCalc(mylat, mylong, myelev, observing_time, p1RA, p1DEC, p2RA, p2DEC, p
10499
resultDEC = (90 - math.sqrt(resultX**2 + resultY**2))
105100
resultRA = math.atan2(resultY, resultX)*360 / (2*math.pi)
106101
if resultRA < 0:
107-
resultRA = (180-abs(resultRA))+180
102+
resultRA = (180-abs(resultRA))+180
108103

109-
#Create coordinate object for current alignment offset
110-
offset = SkyCoord(resultRA, resultDEC, frame='itrs', unit='deg', representation_type='spherical', obstime=observing_time)
111104
print(f"Current alignment in RA/DEC: {Angle(resultRA*u.deg).to_string(u.hour, precision=2)}/{Angle(resultDEC*u.deg).to_string(u.degree, precision=2)}.")
112-
113-
#Create coordinate object for pole
114-
pole = SkyCoord(0, 90, frame='itrs', unit='deg', representation_type='spherical', obstime=observing_time)
115105

116-
#Create coordinate object for pole
117-
poleAzAlt = pole.transform_to(AltAz(obstime=observing_time,location=observing_location))
118-
print(f"True polar alignment in Az./Alt.: 0h00m00s/{poleAzAlt.alt.to_string(u.degree, precision=2)}.")
119-
120-
#Transform current alignment to Alt/Az coordinate system
121-
offsetAzAlt = offset.transform_to(AltAz(obstime=observing_time,location=observing_location))
106+
#Create alt/az coordinate object for pole
107+
poleAzAlt = RADECtoAltAz(observing_location, observing_time, 0, 90)
108+
print(f"True polar alignment in Az./Alt.: 00h00m00s/{poleAzAlt.alt.to_string(u.degree, precision=2)}.")
109+
110+
#Create alt/az coordinate object for current alignment
111+
offsetAzAlt = RADECtoAltAz(observing_location, observing_time, resultRA, resultDEC)
122112
print(f"Current alignment in Az./Alt.: {offsetAzAlt.az.to_string(u.hour, precision=2)}/{offsetAzAlt.alt.to_string(u.degree, precision=2)}.")
123113

124114
#Calculate offset deltas from pole
125115
#Normalize the azimuth values to between -180 and 180 degrees prior to determining offset.
126116
errorAz = (((poleAzAlt.az.deg + 180) % 360 - 180)-((offsetAzAlt.az.deg + 180) % 360 - 180))*60
127117
errorAlt = (poleAzAlt.alt.deg-offsetAzAlt.alt.deg)*60
128118

129-
return errorAz, errorAlt
119+
return errorAz, errorAlt
120+
121+
def RADECtoAltAz(observing_location, observing_time, RA, DEC):
122+
#Create coordinate object for current alignment offset
123+
offset = SkyCoord(RA, DEC, frame='itrs', unit='deg', representation_type='spherical', obstime=observing_time)
124+
125+
#Transform current alignment to Alt/Az coordinate system
126+
AzAlt = offset.transform_to(AltAz(obstime=observing_time,location=observing_location))
127+
128+
return AzAlt

Software/Addons/AutoPA/polaralign_autocalibration.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def polarcalibrate(axis, indiclient, iteration, observing_location, telescope, c
2222
calibration_distance = calibration_distance * -1
2323

2424
#Initialize steppers to a known backlash position
25-
initSteppers(indiclient, telescope, serialport, backlashCorrection)
25+
initSteppers(axis, indiclient, telescope, serialport, backlashCorrection)
2626

2727
p1RA, p1DEC, p1Time = captureSolve(indiclient, RA, DEC, radius, exposure, telescope, ccd, blobEvent, pixel_resolution, "altazCalibration1.fits")
2828
p1Time = datetime.utcfromtimestamp(p1Time)
@@ -85,21 +85,21 @@ def captureSolve(indiclient, RA, DEC, radius, exposure, telescope, ccd, blobEven
8585
print ("Image could not be solved after 5 attempts. Exiting program.")
8686
exit()
8787
solveTimeFinish = time.time()
88-
print (f"Image solved in {(solveTimeFinish - solveTimeStart):.2f} seconds. Coordinates are RA {resultRA}, DEC {resultDEC}")
88+
print (f"Image solved in {(solveTimeFinish - solveTimeStart):.2f} seconds. Coordinates are RA {Angle(resultRA, unit=u.deg).to_string(unit=u.hour)}, DEC {Angle(resultDEC, unit=u.deg).to_string(unit=u.degree)}")
8989

9090
return (resultRA, resultDEC, captureTime)
9191

92-
def initSteppers(indiclient, telescope, serialport, backlashCorrection):
92+
def initSteppers(axis, indiclient, telescope, serialport, backlashCorrection):
9393

9494
print ("Initializing steppers")
9595
#Disconnect telescope mount from INDI to free up serial port for Alt/Az adjustments
9696
indi.disconnectScope(indiclient, telescope)
9797

9898
#Increase both Az & Alt by approximately 16-steps
99-
LX200.sendCommand(f":MAZ{backlashCorrection[0]}#", serialport)
100-
time.sleep(0.5)
101-
LX200.sendCommand(f":MAL{backlashCorrection[1]}#", serialport)
102-
99+
if axis == "az":
100+
LX200.sendCommand(f":MAZ{backlashCorrection[0]}#", serialport)
101+
elif axis == "alt":
102+
LX200.sendCommand(f":MAL{backlashCorrection[1]}#", serialport)
103103
time.sleep(3)
104104

105105
#Re-connect telescope mount to INDI before disconnecting from the INDI server
@@ -188,11 +188,11 @@ def waitForMotors(serialport):
188188
else:
189189
backlashCorrection = [-4.2665,-0.1647]
190190

191-
iteration = input("How many calibration iterations to perform? (Default is 3) ")
191+
iteration = input("How many calibration iterations to perform? (Default is 4) ")
192192
try:
193193
iteration = int(iteration)
194194
except:
195-
iteration = 3
195+
iteration = 4
196196

197197
#Create location object based on lat/long/elev
198198
observing_location = EarthLocation(lat=mylat*u.deg, lon=mylong*u.deg, height=myelev*u.m)
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
#!/usr/bin/env python3
2+
from astropy import units as u
3+
from astropy.coordinates import SkyCoord, EarthLocation, AltAz, Angle
4+
from astropy.time import Time
5+
from astropy.utils import iers
6+
import sys, subprocess, time, math
7+
from autopa_modules import indi
8+
from autopa_modules import platesolve
9+
from autopa_modules import LX200
10+
from datetime import datetime
11+
from statistics import mean
12+
import argparse
13+
14+
def slewCaptureSolve(indiclient, RA, DEC, radius, exposure, telescope, ccd, blobEvent, pixel_resolution, filename="platesolve.fits"):
15+
print (f"Slewing \"{telescope}\" to {Angle(RA*u.deg).to_string(unit=u.hour)}, {Angle(DEC*u.deg).to_string(unit=u.degree)}")
16+
indi.slewSync(indiclient, telescope, RA, DEC)
17+
#Wait one second to let the telescope stop moving and let any motor vibrations reduce
18+
time.sleep(1)
19+
20+
original_exposure = exposure
21+
solved = False
22+
while not solved:
23+
print (f"Capturing {exposure} second exposure on \"{ccd}\"")
24+
captureTime = indi.capture(indiclient, ccd, exposure, filename, blobEvent)
25+
26+
print (f"Attempting plate solving in the region of: RA {Angle(RA*u.deg).to_string(unit=u.hour)}, DEC {Angle(DEC*u.deg).to_string(unit=u.degree)}.")
27+
solveTimeStart = time.time()
28+
#resultRA, resultDEC = platesolve.solve_ASTROMETRY(filename, RA, DEC, radius, pixel_resolution)
29+
result = platesolve.solve_ASTAP(filename, RA, DEC, radius, pixel_resolution)
30+
if not isinstance(result, str):
31+
solved = True
32+
else:
33+
print("Image could not be solved. Attempting another capture with increased exposure time (Max is 10 second exposure)")
34+
if exposure < 10:
35+
exposure += 1
36+
elif exposure - original_exposure >= 5:
37+
print ("Image could not be solved. Exiting.")
38+
solveTimeFinish = time.time()
39+
print (f"Image solved in {(solveTimeFinish - solveTimeStart):.2f} seconds.")
40+
#print (f"Image solved. Coordinates are RA {Angle(resultRA, unit=u.deg).to_string(unit=u.hour)}, DEC {Angle(resultDEC, unit=u.deg).to_string(unit=u.degree)}")
41+
42+
return (result, captureTime)
43+
44+
def initSteppers(indiclient, telescope, serialport, backlashCorrection):
45+
46+
print ("Initializing steppers")
47+
#Disconnect telescope mount from INDI to free up serial port for Alt/Az adjustments
48+
indi.disconnectScope(indiclient, telescope)
49+
50+
#Increase both Az & Alt by approximately 16-steps
51+
LX200.sendCommand(f":MAZ{backlashCorrection[0]}#", serialport)
52+
LX200.sendCommand(f":MAL{backlashCorrection[1]}#", serialport)
53+
54+
time.sleep(3)
55+
56+
#Re-connect telescope mount to INDI before disconnecting from the INDI server
57+
indi.connectScope(indiclient, telescope)
58+
return
59+
60+
parser = argparse.ArgumentParser(usage='%(prog)s [mylat] [mylong] [myelev] [options]', description='OpenAstroTracker AutoPA: This tool is used to automatically rotate the mount, capture images,\
61+
plate solve, and calculate the polar alignment error of the OAT. Serial commands are automatically issued to the OAT to adjust the motorized altitude/azimuth axis to correct this error.')
62+
parser.add_argument("mylat", help="your latitude in degrees", type=float)
63+
parser.add_argument("mylong", help="your longitude in degrees", type=float)
64+
parser.add_argument("myelev", help="your elevation in metres", type=float)
65+
parser.add_argument("--serialport", help="serial port address for the OAT (default is /dev/ttyACM0)", type=str)
66+
parser.add_argument("--targetRA", help="initial starting RA in degrees (default is 0)", type=float)
67+
parser.add_argument("--targetDEC", help="initial starting DEC in degrees (default is 85)", type=float)
68+
parser.add_argument("--exposure", help="exposure time in seconds (default is 8 seconds)", type=float)
69+
parser.add_argument("--telescope", help="name of INDI telescope to control movement (default is Ekos \"Telescope Simulator\")", type=str)
70+
parser.add_argument("--ccd", help="name of INDI CCD for capturing exposures (default is Ekos \"CCD Simulator\")", type=str)
71+
parser.add_argument("--radius", help="field radius of plate solving", type=float)
72+
parser.add_argument("--pixelSize", help="CCD pixel size in micrometres. Used to decrease plate solving time.", type=float)
73+
parser.add_argument("--pixelX", help="Quantity of pixels in the X direction.", type=float)
74+
parser.add_argument("--pixelY", help="Quantity of pixels in the Y direction.", type=float)
75+
parser.add_argument("--focalLength", help="Lens focal length in millimetres. Used to decrease plate solving time.", type=float)
76+
parser.add_argument("--nomove", help="Run AutoPA sequence but do not move the steppers.", action="store_true")
77+
args = parser.parse_args()
78+
mylat = args.mylat
79+
mylong = args.mylong
80+
myelev = args.myelev
81+
if args.serialport:
82+
serialport = args.serialport
83+
else:
84+
serialport = "/dev/ttyACM0"
85+
if args.targetRA:
86+
targetRA = args.targetRA
87+
else:
88+
targetRA = 0
89+
if args.targetDEC:
90+
targetDEC = args.targetDEC
91+
else:
92+
targetDEC = 85
93+
if args.exposure:
94+
exposure = args.exposure
95+
else:
96+
exposure = 8.0
97+
if args.telescope:
98+
telescope = args.telescope
99+
else:
100+
telescope = "Telescope Simulator"
101+
if args.ccd:
102+
ccd = args.ccd
103+
else:
104+
ccd = "CCD Simulator"
105+
if args.radius:
106+
radius = args.radius
107+
else:
108+
radius = 30
109+
if args.pixelSize and args.focalLength:
110+
pixel_resolution = (args.pixelSize/args.focalLength)*206.265
111+
else:
112+
pixel_resolution = 0
113+
if args.nomove:
114+
nomove = True
115+
else:
116+
nomove = False
117+
if args.pixelX and args.pixelY:
118+
pixelX = args.pixelX
119+
pixelY = args.pixelY
120+
if pixel_resolution != 0:
121+
radius = (max(pixelX, pixelY) * pixel_resolution) / 3600 * 1.1 #Calculate FOV and double for range of plate solving
122+
else:
123+
pixelX = 0
124+
pixelY = 0
125+
126+
startTime = time.time()
127+
solveTimeFinish = time.time()
128+
129+
#Backlash correction in arcminutes (approximately 16 full steps of each axis)
130+
backlashCorrection = [4.2665,0.1647]
131+
132+
#Connect to indi server
133+
indiclient, blobEvent = indi.indiserverConnect()
134+
135+
if not nomove:
136+
#Initialize steppers to a known backlash position
137+
initSteppers(indiclient, telescope, serialport, backlashCorrection)
138+
139+
#Execute polar alignment routine to capture and solve three times 30 degrees apart
140+
p1, p1Time = slewCaptureSolve(indiclient, (targetRA % 360), targetDEC, radius, exposure, telescope, ccd, blobEvent, pixel_resolution, "capture1.fits")
141+
p2, p2Time = slewCaptureSolve(indiclient, ((targetRA+30) % 360), targetDEC, radius, exposure, telescope, ccd, blobEvent, pixel_resolution, "capture2.fits")
142+
p3, p3Time = slewCaptureSolve(indiclient, ((targetRA+60) % 360), targetDEC, radius, exposure, telescope, ccd, blobEvent, pixel_resolution, "capture3.fits")
143+
144+
#Calculate capture time based on the average timestamps of each image
145+
observing_time = datetime.utcfromtimestamp(mean((p1Time, p2Time, p3Time)))
146+
print (f"Time of captures is {observing_time} (UTC).")
147+
148+
#Calculate polar alignment error
149+
result = platesolve.polarCalc(mylat, mylong, myelev, observing_time, p1, p2, p3)
150+
print(f"Azimuth error correction is: {result[0]:.4f} arcminutes.")
151+
print(f"Altitude error correction is: {result[1]:.4f} arcminutes.")
152+
153+
if not nomove:
154+
#Disconnect telescope mount from INDI to free up serial port for Alt/Az adjustments
155+
print (f"Disconnecting {telescope} from INDI server")
156+
indi.disconnectScope(indiclient, telescope)
157+
print ("Disconnected.")
158+
159+
#Adjust alt/az axis
160+
print ("Adjusting altitude/azimuth axes.")
161+
platesolve.adjustAltAz(result, serialport, backlashCorrection)
162+
163+
#Re-connect telescope mount to INDI before disconnecting from the INDI server
164+
indi.connectScope(indiclient, telescope)
165+
166+
#Disconnect from indi server
167+
indi.indiserverDisconnect(indiclient)
168+
169+
print (f"Polar alignment took {time.strftime('%Mm%Ss', time.gmtime(time.time() - startTime))}.")

0 commit comments

Comments
 (0)