1 """ rotator module for dazle"""
2
3
4
5
6
7 import ConfigParserRT
8 import datetime
9 import logging
10 import math
11 import slalib
12 import serial
13 import threading
14 import time
15
16
17 debug=True
18 verbose=True
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
40 """Base class for exceptions in this module."""
41 pass
42
44 """
45 Raised where invalid units are specified.
46 """
48 self.function = function
49 self.unit = unit
50
52 return "rotator.UnitsError('%s', '%s')" % (self.function, self.unit)
53
55 return "Invalid units '%s' specified in call to %s" % \
56 (self.unit, self.function)
57
59 """
60 Raised when problems occur when parsing strings with slalib routines.
61 """
62 - def __init__(self, function, status, startPos, endPos):
63 self.function = function
64 self.status = status
65 self.startPos = startPos
66 self.endPos = endPos
67
69 """
70 Raised when an error code is returned by a slalib routine.
71 """
73 self.function = function
74 self.status = status
75
77 """
78 Worker thread intended to be used by an instance of rotator.rotator in
79 order to perform the operations required for rotator tracking in the
80 background.
81 """
82 - def __init__(self, tele, tgt, dt, low, high, port, stop, lockedOn, logger, \
83 logData=False):
84 """
85 Arguments:
86
87 tele: A rotator.telescope object describing the observatory at
88 which the instrument is situated.
89 tgt: A rotator.target object representing the target field
90 dt: The interval at which tracking adjustments should be made,
91 in seconds.
92 low: The lower limit of allowed motion, a rotator.angle object.
93 high: The upper limit of allowed motion, a rotator.angle object.
94 port: A serial.Serial object for communication with the hardware.
95 stop: A threading.Event object used to terminate the tracking
96 loop.
97 lockedOn: A threading.Event object used to indicate tracking within
98 tolerances.
99 logger: A logging.Logger instance used to output information.
100 logData: Boolean to indicate whether tracking status information
101 should be logged to the file 'dicstrack.log'. Default False.
102 """
103 if isinstance(tele, telescope):
104 self.__telescope = tele
105 else:
106 raise TypeError('rotator.tracker() expected rotator.telescope,' \
107 + ' got %s' % type(tele))
108 if isinstance(tgt, target):
109 self.__target = tgt
110 else:
111 raise TypeError('rotator.tracker() expected rotator.target,' \
112 + ' got %s' % type(tgt))
113 if hasattr(stop, 'clear'):
114 self.__stop = stop
115 else:
116 raise TypeError('rotator.tracker() expected threading.Event,' \
117 + ' got %s' % type(stop))
118
119 if hasattr(lockedOn, 'clear'):
120 self.lockedOn = lockedOn
121 else:
122 raise TypeError('rotator.tracker() expected threading.Event,' \
123 + ' got %s.' % type(stop))
124
125 if isinstance(logger, logging.Logger):
126 self.logger = logger
127 else:
128 raise TypeError('rotator.tracker() expected logging.Logger,' \
129 + ' got %s' % type(logger))
130
131 self.__dt = float(dt)
132
133 if isinstance(low, angle):
134 self.__lower = low
135 else:
136 raise TypeError('rotator.tracker() expected rotator.angle,' \
137 + ' got %s' % type(low))
138
139 if isinstance(high, angle):
140 self.__upper = high
141 else:
142 raise TypeError('rotator.tracker() expected rotator.angle,' \
143 + ' got %s' % type(high))
144
145 if isinstance(port, serial.Serial):
146 self.__port = port
147 else:
148 raise TypeError('rotator.tracker() expected serial.Serial,' \
149 + ' got %s' % type(port))
150
151 self.__logData = logData
152 if logData:
153 try:
154 self.__trackLogFile = file('dicstrack.log', 'w')
155 except IOError:
156 self.logger.warning(\
157 "Couldn't open track log file 'dicstrack.log")
158 self.__logData = False
159
160 self.__tolerance = angle(2E-4)
161 threading.Thread.__init__(self)
162
163
165 """
166 Don't call this directly, use start().
167 """
168 self.logger.info('Beginning to track target %s' % self.__target.name)
169
170
171
172 self.__telescope.resetField()
173
174
175
176
177 max_time = datetime.datetime.fromtimestamp(0.0)
178 for i in range(0, 4):
179 lt = self.limit_time()
180 if lt and lt > max_time:
181 max_time = lt
182 index = i
183 self.__target.unwind(1)
184 self.__target.unwind(index)
185
186 self.logger.info('Using position angle of %f' % \
187 self.__target.getPA().getAngle('d'))
188 self.logger.info('Unwind needed by %s UTC.' % \
189 max_time.strftime('%H:%M:%S'))
190
191 warned10mins = False
192 warned5mins = False
193 lockOnCount = 0
194
195 self.__port.timeout = 5
196 self.__port.flushInput()
197
198 count = 0
199
200 start_time = time.time()
201
202 utc = datetime.datetime.utcfromtimestamp(start_time + self.__dt)
203 firstpos, az, zd, pa, HAObs, decObs, RAObs = self.calcRotPos(utc)
204 self.logger.info('Initial telescope pointing: Az %f, zd %f.' % \
205 (az.getAngle('d'), zd.getAngle('d')))
206 self.logger.info('Initial parallactic angle: %f.' % pa.getAngle('d'))
207 self.logger.info('Initial observed hour angle: %f.' % \
208 HAObs.getAngle('h'))
209 self.logger.info('Target observed place: RA %s, dec %s.' % \
210 (RAObs.getString('h'), decObs.getString('d')))
211 if zd > angle(75.0, 'd'):
212 self.logger.critical('Target too low to observe! Not tracking.')
213 return
214 self.logger.info('Initial rotator position: %f.' % \
215 firstpos.getAngle('d'))
216
217 self.__port.write('T')
218 self.__port.write('%e\n' % self.__dt)
219 self.__port.write('%e\n' % firstpos.getAngle())
220
221 while not self.__stop.isSet():
222 if self.__logData:
223 self.__trackLogFile.write(utc.isoformat() + \
224 '\t%f\t%f\t%f\t%f\t%f\t%f\t' % \
225 (az.getAngle('d'), zd.getAngle('d'),\
226 pa.getAngle('d'), \
227 HAObs.getAngle('h'), \
228 decObs.getAngle('d'), \
229 RAObs.getAngle('h')))
230 count += 1
231 next_time = start_time + self.__dt * count
232 utc = datetime.datetime.utcfromtimestamp(next_time + self.__dt)
233
234
235 if not warned10mins and \
236 max_time - utc < datetime.timedelta(minutes=10):
237 self.logger.warning('10 minutes to rotator limit.')
238 warned10mins = True
239 if not warned5mins and \
240 max_time - utc < datetime.timedelta(minutes=5):
241 self.logger.warning('5 minutes to rotator limit.')
242 warned5mins = True
243
244 if self.__logData:
245 next_pos, az, zd, pa, HAObs, decObs, RAObs = \
246 self.calcRotPos(utc)
247 else:
248 next_pos = self.calcRotPos(utc)[0]
249
250 if next_pos <= self.__lower:
251 self.logger.critical('Reached lower limit of rotator travel!')
252 break
253 elif next_pos >= self.__upper:
254 self.logger.critical('Reached upper limit of rotator travel!')
255 break
256
257 self.__port.write('%e\n' % next_pos.getAngle())
258
259 delay = next_time - time.time()
260 if delay > 0:
261 time.sleep(delay)
262
263 self.__port.write('M')
264
265 response = self.__port.readline()
266 if response == 'O\n':
267 position = angle(float(self.__port.readline()))
268 error = angle(float(self.__port.readline()))
269
270
271
272 if self.__logData:
273 self.__trackLogFile.write('%f\t%f\n' % \
274 (position.getAngle('d'),
275 error.getAngle('d')))
276
277 elif response == 'U\n':
278 self.logger.critical('Position rejected as outside upper' + \
279 'limit by Taranis!')
280 self.logger.critical('Position sent was %f.' % \
281 next_pos.getAngle('d'))
282 self.logger.critical('Upper limit is %f.' % \
283 self.__upper.getAngle('d'))
284 break
285 elif response == 'L\n':
286 self.logger.critical('Position rejected as outside lower' + \
287 'limit by Taranis!')
288 self.logger.critical('Position sent was %f.' % \
289 next_pos.getAngle('d'))
290 self.logger.critical('Lower limit is %f.' % \
291 self.__lower.getAngle('d'))
292 break
293 elif response == 'u\n':
294 self.logger.critical('Rotator moved outside upper limit!')
295 break
296 elif response == 'l\n':
297 self.logger.critical('Rotator moved outside lower limit!')
298 break
299 elif response == '1\n':
300 self.logger.critical('Rotator hit lower limit switch!')
301 break
302 elif response == '2\n':
303 self.logger.critical('Rotator hit upper limit switch!')
304 break
305 else:
306 self.logger.critical("Unexpected response '%s' from Taranis!"\
307 % response)
308 break
309
310 if not self.lockedOn.isSet() and abs(error) < self.__tolerance:
311 if lockOnCount < 10:
312 lockOnCount += 1
313 else:
314 self.lockedOn.set()
315 self.logger.info('Tracking within tolerance.')
316
317 if self.lockedOn.isSet() and abs(error) > self.__tolerance:
318 lockOnCount = 0
319 self.lockedOn.clear()
320 self.logger.warning('Tracking tolerance exceeded.')
321
322 self.__port.write('0.0\n')
323 self.__port.write('C')
324 self.lockedOn.clear()
325 if self.__logData:
326 self.__trackLogFile.close()
327 self.logger.info('Tracking stopped')
328 az, zd = self.__telescope.getPointing(self.__target, utc)[:2]
329 self.logger.info('Final telescope pointing: Az %f, zd %f.' % \
330 (az.getAngle('d'), zd.getAngle('d')))
331 self.logger.info('Final rotator position: %f.' % \
332 next_pos.getAngle('d'))
333
335 """
336 Based on the currently set target and telescope calculates the
337 required rotator position for the UTC given as a datetime.datetime
338 object.
339
340 DAzLE will mount on VLT UT3 Nasmyth platform 'A', which is on the
341 LEFT hand side when looking in the direction in which the telescope
342 if pointing. For the left hand Nasmyth platform the position angle
343 of the sky image on the detector with the rotator at zero degrees is
344 equal to parallactic angle + zenith distance + 180 degrees. The
345 required rotator position is therefore requested PA - parallactic angle
346 - zenith distance - 180degrees, appropriately normalised into -180 to
347 +180.
348 """
349 az, zd, pa, HAobs, decObs, RAobs = \
350 self.__telescope.getPointing(self.__target, utc)
351 rotpos = self.__target.getPA() - pa - zd - angle(180, 'd')
352 rotpos.normPi()
353 return rotpos, az, zd, pa, HAobs, decObs, RAobs
354
356 """
357 Calculates the time (UTC) at which the rotator will reach the limits
358 of its range of movement for the set telescope and target. Returns
359 None if the the rotator position is currently outside the limits,
360 otherwise a datetime.datetime object.
361 """
362 step = datetime.timedelta(seconds=300)
363 accuracy = datetime.timedelta(seconds=1)
364 utc1 = datetime.datetime.utcnow()
365
366 pos1 = self.calcRotPos(utc1)[0]
367
368
369
370
371 if pos1 > self.__lower and pos1 < self.__upper:
372
373
374
375 while pos1 > self.__lower and pos1 < self.__upper:
376
377 utc0 = utc1
378 pos0 = pos1
379
380 utc1 = utc1 + step
381
382 pos1 = self.calcRotPos(utc1)[0]
383
384
385
386 if pos1 < self.__lower:
387 limit = self.__lower
388 elif pos1 > self.__upper:
389 limit = self.__upper
390
391
392
393
394 delta_t = step
395 while abs(delta_t) > accuracy:
396 factor = (pos1-limit).getAngle() / (pos1 - pos0).getAngle()
397 delta_t = - datetime.timedelta(delta_t.days * factor, \
398 delta_t.seconds * factor, \
399 delta_t.microseconds * factor)
400 utc = utc1 + delta_t
401 pos = self.calcRotPos(utc)[0]
402 if pos0 < pos < limit or pos0 > pos > limit:
403 pos0 = pos1
404 utc0 = utc0
405 pos1 = pos
406 utc1 = utc
407 elif limit < pos < pos1 or limit > pos > pos1:
408 pos1 = pos
409 utc1 = utc
410
411
412
413 return utc1
414
416 """ Print the Target PA
417 useage: dazle.rotator_1.TargetPA()
418 """
419 print 'Target RotPos: ',self.calcRotPos(utc)
420
421
423 """
424 Class representing the camera rotation system used by DAZLE for field
425 rotation comepensation.
426 """
427 - def __init__(self, portname, telescope, target, idle, run, boost, \
428 dt, name='rotator'):
429 """
430 Initialiser for rotator class.
431
432 Arguments:
433
434 portname: The device name of the serial port which the
435 Taranis stepper motor controller is connected to.
436 telescope: A rotator.telescope object containing observatory
437 specific information.
438 target: A rotator.target object contain details of the target
439 field for observations.
440 idle: The peak motor current when the motor is stationary,
441 in amps. Must be >=0 and <= run.
442 run: The peak motor current when the motor is moving
443 continously, in amps. Must be >= idle and <= boost.
444 boost: The peak motor current when the motor is accelerating/
445 decelerating, in amps. Must be >=run and <= 4.5.
446 dt: The interval between tracking corrections, in seconds.
447 name: label used for logging purposes.
448 """
449
450 self.__portname = portname
451 self.__telescope = telescope
452 self.__target = target
453 self.__idle = float(idle)
454 self.__run = float(run)
455 self.__boost = float(boost)
456 self.__dt = float(dt)
457 self.name = name
458
459 self.logger = logging.getLogger(name)
460
461
462 try:
463 self.__port = serial.Serial(portname, timeout=5)
464 self.__port.open()
465 self.__port.flushInput()
466 except serial.SerialException:
467
468 self.logger.critical('Failed to open serial port %s!' % portname)
469 return
470 self.logger.info('Connected to serial port %s' % portname)
471
472
473 self.where()
474
475
476 self.setCurrents(self.__idle, self.__run, self.__boost)
477
478
479 self.__stop = threading.Event()
480 self.__stop.set()
481 self.lockedOn = threading.Event()
482 self.lockedOn.clear()
483
485 """
486 Should be called by the parent instrument object's destructor
487 to enable status information to be saved to file on exit.
488
489 Takes a ConfigParserRT.SafeConfigParser object as argument.
490 """
491 self.logger.debug("updateConfig() called")
492
493 self.stopTracking()
494
495 if isinstance(config, ConfigParserRT.SafeConfigParser):
496
497 config.set(self.name, 'target', self.__target.name)
498 config.set(self.name, 'telescope', self.__telescope.name)
499 config.set(self.name, 'idle', str(self.__idle))
500 config.set(self.name, 'run', str(self.__run))
501 config.set(self.name, 'boost', str(self.__boost))
502 config.set(self.name, 'dt', str(self.__dt))
503 else:
504 self.logger.critical("Invalid argument to updateConfig(%s)" \
505 % repr(config))
506 self.logger.critical("Unable to save status data")
507
509 """
510 Sets the motor currents to be used by the rotator.
511
512 Arguments:
513
514 idle: The peak motor current when the motor is stationary, in amps.
515 Must be >=0 and <= run.
516 run: The peak motor current when the motor is moving continously,
517 in amps. Must be >= idle and <= boost.
518 boost: The peak motor current when the motor is accelerating/
519 decelerating, in amps. Must be >=run and <= 4.5.
520 """
521 self.logger.debug('setCurrents(%f, %f, %f) called.' % \
522 (idle, run, boost))
523 if self.isTracking():
524 self.logger.critical("Currently tracking, can't setCurrents().")
525 return
526 self.__port.flushInput()
527 self.__port.timeout = 5
528 if boost <= 4.5 and boost >= run and run >= idle and idle >=0:
529 self.__port.write('S')
530 self.__port.write('%f\n' % (idle * 1000))
531 self.__port.write('%f\n' % (run * 1000))
532 self.__port.write('%f\n' % (boost * 1000))
533 response = self.__port.readline()
534 if response == 'O\n':
535 self.__idle = idle
536 self.__run = run
537 self.__boost = boost
538 self.logger.debug('Motor currents set.')
539 elif response == 'F\n':
540 self.logger.critical('Motor currents rejected by Taranis!')
541 self.logger.critical(\
542 'Call setCurrents() again before using rotator.')
543 else:
544 self.logger.critical('Unexpected response %s from Taranis!' % \
545 repr(response))
546 self.logger.critical(\
547 'Call setCurrents() again before using rotator.')
548 else:
549 self.logger.critical('Attempt to set illegal motor currents, ' + \
550 '%fA, %fA, %fA.' % (idle, run, boost))
551 self.logger.critical(\
552 'Call setCurrents() again before using rotator.')
553
555 """
556 Instructs the rotator to perform its initialisation routine, which
557 determines the encoder angular scale and zero point, and sets up
558 the upper and lower motion limits.
559 """
560 self.logger.debug('setupRotator() called.')
561 if self.isTracking():
562 self.logger.critical("Currently tracking, can't setupRotator().")
563 return
564 self.__port.timeout = 1200
565 self.__port.flushInput()
566 self.__port.write('I')
567 response = self.__port.readline()
568 if response == "O\n":
569 self.logger.info('Rotator initialisation complete.')
570 self.where()
571 elif response == "F\n":
572 self.logger.critical('Rotator initialisation failed!')
573 self.logger.critical('Check limit switch and motor function')
574 self.logger.critical('then run again.')
575 elif not response:
576 self.logger.critical('No response from Taranis in setupRotator()!')
577 self.logger.critical('Check serial cable and run ' + \
578 'setupRotator again.')
579 else:
580 self.logger.critical("Unexpected response '%s' from Taranis in" \
581 % response + 'setupRotator()')
582
584 """
585 Changes to current target field to the rotator.target instance
586 given as the argument.
587 """
588 if self.isTracking():
589 self.logger.critical("Currently tracking, can't setTarget().")
590 return
591 if isinstance(field, target):
592 self.__target = field
593 else:
594 self.logger.warning(\
595 'setTarget() expected rotator.target, got %s.' % type(field))
596
598 """
599 Changes the current telescope/observatory for the instrument to the
600 rotator.telescope instance passed as the srgument
601 """
602 if self.isTracking():
603 self.logger.critical("Currently tracking, can't setTelescope().")
604 return
605 if isinstance(observatory, telescope):
606 self.__telescope = observatory
607 else:
608 self.logger.warning(\
609 'setTelescope() expected rotator.telescope, for %s' \
610 % type(telescope))
611
613 """
614 Creates an instance of rotator.tracker thread and sets it running
615 for the currently set telescope and target.
616
617 Arguments:
618
619 logData: Boolean to indicate whether tracking status information
620 should be logged to the file 'dicstrack.log'. Default False.
621 """
622 if self.isTracking():
623 self.logger.critical('Rotator already tracking, ' + \
624 'ignoring startTracking()')
625 return
626 self.__stop.clear()
627 self.__tracker = tracker(self.__telescope, self.__target, \
628 self.__dt, self.__lower, self.__upper, \
629 self.__port, self.__stop, self.lockedOn, \
630 self.logger, logData)
631 self.__tracker.start()
632
635
637 try:
638 return self.__tracker.isAlive()
639 except AttributeError:
640 return False
641
643 if self.lockedOn.isSet():
644 return True
645 else:
646 return False
647
649 """
650 Request status information from the Taranis.
651 """
652 self.logger.debug('rotator.where() called.')
653 if self.isTracking():
654 self.logger.critical('Currently tracking, where() ignored.')
655 return
656 self.__port.timeout = 5
657 self.__port.flushInput()
658 self.__port.write('W')
659 pos = self.__port.readline()
660 upper = self.__port.readline()
661 lower = self.__port.readline()
662 if pos and upper and lower:
663 self.__position = angle(float(pos))
664 self.__upper = angle(float(upper))
665 self.__lower = angle(float(lower))
666 self.logger.info('Received %f, %f, %f.' % \
667 (self.__position.getAngle('d'), \
668 self.__upper.getAngle('d'), \
669 self.__lower.getAngle('d')))
670 else:
671 self.logger.critical('No response from Taranis during ' + \
672 'where()!')
673 self.logger.critical('Check serial cable then run ' + \
674 'where() again.')
675
677 """ Get Rotator Info useage: dazle.rotator_1.Info() """
678
679 """ History 20060605 rgm """
680
681
682
683
684
685
686
687
688
689
690
691 timestamp=time.time()
692 utc=datetime.datetime.utcfromtimestamp(timestamp)
693 print 'UTC: ',datetime.datetime.utcfromtimestamp(timestamp)
694 print 'UTC(ISO format): ',utc.isoformat()
695
696
697
698 print 'Name: ',self.name
699
700 print 'Portname: ',self.__portname
701 print 'Currents: ',self.__idle,self.__run,self.__boost
702 print 'Time step(dt): ',self.__dt
703 print 'Target: ',self.__target.name
704 print 'Target (ra,dec,pa): ',self.__target.getCoords()
705 print 'Target RA: ',angle.getString(self.__target.getRA(),units='h',ndp=2),self.__target.getRA(),math.degrees(angle.getAngle(self.__target.getRA()))
706 print 'Target Dec: ',angle.getString(self.__target.getDec(),units='d', ndp=3),self.__target.getDec()
707 print 'Target PA: ',self.__target.getPA()
708 print 'Target ApparentPlace: ',self.__target.getApparentPlace()
709
710 print 'Telescope.pointing: ',self.__telescope.getPointing(self.__target,utc)
711 az, zd, pa, HAobs, decObs, RAobs = \
712 self.__telescope.getPointing(self.__target, utc)
713 print 'Telescope.pointing.az: ',az
714
715 print 'Telescope.name: ',self.__telescope.name
716 print 'Telescope.location: ',self.__telescope.getLocation()
717
718 print 'Telescope.weather: ',self.__telescope.getWeather()
719 print 'Telescope.wavelength: ',self.__telescope.getWavelength()
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745 - def MoveByDeg(self, MoveAngleDeg=1.0, MoveTimeSec=1.0):
746 """
747 Moves the rotator by an angle in degrees(default is 1.0deg)
748 MoveTimeSec is to be the time step for the move. This could be
749 sepecified as a rate RotRate in Degrees/sec.
750
751 The Taranis software has a time step parameter which
752 I have yet to understand.
753
754 See rotator.where for getting rotator location
755
756
757 History
758 20060605 Original by rgm
759
760 """
761
762 print 'UTC(current): ',datetime.datetime.utcfromtimestamp(time.time())
763
764
765 MoveAngleRad = math.radians(MoveAngleDeg)
766 print 'MoveAngle(Radians, Degrees)= ',MoveAngleRad,MoveAngleDeg
767 print 'MoveTimeSec: ',MoveTimeSec
768
769
770
771 response=self.__port.readline()
772 print 'Taranis response: ',response
773
774
775 self.where()
776
777
778
779 print 'Current values of dt(secs), position(radians, degrees): ',self.__dt, self.__position,self.__position.getAngle('d')
780
781
782 CurrentPosRad =self.__position.getAngle()
783 NewPosRad = self.__position.getAngle() + MoveAngleRad
784 NextPosRad = self.__position.getAngle() + 2.0*MoveAngleRad
785 print 'Current Position(Radians,Degrees): ',CurrentPosRad,math.degrees(CurrentPosRad),CurrentPosRad
786 print 'New Position(Radians,Degrees): ', NewPosRad, math.degrees(NewPosRad)
787 print 'Next Position(Radians,Degrees): ',NextPosRad, math.degrees(NextPosRad)
788
789 timestamp=time.time()
790 utc=datetime.datetime.utcfromtimestamp(timestamp)
791 print 'UTC: ',datetime.datetime.utcfromtimestamp(timestamp)
792
793
794 self.__port.timeout = 5
795 self.__port.flushInput()
796 self.__port.write('W')
797 pos = self.__port.readline()
798 upper = self.__port.readline()
799 lower = self.__port.readline()
800 self.logger.info('Received %f, %f, %f.' % \
801 (self.__position.getAngle('d'), \
802 self.__upper.getAngle('d'), \
803 self.__lower.getAngle('d')))
804
805
806
807
808 print 'MOVING ',MoveAngleDeg,' degrees to ',math.degrees(NewPosRad)
809
810
811
812
813 self.__port.timeout = 5
814 self.__port.flushInput()
815 self.__port.write('T')
816 self.__port.write('%e\n' % self.__dt)
817
818 self.__port.write('%e\n' % self.__position.getAngle())
819
820 i = 0
821 while i < 3:
822
823 i = i + 1
824 NewPosRad = self.__position.getAngle() + MoveAngleRad
825 self.__port.write('%e\n' % NewPosRad)
826
827
828
829 delay=5.0
830 print 'Taranis moving for: ',delay,' seconds',NewPosRad,NextPosRad
831 time.sleep(delay)
832
833 self.__port.write('M')
834 response= self.__port.readline()
835 print 'UTC: ',datetime.datetime.utcfromtimestamp(time.time())
836 print 'Taranis response: ',response
837 if response == 'O\n':
838 position = angle(float(self.__port.readline()))
839 error = angle(float(self.__port.readline()))
840 print 'Position, Err: ',position, error,position.getAngle('d'),error.getAngle('d')
841
842
843
844 self.__port.write('0.0\n')
845 self.__port.write('C')
846
847
848 response=self.__port.readline()
849 print 'UTC: ',datetime.datetime.utcfromtimestamp(time.time())
850 print 'Taranis response: ',response
851
852 print 'UTC: ',datetime.datetime.utcfromtimestamp(time.time())
853 self.where()
854 print 'position(deg): ',self.__position.getAngle('d')
855
856
858 """
859 Class representing the telescope/observatory. Stores information relating
860 to the position and meteorological conditions (for refraction
861 calculations) and performs calculations of observed azimuth and elevation.
862 """
863 - def __init__(self, longitude, latitude, altitude, \
864 polar_motion_x, polar_motion_y, delta_ut, \
865 temperature, pressure, humidity, tlr, \
866 wavelength, name):
867 """
868 Initialiser for a telescope object. Arguments representing angles
869 should be an object acceptable as an argument to the rotator.angle
870 constructor, or a sequence type containing a list of arguments
871 acceptible to rotator.angle().
872
873 Arguments:
874
875 longitude: Mean longitude of the telescope, east positive.
876 latitude: Mean geodetic latitude of the telescope.
877 altitude: Telescope's height above mean sea level (metres).
878 polar_motion_x: Polar motion x-coordinate for today, from IERS
879 polar_motion_y: Polar motion y-coordinate for today, from IERS
880 delta_ut: UT1-UTC from IERS, in seconds, as a float
881 temperature: Local ambient temperature (Kelvin)
882 pressure: Local atmospheric pressure (millibars)
883 humidity: Local relative humidity (in the range 0.0-1.0)
884 tlr: Tropospheric lapse rate (Kelvin/metre)
885 wavelength: Effective wavelength of the observations (microns)
886 name: An identification string for logging purposes.
887 """
888
889 for varname, arg in \
890 ('_telescope__longitude', longitude), \
891 ('_telescope__latitude', latitude), \
892 ('_telescope__polar_motion_x', polar_motion_x), \
893 ('_telescope__polar_motion_y', polar_motion_y):
894 try:
895 setattr(self, varname, angle(*arg))
896 except TypeError:
897 setattr(self, varname, angle(arg))
898
899
900 self.__altitude = float(altitude)
901 self.__delta_UT = float(delta_ut)
902 self.__temperature = float(temperature)
903 self.__pressure = float(pressure)
904 self.__humidity = float(humidity)
905 self.__tlr = float(tlr)
906 self.__wavelength = float(wavelength)
907 self.name = name
908
909 self.logger = logging.getLogger(name)
910
911
912
913 self.__aoprms = slalib.doubleArray(14)
914
916 """
917 Should be called by the parent instrument object's destructor
918 to enable status information to be saved to file on exit.
919
920 Takes a ConfigParserRT.SafeConfigParser object as argument.
921 """
922 self.logger.debug("updateConfig() called.")
923
924 if isinstance(config, ConfigParserRT.SafeConfigParser):
925 config.set(self.name, 'longitude', self.__longitude.getString('d'))
926 config.set(self.name, 'latitude', self.__latitude.getString('d'))
927 config.set(self.name, 'altitude', str(self.__altitude))
928 config.set(self.name, 'polar_motion_x', \
929 self.__polar_motion_x.getString('d', 5))
930 config.set(self.name, 'polar_motion_y', \
931 self.__polar_motion_y.getString('d', 5))
932 config.set(self.name, 'delta_ut', str(self.__delta_UT))
933 config.set(self.name, 'temperature', str(self.__temperature))
934 config.set(self.name, 'pressure', str(self.__pressure))
935 config.set(self.name, 'humidity', str(self.__humidity))
936 config.set(self.name, 'tlr', str(self.__tlr))
937 config.set(self.name, 'wavelength', str(self.__wavelength))
938 else:
939 self.logger.critical("Invalid argument to updateConfig(%s)" \
940 % repr(config))
941 self.logger.critical("Unable to save status data")
942
944 """
945 Returns the stored earth rotation parameters, as provided by the
946 International Earth Rotation Service, UT1-UTC (seconds, float),
947 and the x and y components of polar motion (rotator.angles).
948 """
949 return self.__delta_UT, angle(self.__polar_motion_x), \
950 angle(self.__polar_motion_y)
951
952 - def setIERS(self, delta_UT, polar_motion_x, polar_motion_y):
953 """
954 Sets the earth rotation parameters. The information to do this
955 should be obtained from the International Earth Rotation Service
956 bulletin covering the day in question.
957
958 Arguments:
959
960 delta_UT: UT1-UTC, in seconds.
961 polar_motion_x: Polar motion x-coordinate, as either an argument or
962 sequence containing an argument list for the
963 rotator.angle() constructor.
964 polar_motion_y: Polar motion y-coordinate, as either an argument or
965 sequence containing an argument list for the
966 rotator.angle() constructor.
967 """
968 self.__delta_UT = float(delta_UT)
969 for name, arg in \
970 ('_telescope__polar_motion_x', polar_motion_x), \
971 ('_telescope__polar_motion_y', polar_motion_y):
972 try:
973 setattr(self, name, angle(*arg))
974 except TypeError:
975 setattr(self, name, angle(arg))
976
978 """
979 Returns the telescope location information, longitude, latitude
980 (both as rotator.angles) and altitude (metres).
981 """
982 return angle(self.__longitude), angle(self.__latitude), self.__altitude
983
985 """
986 Sets the telescope location information.
987
988 Arguments:
989
990 longitude: Mean longitude of the telescope, east positive, as either
991 an argument or sequence containing an argument list for the
992 rotator.angle() constructor.
993 latitude: Mean geodetic latitude of the telescope, as either an
994 argument or sequence containing an argument list for the
995 rotator.angle() constructor.
996 altitude: The telescope's height above mean sea level, in metres.
997 """
998 for name, arg in \
999 ('_telescope__longitude', longitude), \
1000 ('_telescope__latitude', latitude):
1001 try:
1002 setattr(self, name, angle(*arg))
1003 except TypeError:
1004 setattr(self, name, angle(arg))
1005 self.__altitude = float(altitude)
1006
1008 """
1009 Deletes any stored field reference. The effect of this is to force
1010 a recalculation of all the observed-to-apparent-place parameters
1011 in the next call to getPointing(), rather than just updating the
1012 sidereal time.
1013 """
1014 try:
1015 del(self.__field)
1016 except AttributeError:
1017 pass
1018
1020 """
1021 Calculates the observed azimuth and zenith distance for a given field
1022 and time. Note that these calculations assume a 'perfect' telescope,
1023 no pointing model is used.
1024
1025 Arguments:
1026
1027 field: the target field, expressed as a rotator.field object.
1028 utc: the time of observation, UTC, expressed as a datetime.datetime
1029 object
1030
1031 Returned:
1032
1033 az: the observed azimuth expressed as a rotator.angle, East = 90deg
1034 zd: the observed zenith distance expressed as a rotator.angle.
1035 pa: the parallactic angle at the target field.
1036 """
1037
1038 if not isinstance(field, target):
1039 raise TypeError("telescope.getPointing() expected rotator.target" \
1040 + ", got %s." % type(field))
1041 elif not isinstance(utc, datetime.datetime):
1042 raise TypeError("telescope.getPointing() expected " + \
1043 "datetime.datetime, got %s." % type(utc))
1044 else:
1045
1046
1047
1048 if not field.getApparentPlace():
1049 field.updateApparentPlace(utc)
1050 RAApp, DecApp = field.getApparentPlace()
1051
1052
1053
1054
1055 imjd, status = slalib.slaCldj(utc.year, utc.month, utc.day)
1056 if status != 0:
1057 raise SlalibError('slaCldj', status)
1058
1059 status = 0
1060 fmjd, status = slalib.slaDtf2d(utc.hour, utc.minute, \
1061 utc.second + \
1062 utc.microsecond/1000000.0)
1063
1064
1065
1066
1067
1068
1069 if not hasattr(self, 'self.__field') or self.__field != field:
1070
1071 slalib.slaAoppa(\
1072 imjd + fmjd, \
1073 self.__delta_UT, \
1074 self.__longitude.getAngle(), \
1075 self.__latitude.getAngle(), \
1076 self.__altitude, \
1077 self.__polar_motion_x.getAngle(), \
1078 self.__polar_motion_y.getAngle(), \
1079 self.__temperature, \
1080 self.__pressure, \
1081 self.__humidity, \
1082 self.__wavelength, \
1083 self.__tlr, \
1084 self.__aoprms)
1085 self.__field = field
1086 else:
1087
1088
1089 slalib.salAoppat(imjd + fmjd, self.__aoprms)
1090
1091
1092 az, zd, HAObs, decObs, RAObs = slalib.slaAopqk(\
1093 RAApp.getAngle(), DecApp.getAngle(), \
1094 self.__aoprms)
1095
1096
1097 pa = slalib.slaPa(\
1098 self.__aoprms[13] - RAApp.getAngle(), \
1099 DecApp.getAngle(), \
1100 self.__latitude.getAngle())
1101
1102
1103 return angle(az), angle(zd), angle(pa), angle(HAObs), \
1104 angle(decObs), angle(RAObs)
1105
1107 """
1108 Returns the stored effective wavelength of observations, in microns.
1109 """
1110 return self.__wavelength
1111
1113 """
1114 Sets the effective wavelength of observations.
1115
1116 Arguments:
1117
1118 wavelength: Effective wavelength of observations, in microns.
1119 """
1120 self.__wavelength = float(wavelength)
1121
1123 """
1124 Returns the stored meteorological parameters, temperature (in Kelvin),
1125 pressure (in millibars), relative humidity (in the range 0-1.0) and
1126 tropospheric lapse rate (Kelvin/metre).
1127 """
1128 return self.__temperature, self.__pressure, self.__humidity, \
1129 self.__tlr
1130
1131 - def setWeather(self, temperature, pressure, humidity, tlr):
1132 """
1133 Sets the meteorogical parameters used in the refraction calculations.
1134
1135 Arguments:
1136
1137 temperature: Local ambient temperature (Kelvin)
1138 pressure: Local atmospheric pressure (millibars)
1139 humidity: Local relative humidity (in the range 0.0-1.0)
1140 tlr: Tropospheric lapse rate (Kelvin/metre)
1141 """
1142 self.__temperature = float(temperature)
1143 self.__pressure = float(pressure)
1144 self.__humidity = float(humidity)
1145 self.__tlr = float(tlr)
1146
1148 """
1149 Class representing a target field. Stores the position of the field
1150 in mean place FK5 J2000 coordinates, and calculates the geocentric
1151 apparent coordinates when given the epoch. Also have a number of methods
1152 for setting and retrieving coordinates.
1153 """
1154 - def __init__(self, ra, dec, pa, name):
1155 """
1156 Initialises the target object. Takes four arguments, the target's
1157 RA, Dec, desired position angle, and name (an identification string
1158 primarily for logging purposes. RA and Dec should be mean place
1159 FK5 J2000 coordinates. Position angle is here defined as the angle
1160 Northpole-field centre-'up', where 'up' is the upwards direction on the
1161 detector. Each of the arguments should be either a single value
1162 acceptable to the rotator.angle constructor (e.g. a rotator.angle
1163 object, a numeric type representing an angle, or a string containing a
1164 sexagesimal/decimal representation of the angle) or a sequence object
1165 containing an argument list for the constructor. If a single value
1166 is given for RA it is assumed to be in units of hours, whereas Dec and
1167 PA are assumed to be in degrees.
1168 """
1169 self.name = name
1170 for label, arg, unit in ('_target__RA', ra, 'h'), \
1171 ('_target__Dec', dec, 'd'), ('_target__PA', pa, 'd'):
1172 try:
1173 setattr(self, label, angle(*arg))
1174 except TypeError:
1175 setattr(self, label, angle(arg, unit))
1176
1177 self.logger = logging.getLogger(name)
1178
1180 """
1181 Should be called by the parent instrument object's destructor
1182 to enable status information to be saved to file on exit.
1183
1184 Takes a ConfigParserRT.SafeConfigParser object as argument.
1185 """
1186 self.logger.debug("updateConfig() called.")
1187
1188 if isinstance(config, ConfigParserRT.SafeConfigParser):
1189 config.set(self.name, 'ra', self.__RA.getString('h'))
1190 config.set(self.name, 'dec', self.__Dec.getString('d'))
1191 config.set(self.name, 'pa', str(self.__PA.getAngle('d')))
1192 else:
1193 self.logger.critical("Invalid argument to updateConfig(%s)" \
1194 % repr(config))
1195 self.logger.critical("Unable to save status data")
1196
1198 """
1199 If apparent place RA and Dec have been calculated (with
1200 updateApparentPlace()) then returns them, else returns None.
1201 """
1202 try:
1203 return angle(self.__RAApp), angle(self.__DecApp)
1204 except AttributeError:
1205 pass
1206
1208 """
1209 Returns the mean place FK5 J2000 RA and Dec, and the position angle
1210 for the target, as rotator.angle objects.
1211 """
1212 return angle(self.__RA), angle(self.__Dec), angle(self.__PA)
1213
1214
1216 """
1217 Returns the mean place FK5 J2000 RA and Dec, and the position angle
1218 for the target, as rotator.angle objects.
1219 """
1220 return angle(self.__RA)
1221
1222
1224 """
1225 Returns the mean place FK5 J2000 RA and Dec, and the position angle
1226 for the target, as rotator.angle objects.
1227 """
1228 return angle(self.__Dec)
1229
1230
1232 """
1233 Returns the position angle for the target as a rotator.angle object
1234 """
1235 return angle(self.__PA)
1236
1237
1239 """
1240 Sets the requested position angle.
1241 """
1242 try:
1243 self.__PA = angle(*pa)
1244 except TypeError:
1245 self.__PA = angle(pa)
1246
1248 """
1249 Sets the mean place FK5 J2000 RA and Dec, and the requested position
1250 angle for the target. Arguments are as for __init__().
1251 """
1252 for name, arg in ('_target__RA', RA), ('_target__Dec', Dec), \
1253 ('_target__PA', PA):
1254 try:
1255 setattr(self, name, angle(*arg))
1256 except TypeError:
1257 setattr(self, name, angle(arg))
1258
1259
1260
1261 try:
1262 del self.__RAApp
1263 except AttributeError:
1264 pass
1265 try:
1266 del self.__DecApp
1267 except AttributeError:
1268 pass
1269
1271 """
1272 Performs the precession-nutation, light deflection and annual
1273 aberration calculations required to obtain the apparent place
1274 RA and dec of the target, and both returns and stores the result.
1275
1276 A single argument is required, the Barycentric Dynamical Time of
1277 observation as a datetime.datetime object. According to the called
1278 slalib.slaMap routine's notes, 'The distinction between the required
1279 TDB and TT is always negligible. Moreover, for all but the most
1280 critical applications UTC is adequate'.
1281 """
1282 if isinstance(tdb, datetime.datetime):
1283
1284
1285
1286
1287 imjd, status = slalib.slaCldj(tdb.year, tdb.month, tdb.day)
1288 if status != 0:
1289 raise SlalibError('slaCldj', status)
1290
1291
1292 status = 0
1293 fmjd, status = slalib.slaDtf2d(tdb.hour, tdb.minute, \
1294 tdb.second + \
1295 tdb.microsecond/1000000.0)
1296 if status != 0:
1297 raise SlalibError('slaDtf2d', status)
1298
1299
1300
1301
1302 RAApp, DecApp = slalib.slaMap(self.__RA.getAngle(), \
1303 self.__Dec.getAngle(), \
1304 0, 0, 0, 0, 2000, \
1305 imjd + fmjd)
1306
1307 self.__RAApp = angle(RAApp)
1308 self.__DecApp = angle(DecApp)
1309 return angle(self.__RAApp), angle(self.__DecApp)
1310 else:
1311 raise TypeError(\
1312 'target.updateApparentPlace() requires a datetime.datetime' + \
1313 ' object argument, got %s' % type(tdb))
1314
1315 - def unwind(self, shiftnumber):
1316 """
1317 Changes the position angle by the argument, shiftnumber, times
1318 90 degrees. This will be done by a rotator object in order to
1319 keep its required position within its range of motion. The
1320 argument must be a type acceptible to the builtin int() function.
1321 """
1322 self.__PA += int(shiftnumber) * math.pi / 2.0
1323 self.__PA.normPi()
1324
1325
1326 try:
1327 del(self.__RAApp)
1328 except AttributeError:
1329 pass
1330 try:
1331 del(self.__DecApp)
1332 except AttributeError:
1333 pass
1334
1336 """
1337 Class for storing angles. Internally stores an angle as a float in radians
1338 and provides methods for setting and retrieving the angle in a variety of
1339 units, both via floats or various string formats.
1340 """
1341 - def __init__(self, value = 0.0, units=False):
1342 """
1343 Standard initialiser, takes up to two arguments. The first argument
1344 (which defaults to zero) is the angle to store, the second argument
1345 (which defaults to 'r') indicates the units used, 'r' for radians,
1346 'd' for degrees and 'h' for hours. The angle may be in any numeric
1347 type acceptable to the builtin function float(). Alternatively, if
1348 value is itself an angle object, this becomes a C++ style 'copy
1349 constructor' and units is ignored. Thirdly, value may be a string
1350 representation of an angle, in which case it will be parsed by
1351 angle.setString(). For string values only units of 'd' or 'h' are
1352 acceptable, and units will default to 'd'.
1353 """
1354 self.__radians = 0.0
1355 if isinstance(value, str):
1356 self.setString(value, units or 'd')
1357 else:
1358 self.setAngle(value, units or 'r')
1359
1360
1361
1362
1363
1365 try:
1366 return self.__radians == other.getAngle()
1367 except AttributeError:
1368 return self.__radians == other
1369
1371 try:
1372 return self.__radians != other.getAngle()
1373 except AttributeError:
1374 return self.__radians != other
1375
1377 try:
1378 return self.__radians >= other.getAngle()
1379 except AttributeError:
1380 return self.__radians >= other
1381
1383 try:
1384 return self.__radians > other.getAngle()
1385 except AttributeError:
1386 return self.__radians > other
1387
1389 try:
1390 return self.__radians <= other.getAngle()
1391 except AttributeError:
1392 return self.__radians <= other
1393
1395 try:
1396 return self.__radians < other.getAngle()
1397 except AttributeError:
1398 return self.__radians < other
1399
1400
1401
1402
1403
1404
1405
1407 try:
1408 return angle(self.__radians + other.getAngle())
1409 except AttributeError:
1410 return angle(self.__radians + other)
1411
1413 return angle(self.__radians + other)
1414
1416 try:
1417 self.__radians += other.getAngle()
1418 except AttributeError:
1419 self.__radians += other
1420 return self
1421
1423 try:
1424 return angle(self.__radians - other.getAngle())
1425 except AttributeError:
1426 return angle(self.__radians - other)
1427
1429 return angle(self.__radians - other)
1430
1432 try:
1433 self.__radians -= other.getAngle()
1434 except AttributeError:
1435 self.__radians -= other
1436 return self
1437
1439 return angle(self.__radians * other)
1440
1442 return angle(self.__radians * other)
1443
1445 self.__radians *= other
1446 return self
1447
1449 return angle(self.__radians / other)
1450
1452 self.__radians /= other
1453 return self
1454
1456 return angle(self.__radians / other)
1457
1459 self.__radians /= other
1460 return self
1461
1463 return angle(-self.__radians)
1464
1467
1469 return angle(abs(self.__radians))
1470
1471
1472
1474 return "rotator.angle(%s)" % self.__radians
1475
1477 return str(self.__radians)
1478
1479
1480
1482 """
1483 Returns the value of the angle as a float in the specified units,
1484 'r' - radians, 'd' - degrees or 'h' - hours.
1485 """
1486 if units == 'r':
1487 return self.__radians
1488 elif units == 'd':
1489 return math.degrees(self.__radians)
1490 elif units == 'h':
1491 return math.degrees(self.__radians) / 15.0
1492 else:
1493 raise UnitsError('angle.getAngle()', units)
1494
1495 - def setAngle(self, value=0.0, units='r'):
1496 """
1497 Sets the angle. Takes up to two arguments. The first argument
1498 (which defaults to zero) is the angle to store, the second argument
1499 (which defaults to 'r') indicates the units used, 'r' for radians,
1500 'd' for degrees and 'h' for hours. The angle may be in any form
1501 acceptable to the builtin function float(). Alternatively, if value
1502 is itself an angle object, this makes the angles equal and the units
1503 argument is ignored.
1504 """
1505 try:
1506 self.__radians = value.getAngle()
1507 except AttributeError:
1508 if units == 'r':
1509 self.__radians = float(value)
1510 elif units == 'd':
1511 self.__radians = math.radians(float(value))
1512 elif units == 'h':
1513 self.__radians = math.radians(15 * float(value))
1514 else:
1515 raise UnitsError('angle.setAngle()', units)
1516
1518 """
1519 Returns a string representation of the angle in sexagesimal or
1520 'time' formats. The optional first parameter units can be 'd', 'h' or
1521 't' for degrees, hours (sexagesimal) or hours (time format), default
1522 is 'd'. The optional second parameter, 'ndp', specified the number of
1523 decimal places in the seconds field, default 3.
1524
1525 For decimal string representations getAngle() can be used together with
1526 standard string formatting commands.
1527 """
1528
1529
1530 ints = slalib.intArray(4)
1531 if units == 'd':
1532 sign = slalib.slaDr2af(ndp, self.__radians, ints)
1533 return "%c%d %02d %02d.%0*d" % \
1534 (sign[0], ints[0], ints[1], ints[2], ndp, ints[3])
1535 elif units == 'h':
1536 sign = slalib.slaDr2tf(ndp, self.__radians, ints)
1537 return "%c%d %02d %02d.%0*d" % \
1538 (sign[0], ints[0], ints[1], ints[2], ndp, ints[3])
1539 elif units == 't':
1540 sign = slalib.slaDr2tf(ndp, self.__radians, ints)
1541 return "%c%d:%02d:%02d.%0*d" % \
1542 (sign[0], ints[0], ints[1], ints[2], ndp, ints[3])
1543 else:
1544 raise UnitsError('angle.getString()', units)
1545
1546 - def setString(self, angleString, units='d', startPos=1):
1547 """
1548 Sets the angle from a string representation. The first argument
1549 contains a free format string representing the angle as either
1550 a decimal or sexagesimal number (or a combination of the two) and
1551 the second is a character specifying the units used ('d' for degrees,
1552 'h' for hours). Uses slaDafin from the slalib module to parse the
1553 string. An optional third argument specifies where in the string
1554 to start parsing (in a 1 based, not zero based, count).
1555
1556 Returns the position in the string at which encoding finished.
1557 """
1558 (endPos, value, status) = slalib.slaDafin(angleString, startPos)
1559
1560 if status == 0:
1561
1562 if units == 'd':
1563 self.__radians = value
1564 elif units == 'h':
1565 self.__radians = value * 15
1566 else:
1567 raise UnitsError('angle.setString()', units)
1568 return endPos
1569 else:
1570
1571 raise ParseError('slalib.slaDafin()', status, startPos, endPos)
1572
1574 """
1575 Normalises the angle into the range +/-Pi.
1576 """
1577 self.__radians = slalib.slaDrange(self.__radians)
1578
1580 """
1581 Normalises the angle into the range 0-2Pi.
1582 """
1583 self.__radians = slalib.slaDranrm(self.__radians)
1584