Package dics :: Module rotator
[hide private]
[frames] | no frames]

Source Code for Module dics.rotator

   1  """ rotator module for dazle""" 
   2   
   3  # $Id$   
   4   
   5  # $Log$ 
   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  # support for debuf and verbose logging 
  17  debug=True 
  18  verbose=True 
  19   
  20   
  21  #class GetInfo(GetInfo): 
  22  #    """ 
  23  #    Class to get information about paramters used by Rotator 
  24  #    """ 
  25  #     
  26  #    def __init__(self, tele, tgt, dt, low, high, port, stop, lockedOn, logger, \ 
  27  #                 logData=False): 
  28  #     
  29  #    def target(self): 
  30  #        """ test """     
  31  # 
  32  #    def rotator(self): 
  33  #        """ test """     
  34  # 
  35  #    def target(self): 
  36  #        """ test """     
  37           
  38   
39 -class Error(Exception):
40 """Base class for exceptions in this module.""" 41 pass
42
43 -class UnitsError(Error):
44 """ 45 Raised where invalid units are specified. 46 """
47 - def __init__(self, function, unit):
48 self.function = function 49 self.unit = unit
50
51 - def __repr__():
52 return "rotator.UnitsError('%s', '%s')" % (self.function, self.unit)
53
54 - def __str__():
55 return "Invalid units '%s' specified in call to %s" % \ 56 (self.unit, self.function)
57
58 -class ParseError(Error):
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
68 -class SlalibError(Error):
69 """ 70 Raised when an error code is returned by a slalib routine. 71 """
72 - def __init__(function, status):
73 self.function = function 74 self.status = status
75
76 -class tracker(threading.Thread):
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
164 - def run(self):
165 """ 166 Don't call this directly, use start(). 167 """ 168 self.logger.info('Beginning to track target %s' % self.__target.name) 169 170 # Ensure all observed-to-apparent-place parameters will be recalculated 171 # with the next call of calcRotPos(). 172 self.__telescope.resetField() 173 174 # Check each of the 4 possible position angle settings to see which 175 # fall within the range of motion of the rotator, and select the one 176 # which gives the longest time to limit. 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 ### if debug: self.logger.debug('%e\n' % firstpos.getAngle()) 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 # See if warnings are required. 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 #self.logger.debug('Pos: %s, Err: %s' % \ 270 # (position.getAngle('d'), \ 271 # error.getAngle('d'))) 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
334 - def calcRotPos(self, utc):
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
355 - def limit_time(self):
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 #self.logger.debug('pos1: %f, lower: %f, upper%f' % \ 368 # (pos1.getAngle('d'), \ 369 # self.__lower.getAngle('d'), \ 370 # self.__upper.getAngle('d'))) 371 if pos1 > self.__lower and pos1 < self.__upper: 372 373 # Step time forwards until a limit is exceeded. 374 375 while pos1 > self.__lower and pos1 < self.__upper: 376 # Store the previous time and position 377 utc0 = utc1 378 pos0 = pos1 379 # Increment 380 utc1 = utc1 + step 381 # Recalculate 382 pos1 = self.calcRotPos(utc1)[0] 383 384 # Work out which limit has been reached. 385 386 if pos1 < self.__lower: 387 limit = self.__lower 388 elif pos1 > self.__upper: 389 limit = self.__upper 390 391 # Now use iterative linear interpolation to improve 392 # the accuracy. 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 # Should now have an estimate of the time of reaching a limit 412 # to better than the required accuracy. 413 return utc1
414
415 - def TargetPA(self):
416 """ Print the Target PA 417 useage: dazle.rotator_1.TargetPA() 418 """ 419 print 'Target RotPos: ',self.calcRotPos(utc)
420 421
422 -class rotator:
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 # Store all the parameters 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 # Initialise logging. 459 self.logger = logging.getLogger(name) 460 # Open the serial port. If it doesn't work there'll be 461 # a SerialException. 462 try: 463 self.__port = serial.Serial(portname, timeout=5) 464 self.__port.open() 465 self.__port.flushInput() 466 except serial.SerialException: 467 # It's all gone horribly wrong 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 # Get status info from Taranis. 473 self.where() 474 475 # Set motor currents. 476 self.setCurrents(self.__idle, self.__run, self.__boost) 477 478 # Create a threading.Event for control of the tracking thread 479 self.__stop = threading.Event() 480 self.__stop.set() 481 self.lockedOn = threading.Event() 482 self.lockedOn.clear()
483
484 - def updateConfig(self, config):
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 # Shouldn't still be tracking when shutting down, but just in case... 493 self.stopTracking() 494 # Store all the parameters that may have changed. 495 if isinstance(config, ConfigParserRT.SafeConfigParser): 496 #self.logger.debug("Setting target to '%s'." % self.__target.name) 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
508 - def setCurrents(self, idle, run, boost):
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
554 - def setupRotator(self):
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
583 - def setTarget(self, field):
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
597 - def setTelescope(self, observatory):
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
612 - def startTracking(self, logData=False):
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
633 - def stopTracking(self):
634 self.__stop.set()
635
636 - def isTracking(self):
637 try: 638 return self.__tracker.isAlive() 639 except AttributeError: 640 return False
641
642 - def isLockedOn(self):
643 if self.lockedOn.isSet(): 644 return True 645 else: 646 return False
647
648 - def where(self):
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
676 - def Info(self):
677 """ Get Rotator Info useage: dazle.rotator_1.Info() """ 678 679 """ History 20060605 rgm """ 680 681 # logging.basicConfig(level=logging.DEBUG, 682 # format='%(asctime)s %(levelname)-8s %(message)s', 683 # datefmt='%a, %d %b %Y %H:%M:%S', 684 # filename='logfile.log', 685 # filemode='w') 686 # 687 # logging.info('Some information') 688 # logging.debug('A debug message') 689 # logging.warning('A warning#') 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 # print 'RotPos: ',tself.calcRotPos(utc)[0] 697 # print 'Tracking limits: ',self.__tracker.limit_time() 698 print 'Name: ',self.name 699 # print 'Name: ',self.__name 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 # print 'Telescope: ',self.__telescope.altitude.getString('d') 721 # print 'Telescope: ',self.__telescope.longitude.getString('d') 722 # print 'Telescope: ',self.__telescope.__longitude 723 # print 'Telescope: ',self.__telescope__.longitude 724 # print 'Telescope: ',self.__telescope.longitude.getAngle('d') 725 # print 'Telescope: ',self.__telescope,'\n' 726 # print 'Telescope: ',telescope,'\n' 727 728 # print 'Telescope: ',self.__telescope()[0],'\n' 729 730 731 # print 'Telescope: ',self.rotator.telescope,'\n' 732 # print 'Telescope: ',rotator.telescope,'\n' 733 734 735 # self.where() 736 # print 'position(rad): ',self.__position 737 # print 'position(deg): ',self.__position.getAngle('d') 738 # print self.__dt, self.__position.getAngle() 739 740 # self.__port.write('T') 741 # self.__port.write('%e\n' % self.__dt) 742 # self.__port.write('%e\n' % self.__position.getAngle()) 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 # Get the current UTC 762 print 'UTC(current): ',datetime.datetime.utcfromtimestamp(time.time()) 763 764 # Checking the inputs are OK 765 MoveAngleRad = math.radians(MoveAngleDeg) 766 print 'MoveAngle(Radians, Degrees)= ',MoveAngleRad,MoveAngleDeg 767 print 'MoveTimeSec: ',MoveTimeSec 768 769 770 # Check status of Taranis 771 response=self.__port.readline() 772 print 'Taranis response: ',response 773 774 # Determine location of rotator 775 self.where() 776 777 778 # Check Taranis response is available to module 779 print 'Current values of dt(secs), position(radians, degrees): ',self.__dt, self.__position,self.__position.getAngle('d') 780 781 # Compute the new angle 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 # determine current location of rotator from Taranis 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 # print 'pos: ', pos 805 # print 'upper: ', upper 806 # print 'lower: ', lower 807 808 print 'MOVING ',MoveAngleDeg,' degrees to ',math.degrees(NewPosRad) 809 # firstpos, az, zd, pa, HAObs, decObs, RAObs = self.__tracker.calcRotPos(utc) 810 # print 'firstpos: ', firstpos 811 812 # move Rotator using 813 self.__port.timeout = 5 814 self.__port.flushInput() 815 self.__port.write('T') 816 self.__port.write('%e\n' % self.__dt) 817 # self.__port.write('%e\n' % MoveTimeSec) 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 # self.__port.write('%e\n' % MoveAngleRad) 827 # self.__port.write('%e\n' % NextPosRad) 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 # stop tracking 844 self.__port.write('0.0\n') 845 self.__port.write('C') 846 847 # may need to interate a bit including a tolerance? 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
857 -class telescope:
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 # First store the angle type arguments. 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 # Now store the rest 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 # Create a C-array for passing around parameters used in the 912 # pointing calculations. 913 self.__aoprms = slalib.doubleArray(14)
914
915 - def updateConfig(self, config):
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 # Store all the parameters that may have changed. 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
943 - def getIERS(self):
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
977 - def getLocation(self):
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
984 - def setLocation(self, longitude, latitude, altitude):
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
1007 - def resetField(self):
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
1019 - def getPointing(self, field, utc):
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 # First check the nature of the arguments. 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 # If the target has not calculated its apparent place do it 1046 # now. It's not necessary to recalculate this every time, 1047 # once per target per night ought to be sufficient. 1048 if not field.getApparentPlace(): 1049 field.updateApparentPlace(utc) 1050 RAApp, DecApp = field.getApparentPlace() 1051 # Need to transform the datetime object into 1052 # a modified Julian date. 1053 1054 # Get the integer part of the modified Julian date. 1055 imjd, status = slalib.slaCldj(utc.year, utc.month, utc.day) 1056 if status != 0: 1057 raise SlalibError('slaCldj', status) 1058 # Get the fractional part. 1059 status = 0 1060 fmjd, status = slalib.slaDtf2d(utc.hour, utc.minute, \ 1061 utc.second + \ 1062 utc.microsecond/1000000.0) 1063 1064 # Calling slaAop every time is rather slow, so instead use a 1065 # combination of one call to slaAoppa for each field then 1066 # call slaAoppat and slaAopqk to update the position. A fresh 1067 # call to slaAoppa should only be needed when precession effects 1068 # have grown too large. 1069 if not hasattr(self, 'self.__field') or self.__field != field: 1070 # Haven't yet called slaAoppa for this field, do it now. 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 # Already have calculated parameters for this field, 1088 # just need to update the sidereal time. 1089 slalib.salAoppat(imjd + fmjd, self.__aoprms) 1090 1091 # Now calculate the telescope pointing parameters. 1092 az, zd, HAObs, decObs, RAObs = slalib.slaAopqk(\ 1093 RAApp.getAngle(), DecApp.getAngle(), \ 1094 self.__aoprms) 1095 1096 # Now calculate the parallactic angle. 1097 pa = slalib.slaPa(\ 1098 self.__aoprms[13] - RAApp.getAngle(), \ 1099 DecApp.getAngle(), \ 1100 self.__latitude.getAngle()) 1101 1102 # Finally return the results we're interested in. 1103 return angle(az), angle(zd), angle(pa), angle(HAObs), \ 1104 angle(decObs), angle(RAObs)
1105
1106 - def getWavelength(self):
1107 """ 1108 Returns the stored effective wavelength of observations, in microns. 1109 """ 1110 return self.__wavelength
1111
1112 - def setWavelength(self, wavelength):
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
1122 - def getWeather(self):
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
1147 -class target:
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
1179 - def updateConfig(self, config):
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 # Store all the parameters that may have changed. 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
1197 - def getApparentPlace(self):
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
1207 - def getCoords(self):
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
1215 - def getRA(self):
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
1223 - def getDec(self):
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
1231 - def getPA(self):
1232 """ 1233 Returns the position angle for the target as a rotator.angle object 1234 """ 1235 return angle(self.__PA)
1236 1237
1238 - def setPA(self, pa):
1239 """ 1240 Sets the requested position angle. 1241 """ 1242 try: 1243 self.__PA = angle(*pa) 1244 except TypeError: 1245 self.__PA = angle(pa)
1246
1247 - def setCoords(RA, Dec, PA):
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 # If we've changed the coordinates, any previously calculated 1260 # apparent place will not longer be correct, so remove them. 1261 try: 1262 del self.__RAApp 1263 except AttributeError: 1264 pass 1265 try: 1266 del self.__DecApp 1267 except AttributeError: 1268 pass
1269
1270 - def updateApparentPlace(self, tdb):
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 # First thing is to transform the datetime object into 1284 # a modified Julian date. 1285 1286 # Get the integer part of the modified Julian date. 1287 imjd, status = slalib.slaCldj(tdb.year, tdb.month, tdb.day) 1288 if status != 0: 1289 raise SlalibError('slaCldj', status) 1290 1291 # Get the fractional part. 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 # Can now get the apparent place. The zeroes are the 1300 # proper motions, parallax and radial velocity. The 1301 # 2000 is the epoch of the original coords. 1302 RAApp, DecApp = slalib.slaMap(self.__RA.getAngle(), \ 1303 self.__Dec.getAngle(), \ 1304 0, 0, 0, 0, 2000, \ 1305 imjd + fmjd) 1306 # Store the results as rotator.angle objects, and return them. 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 # This is just to force an update of the apparent place every 1325 # now and then during an observing campaign.: 1326 try: 1327 del(self.__RAApp) 1328 except AttributeError: 1329 pass 1330 try: 1331 del(self.__DecApp) 1332 except AttributeError: 1333 pass
1334
1335 -class angle:
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 # 'Rich comparison' functions. Allow angle objects to be compared with 1361 # other angle objects, and also with numeric types. For comparisons with 1362 # numeric types units of radians are implied. 1363
1364 - def __eq__(self, other):
1365 try: 1366 return self.__radians == other.getAngle() 1367 except AttributeError: 1368 return self.__radians == other
1369
1370 - def __ne__(self, other):
1371 try: 1372 return self.__radians != other.getAngle() 1373 except AttributeError: 1374 return self.__radians != other
1375
1376 - def __ge__(self, other):
1377 try: 1378 return self.__radians >= other.getAngle() 1379 except AttributeError: 1380 return self.__radians >= other
1381
1382 - def __gt__(self, other):
1383 try: 1384 return self.__radians > other.getAngle() 1385 except AttributeError: 1386 return self.__radians > other
1387
1388 - def __le__(self, other):
1389 try: 1390 return self.__radians <= other.getAngle() 1391 except AttributeError: 1392 return self.__radians <= other
1393
1394 - def __lt__(self, other):
1395 try: 1396 return self.__radians < other.getAngle() 1397 except AttributeError: 1398 return self.__radians < other
1399 1400 # Arithmetic functions, allows natural basic arithmetic with angle 1401 # objects (addition, subtraction, negation and absolute value) and 1402 # with angle and numeric types (addition and subtraction, for which 1403 # radians are implied, and multiplication and division). The augmented 1404 # and unary arithmetic operators are also implemented, as appropriate. 1405
1406 - def __add__(self, other):
1407 try: 1408 return angle(self.__radians + other.getAngle()) 1409 except AttributeError: 1410 return angle(self.__radians + other)
1411
1412 - def __radd__(self, other):
1413 return angle(self.__radians + other)
1414
1415 - def __iadd__(self, other):
1416 try: 1417 self.__radians += other.getAngle() 1418 except AttributeError: 1419 self.__radians += other 1420 return self
1421
1422 - def __sub__(self, other):
1423 try: 1424 return angle(self.__radians - other.getAngle()) 1425 except AttributeError: 1426 return angle(self.__radians - other)
1427
1428 - def __rsub__(self, other):
1429 return angle(self.__radians - other)
1430
1431 - def __isub__(self, other):
1432 try: 1433 self.__radians -= other.getAngle() 1434 except AttributeError: 1435 self.__radians -= other 1436 return self
1437
1438 - def __mul__(self, other):
1439 return angle(self.__radians * other)
1440
1441 - def __rmul__(self, other):
1442 return angle(self.__radians * other)
1443
1444 - def __imul__(self, other):
1445 self.__radians *= other 1446 return self
1447
1448 - def __div__(self, other):
1449 return angle(self.__radians / other)
1450
1451 - def __idiv__(self, other):
1452 self.__radians /= other 1453 return self
1454
1455 - def __truediv__(self, other):
1456 return angle(self.__radians / other)
1457
1458 - def __itruediv__(self, other):
1459 self.__radians /= other 1460 return self
1461
1462 - def __neg__(self):
1463 return angle(-self.__radians)
1464
1465 - def __pos__(self):
1466 return angle(self)
1467
1468 - def __abs__(self):
1469 return angle(abs(self.__radians))
1470 1471 # Standard string functions 1472
1473 - def __repr__(self):
1474 return "rotator.angle(%s)" % self.__radians
1475
1476 - def __str__(self):
1477 return str(self.__radians)
1478 1479 # The main methods 1480
1481 - def getAngle(self, units='r'):
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
1517 - def getString(self, units='d', ndp=3):
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 # Create a int array to receive the degrees/hour, minutes, seconds 1529 # and fraction of a second. 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 # All is well. 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 # Something went wrong. 1571 raise ParseError('slalib.slaDafin()', status, startPos, endPos)
1572
1573 - def normPi(self):
1574 """ 1575 Normalises the angle into the range +/-Pi. 1576 """ 1577 self.__radians = slalib.slaDrange(self.__radians)
1578
1579 - def norm2Pi(self):
1580 """ 1581 Normalises the angle into the range 0-2Pi. 1582 """ 1583 self.__radians = slalib.slaDranrm(self.__radians)
1584