import ConfigParserRT import linear import logging import math from rotator import angle class detmech: """ Object representing the DAZLE/CIRPASS camera's detector focus/tilt mechanism which consists of a three point kinematic mount. """ def __init__(self, det1, det2, det3, offset1, offset2, offset3, name): """ Arguments: det1: A linear.linear object corresponding to detector motor 1. det2: A linear.linear object corresponding to detector motor 2. det3: A linear.linear object corresponding to detector motor 3. offset1: Offset from the nominal zero position to zero the tilt. offset2: Offset from the nominal zero position to zero the tilt. offset3: Offset from the nominal zero position to zero the tilt. name: An identifier used for logging purposes. """ self.logger = logging.getLogger(name) if isinstance(det1, linear.linear): self.det1 = det1 else: self.logger.critical('det1 is not a linear.linear object!') if isinstance(det2, linear.linear): self.det2 = det2 else: self.logger.critical('det2 is not a linear.linear object!') if isinstance(det3, linear.linear): self.det3 = det3 else: self.logger.critical('det3 is not a linear.linear object!') self.offset1 = int(offset1) self.offset2 = int(offset2) self.offset3 = int(offset3) def updateConfig(self, config): """ Should be called by the parent instrument object's destructor to enable status information to be saved to file on exit. Takes a ConfigParserRT.SafeConfigParser object as argument. """ self.logger.debug("updateConfig() called") if isinstance(config, ConfigParserRT.SafeConfigParser): config.set(self.name, 'offset1', str(self.offset1)) config.set(self.name, 'offset2', str(self.offset2)) config.set(self.name, 'offset3', str(self.offset3)) else: self.logger.critical("Invalid argument to updateConfig(%s)" \ % repr(config)) self.logger.critical("Unable to save status data") def setupDetmech(self): """ Calls the setupLinear() methods of each of the three mechanism motors to establish the centre of the range of movement, limit switch positions and software limits. Due to slight variations in limit switch positions this will not properly zero the tilts of the detector, in order to do this a target screen should be used together with manual tilt adjustments, and then zeroTilts() called to set the required offsets for each motor. Care should be taken in using this routine, and it should not be run unless necessary. As each of the motors is exercised over its full range in turn large tilts will occur and there are some combinations of motor positions where this can result in the detector box contacting the LN2 can. Attempting to ensure that the motors, and in particular motor 3, are near the middle of their range before beginning this process will minimise risk. """ det1.setupLinear() det3.setupLinear() det2.setupLinear() def getPosition(self): """ Returns the current focus position (in microns) and the tilts along the two diagonals of the detector (as rotator.angle objects). """ pos1 = det1.getPosition() - self.offset1 pos2 = det2.getPosition() - self.offset2 pos3 = det3.getPosition() - self.offset3 focus = (pos1 + pos3) / 2.0 tilt1 = angle(math.arctan2(pos1 - pos3, 152000)) tilt2 = angle(math.arctan2(pos2 - focus, 76000)) return focus, tilt1, tilt2 def printPosition(self): """ Similar to getPosition(), but instead prints a human readable string decribing the focus position (microns) and the tiles (degrees). """ focus, tilt1, tilt2 = self.getPosition() print 'Focus: %i, tilt1: %f, tilt2: %f' % \ (focus, tilt1.getAngle('d'), tilt2.getAngle('d')) def zeroTilts(self): """ Call this function after manually zeroing the tilt of the detector by imaging a test target screen. This adjusts the 3 offsets used in calculating the detector position so that afterwards the tilt calculations will be correct. """ # Calculate focus position from old offsets. This will be preserved. pos1 = det1.getPosition() - self.offset1 pos3 = det3.getPosition() - self.offset3 focus = (pos1 + pos3) / 2.0 # Can now calculate new offsets on basis that tilt1 == tilt2 == 0. self.offset1 = det1.getPosition() - focus self.offset2 = det2.getPosition() - focus self.offset3 = det3.getPosition() - focus def moveTo(self, focus, tilt1=0, tilt2=0): """ Arguments: focus: new focus position, in microns. tilt1: new tilt along the 1-3 diagonal, in degrees, default 0 tilt2: new tilt along the 3 diagonal, in degrees, default 0. """ focus = float(focus) # Convert to radians tilt1 = math.radians(float(tilt1)) tilt2 = math.radians(float(tilt2)) # Calculate new required motor positions new1 = focus + 76000 * math.tan(tilt1) + offset1 new3 = focus - 76000 * math.tan(tilt1) + offset3 new2 = focus + 76000 * math.tan(tilt2) + offset2 # Check that the move will not be out of range for any motor. low1, up1 = self.det1.getLimits() low2, up2 = self.det2.getLimits() low3, up3 = self.det3.getLimits() if new1 < low1: self.logger.critical(\ 'Move exceeds lower limit of motor 1, ignored!') return if new1 > up1: self.logger.critical(\ 'Move exceeds upper limit of motor 1, ignored!') return if new2 < low2: self.logger.critical(\ 'Move exceeds lower limit of motor 2, ignored!') return if new2 > up2: self.logger.critical(\ 'Move exceeds upper limit of motor 2, ignored!') return if new3 < low3: self.logger.critical(\ 'Move exceeds lower limit of motor 3, ignored!') return if new3 > up3: self.logger.critical(\ 'Move exceeds upper limit of motor 3, ignored!') return # Now execute the required moves, in millimetre increments # to avoid extreme tilts. cur1 = self.det1.getPosition() cur2 = self.det2.getPosition() cur3 = self.det3.getPosition() while cur1 != new1 or cur2 != new2 or cur3 != new3: if abs(new1-cur1) > 1000: self.det1.moveBy(1000 * (new1-cur1) / abs(new1-cur1)) elif new1 != cur1: self.det1.moveTo(new1) if abs(new2-cur2) > 1000: self.det2.moveBy(1000 * (new2-cur2) / abs(new2-cur2)) elif new2 != cur2: self.det2.moveTo(new2) if abs(new3-cur3) > 1000: self.det3.moveBy(1000 * (new3-cur3) / abs(new3-cur3)) elif new3 != cur3: self.det3.moveTo(new3) cur1 = self.det1.getPosition() cur2 = self.det2.getPosition() cur3 = self.det3.getPosition() def moveBy(dfocus, dtilt1=0, dtilt2=0): """ Arguments: dfocus: The amount that the focus position should change by, in microns. dtilt1: The amount that tilt about the first diagonal should change by in degrees dtilt2: The amount that tilt about the second diagonal should change by in degrees. """ curr_focus, curr_tilt1, curr_tilt2 = self.getPosition() new_focus = curr_focus + float(dfocus) new_tilt1 = curr_tilt1.getAngle('d') + float(dtilt1) new_tilt2 = curr_tilt2.getAngle('d') + flaot(dtilt2) self.moveTo(new_focus, new_tilt1, new_tilt2) def getLimits(self): """ Returns a tuple containing the upper and lower focus position limits for zero tilts. If tilts are non-zero this range will be reduced, and likewise the focus position will determine the range of available tilts. This function doesn't try to work those effects out. """ lowers, uppers = zip(self.det1.getLimits(), self.det2.getLimits(), \ self.det3.getLimits()) lowers = lowers - (self.offset1, self.offset2, self.offset3) uppers = uppers - (self.offset1, self.offset2, self.offset3) return min(lowers), max(uppers)