class SelectorSwitcher(KWS1SelectorSwitcher): """Switcher whose mapping is determined by a list of presets.""" parameter_overrides = { 'presets': Override(type=dictof( str, dictwith(lam=float, speed=float, spread=float, tilted=bool))), }
class SelectorSwitcher(MultiSwitcher): """Switcher whose mapping is determined by a list of presets.""" parameters = { 'presets': Param('Presets that determine the mapping', type=dictof(str, dictwith(lam=float, speed=float, spread=float)), mandatory=True), } attached_devices = { 'det_pos': Attach('Detector preset device', DetectorPosSwitcherMixin), } def doUpdateValue(self, position): self._attached_det_pos._updateMapping(position) def _getWaiters(self): return self._attached_moveables def _start_unchecked(self, position): MultiSwitcher._start_unchecked(self, position) self._attached_det_pos._updateMapping(position)
class SingleSlit(PseudoNOK, HasOffset, Moveable): """Slit using one axis.""" hardware_access = False attached_devices = { 'motor': Attach('moving motor', Moveable), } parameters = { 'mode': Param('Beam mode', type=oneof(*MODES), settable=True, userparam=True, default='slit', category='general'), '_offsets': Param('List of offsets per mode position', settable=False, internal=True, type=dictof(str, float), default={}), 'opmode': Param('Mode of operation for the slit', type=oneof(CENTERED), userparam=True, settable=True, default=CENTERED, category='experiment'), } parameter_overrides = { 'masks': Override(type=dictwith(**{name: float for name in MODES}), unit='', mandatory=True), } valuetype = float def doWriteOffset(self, value): HasOffset.doWriteOffset(self, value) # deep copy is need to be able to change the values d = self._offsets.copy() d[self.mode] = value self._setROParam('_offsets', d) def doRead(self, maxage=0): return self._attached_motor.read(maxage) - self.masks[self.mode] - \ self.offset def doIsAllowed(self, target): return self._attached_motor.isAllowed(target + self.masks[self.mode]) def doStop(self): self._attached_motor.stop() def doStart(self, target): self._attached_motor.start(target + self.masks[self.mode] + self.offset) def doWriteMode(self, mode): self._attached_motor.start( self._attached_motor.read(0) + self.masks[mode] - self.masks[self.mode]) # update the offset parameter from offset mapping self._setROParam('offset', self._offsets.get(mode, 0.)) self.log.debug('New offset is now: %f', self.offset)
class HasSwitchPv(DeviceMixinBase): """ A mixin that can be used with EPICS based devices. Devices that inherit this mixin get a new property that indicates whether the device is switched on (that may mean different things in different devices): dev.isSwitchedOn To switch the device on or off, use the provided methods: dev.switchOn() dev.switchOff() The link to EPICS is configured via the switchpvs and switchstates parameters. The former defines which PV to read for the status information as well as which one to write to when using the methods. The latter defines what values the PV accepts for on and off respectively. """ parameters = { 'switchstates': Param('Map of boolean switch states to underlying type', type=dictwith(on=anytype, off=anytype), userparam=False), 'switchpvs': Param('Read and write pv for switching device on and off.', type=dictwith(read=pvname, write=pvname), userparam=False) } def _get_pv_parameters(self): # Use colon prefix to prevent name clashes with # PVs specified in EpicsDevice.param switch_pvs = {'switchpv:' + pv for pv in self.switchpvs} return super(HasSwitchPv, self)._get_pv_parameters() | switch_pvs def _get_pv_name(self, pvparam): components = pvparam.split(':', 1) if len(components) == 2 and components[0] == 'switchpv': return self.switchpvs[components[1]] return super(HasSwitchPv, self)._get_pv_name(pvparam) @property def isSwitchedOn(self): """ True if the device is switched on. """ raw_value = self._get_pv('switchpv:read') if raw_value not in self.switchstates.values(): self.log.warning('State by attached switch device not recognized. ' 'Returning raw value.') return raw_value return raw_value == self.switchstates['on'] @usermethod def switchOn(self): """ Switch the device on (writes the 'on' of switchstates map to the write-pv specified in switchpvs). """ if not self.isSwitchedOn: self._put_pv('switchpv:write', self.switchstates['on']) else: self.log.info('Device is already switched on') @usermethod def switchOff(self): """ Switch the device off (writes the 'off' of switchstates map to the write-pv specified in switchpvs). """ if self.isSwitchedOn: self._put_pv('switchpv:write', self.switchstates['off']) else: self.log.info('Device is already switched off')
class DetectorPosSwitcher(DetectorPosSwitcherMixin, SequencerMixin, MappedMoveable): """Switcher for the detector and detector position. This controls the large/small detector and the X/Y/Z components of the detector position. Presets depend on the target wavelength given by the selector. """ hardware_access = False attached_devices = { 'det_z': Attach('Large detector Z axis', Moveable), 'bs_x': Attach('Large detector beamstop X axis', Moveable), 'bs_y': Attach('Large detector beamstop Y axis', Moveable), 'psd_x': Attach('Small detector X axis', Moveable), 'psd_y': Attach('Small detector Y axis', Moveable), 'attenuator': Attach('Beam attenuator', Moveable), } parameters = { 'presets': Param('Presets that determine the mappings', type=dictof( str, dictof( str, dictwith(x=float, y=float, z=float, attenuator=str, small=bool))), mandatory=True), 'offsets': Param( 'Offsets to correct TOF chopper-detector length ' 'for the errors in the det_z axis value', type=dictof(float, float), mandatory=True), 'mapkey': Param('Last selector position for mapping', type=str, settable=True, internal=True), 'psdtoppos': Param('"Top" end position of small detector', unit='mm', mandatory=True), 'detbackpos': Param('"Back" end position of large detector', unit='m', mandatory=True), } parameter_overrides = { 'mapping': Override(mandatory=False, settable=True, internal=True), 'fallback': Override(userparam=False, type=str, mandatory=True), } def doInit(self, mode): # check that an offset is defined for each z distance for _selpos, selpresets in self.presets.items(): for _pname, preset in selpresets.items(): if preset['z'] not in self.offsets: raise ConfigurationError( self, 'no detector offset found in configuration ' 'for detector distance of %.2f m' % preset['z']) MappedMoveable.doInit(self, mode) # apply mapping of last selector pos in case it changed if mode == MASTER: self._updateMapping(self.mapkey) def _updateMapping(self, selpos): self.log.debug('updating the detector mapping for selector ' 'setting %s' % selpos) try: pos = self.presets.get(selpos, {}) new_mapping = { k: [v['x'], v['y'], v['z'], v['small'], v['attenuator']] for (k, v) in pos.items() } self.mapping = new_mapping self.mapkey = selpos self.valuetype = oneof_detector(*sorted(new_mapping, key=num_sort)) if self._cache: self._cache.invalidate(self, 'value') self._cache.invalidate(self, 'status') except Exception: self.log.warning('could not update detector mapping', exc=1) def _startRaw(self, pos): if self._seq_is_running(): if self._mode == SIMULATION: self._seq_thread.join() self._seq_thread = None else: raise MoveError( self, 'Cannot start device, sequence is still ' 'running (at %s)!' % self._seq_status[1]) # switch det_img alias synchronously, the chopper sets its mode param! det_img_alias = session.getDevice('det_img') if pos[3]: det_img_alias.alias = 'det_img_jum' else: det_img_alias.alias = 'det_img_ge' seq = [] seq.append(SeqDev(self._attached_attenuator, pos[4])) if pos[3]: seq.append( SeqDev(self._attached_det_z, self.detbackpos, stoppable=True)) seq.append(SeqDev(self._attached_psd_y, pos[1], stoppable=True)) seq.append(SeqDev(self._attached_psd_x, pos[0], stoppable=True)) else: seq.append(SeqDev(self._attached_psd_x, 0, stoppable=True)) seq.append( SeqDev(self._attached_psd_y, self.psdtoppos, stoppable=True)) seq.append(SeqDev(self._attached_bs_y, pos[1], stoppable=True)) seq.append(SeqDev(self._attached_bs_x, pos[0], stoppable=True)) seq.append(SeqDev(self._attached_det_z, pos[2], stoppable=True)) # maybe reposition beamstop Y axis to counter jitter. seq.append(SeqCall(self._check_bsy, pos[1])) self._startSequence(seq) def _check_bsy(self, target): bsy = self._attached_bs_y for _i in range(5): if self._seq_stopflag: return readings = [] for _j in range(20): readings.append(bsy.read(0)) session.delay(0.2) if all(abs(v - target) <= bsy.precision for v in readings): return # it's ok self.log.warning( 'beamstop not ok, waiting 60 seconds for repositioning') session.delay(60) if self._seq_stopflag: return bsy.maw(target) self.log.error('beamstop not ok after 5 reposition tries') def _readRaw(self, maxage=0): return { n: (d.read(maxage), getattr(d, 'precision', 0)) for (n, d) in self._adevs.items() } def _mapReadValue(self, pos): def eq(posname, val): return abs(pos[posname][0] - val) <= pos[posname][1] for name, values in self.mapping.items(): if pos['attenuator'][0] != values[4]: continue if values[3]: if not eq('det_z', self.detbackpos): continue if eq('psd_x', values[0]) and eq('psd_y', values[1]): return name else: if not eq('psd_y', self.psdtoppos): continue if eq('det_z', values[2]) and eq('bs_x', values[0]) and \ eq('bs_y', values[1]): return name return self.fallback def doStatus(self, maxage=0): seq_status = SequencerMixin.doStatus(self, maxage) if seq_status[0] not in (status.OK, status.WARN): return seq_status return MappedMoveable.doStatus(self, maxage) def doReset(self): multiReset(self._adevs) def doStop(self): multiStop(self._adevs)
def doPreinit(self, mode): self._status_devs = ['soz', 'com', 'cox', 'coz', 'd1b', 'd2b', 'd3b', 'd4b', 'aoz', 'aom'] InterfaceLogicalMotorHandler.doPreinit(self, mode) self.valuetype = dictwith(m2t=float, s2t=float, ath=float)
class ChopperMaster(CanReference, BaseSequencer): valuetype = dictwith( wlmin=float, wlmax=float, gap=float, chopper2_pos=intrange(1, 6), D=float, ) parameters = { 'mode': Param('Chopper operation mode (normal, virtual6)', type=oneof('normal_mode', 'virtual_disc2_pos_6'), settable=False, category='status'), 'delay': Param('delay for Startsignal in Degree', type=floatrange(-360, 360), settable=True, userparam=True), 'wlmin': Param('Wavelength min', type=floatrange(0, 30), settable=True, userparam=True, unit='AA', category='status'), 'wlmax': Param('Wavelength max', type=floatrange(0, 30), settable=True, userparam=True, unit='AA', category='status'), 'dist': Param('flight path (distance chopper disc 1 to detector)', type=floatrange(0), settable=True, userparam=True, unit='m', category='status'), 'gap': Param('Gap ... ', type=floatrange(0, 100), settable=True, userparam=True, unit='%', category='status'), 'resolution': Param('Resolution ...', type=intrange(1, 6), settable=False, userparam=True, mandatory=False, volatile=True, category='status'), 'speed': Param('Chopper1 speed ... ', type=float, settable=False, userparam=True, mandatory=False, volatile=True, category='status'), } _max_disks = 6 attached_devices = { 'chopper1': Attach('chopper1 defining speed', Moveable), 'chopper2': Attach('chopper2 phase', Moveable), 'chopper3': Attach('chopper3 phase also height', Moveable), 'chopper4': Attach('chopper4 phase also height', Moveable), 'chopper5': Attach('chopper5 phase half speed', Moveable), 'chopper6': Attach('chopper6 phase half speed', Moveable), 'shutter': Attach('Shutter device', Moveable), } def doInit(self, mode): self._choppers = (self._attached_chopper1, self._attached_chopper2, self._attached_chopper3, self._attached_chopper4, self._attached_chopper5, self._attached_chopper6) def _generateSequence(self, target): self.wlmin, self.wlmax = limits( (target.get('wlmin', self.wlmin), target.get('wlmax', self.wlmax))) self.dist = target.get('D', self.dist) self.gap = target.get('gap', self.gap) chopper2_pos = target.get('chopper2_pos') speed, angles = chopper_config(self.wlmin, self.wlmax, self.dist, chopper2_pos, gap=self.gap) self.log.debug('speed: %d, angles = %s', speed, angles) seq = [] shutter_pos = self._attached_shutter.read(0) shutter_ok = self._attached_shutter.status(0)[0] == status.OK if chopper2_pos == 6: self._setROParam('mode', 'virtual_disc2_pos_6') else: self._setROParam('mode', 'normal_mode') chopper2_pos_akt = self._attached_chopper2.pos if chopper2_pos_akt != chopper2_pos: if shutter_ok: seq.append( SeqDev(self._attached_shutter, 'closed', stoppable=True)) seq.append(SeqDev(self._attached_chopper1, 0, stoppable=True)) seq.append( SeqSlowParam(self._attached_chopper2, 'pos', chopper2_pos)) for dev, t in zip(self._choppers[1:], angles[1:]): # The Chopper measures the phase in the opposite direction # as we do this was catered for here, we have moved the # sign conversion to the doWritePhase function # dev.phase = -t # sign by history seq.append(SeqFuzzyParam(dev, 'phase', t, 0.5)) seq.append(SeqDev(self._attached_chopper1, speed, stoppable=True)) if shutter_ok: seq.append( SeqDev(self._attached_shutter, shutter_pos, stoppable=True)) return seq def doRead(self, maxage=0): # TODO: for cfg value = { 'D': self.dist, 'wlmin': self.wlmin, 'wlmax': self.wlmax, 'gap': self.gap, 'chopper2_pos': self._attached_chopper2.pos if self.mode == 'normal_mode' else 6, } return value def _getWaiters(self): return self._choppers def doReadResolution(self): if self.mode == 'normal_mode': return self._attached_chopper2.pos else: return 6 def doReadSpeed(self): return self._attached_chopper1.read(0)
class DoubleMotorNOK(SequencerMixin, CanReference, PseudoNOK, HasPrecision, Moveable): """NOK using two axes. If backlash is negative, approach form the negative side (default), else approach from the positive side. If backlash is zero, don't mind and just go to the target. """ attached_devices = { 'motor_r': Attach('NOK moving motor, reactor side', Moveable), 'motor_s': Attach('NOK moving motor, sample side', Moveable), } parameters = { 'mode': Param('Beam mode', type=oneof(*MODES), settable=True, userparam=True, default='ng', category='experiment'), 'nok_motor': Param('Position of the motor for this NOK', type=tupleof(float, float), settable=False, unit='mm', category='general'), 'inclinationlimits': Param('Allowed range for the positional ' 'difference', type=limits, mandatory=True), 'backlash': Param('Backlash correction in phys. units', type=float, default=0., unit='main'), 'offsets': Param('Offsets of NOK-Motors (reactor side, sample side)', type=tupleof(float, float), default=(0., 0.), settable=False, unit='main', category='offsets'), } parameter_overrides = { 'precision': Override(type=floatrange(0, 100)), 'masks': Override(type=dictwith(**{name: float for name in MODES}), unit='', mandatory=True), } valuetype = tupleof(float, float) _honor_stop = True @lazy_property def _devices(self): return self._attached_motor_r, self._attached_motor_s def doInit(self, mode): for dev in self._devices: if hasattr(dev, 'backlash') and dev.backlash != 0: self.log.warning( 'Attached Device %s should not have a ' 'non-zero backlash!', dev) def doRead(self, maxage=0): return [ dev.read(maxage) - ofs - self.masks[self.mode] for dev, ofs in zip(self._devices, self.offsets) ] def doIsAllowed(self, targets): target_r, target_s = targets target_r += self.offsets[0] target_s += self.offsets[1] incmin, incmax = self.inclinationlimits inclination = target_s - target_r if not incmin <= inclination <= incmax: return False, 'Inclination %.2f out of limit (%.2f, %.2f)!' % ( inclination, incmin, incmax) for dev in self._devices: res = dev.isAllowed(target_r) if not res[0]: return res # no problems detected, so it should be safe to go there.... return True, '' def doIsAtTarget(self, targets): traveldists = [ target - (akt + ofs) for target, akt, ofs in zip(targets, self.read(0), self.offsets) ] self.log.debug('doIsAtTarget', targets, 'traveldists', traveldists) return max(abs(v) for v in traveldists) <= self.precision def doStop(self): SequencerMixin.doStop(self) for dev in self._devices: dev.stop() try: self.wait() finally: self.reset() def doStart(self, targets): """Generate and start a sequence if none is running. The sequence is optimised for negative backlash. It will first move both motors to the lowest value of (target + backlash, current_position) and then to the final target. So, inbetween, the NOK should be parallel to the beam. """ if self._seq_is_running(): raise MoveError(self, 'Cannot start device, it is still moving!') # check precision, only move if needed! if self.isAtTarget(targets): return devices = self._devices # XXX: backlash correction and repositioning later # build a moving sequence sequence = [] # now go to target sequence.append([ SeqDev(d, t + ofs + self.masks[self.mode], stoppable=True) for d, t, ofs in zip(devices, targets, self.offsets) ]) # now go to target again sequence.append([ SeqDev(d, t + ofs + self.masks[self.mode], stoppable=True) for d, t, ofs in zip(devices, targets, self.offsets) ]) self.log.debug('Seq: %r', sequence) self._startSequence(sequence) def doReset(self): multiReset(self._motors)
class Doppler(SequencerMixin, MultiSwitcher): """Device to change the dopplerspeed. It also compares the speed and amplitude 'seen' by the SIS detector to the values set in the doppler and notifies the user if these values do not match.""" parameters = { 'margins': Param('margin for readout errors in refdevices', dictwith(speed=float, amplitude=float), default=dict(speed=.0, amplitude=.0), settable=True), 'maxacqdelay': Param( 'maximum time to wait for the detector to adjust ' 'when a measurement is started in seconds', int, default=50, settable=True), 'customrange': Param('min and max for custom values', tuple, settable=False, volatile=True) } attached_devices = { 'switch': Attach('The on/off switch of the doppler', NamedDigitalOutput), 'acq': Attach('The doppler as seen by the SIS-Detector', AcqDoppler), } def doInit(self, mode): MultiSwitcher.doInit(self, mode) self._sm_values = sorted( [x for x in self.mapping if isinstance(x, number_types)])[1:] named_vals = {k: v[0] for k, v in self.mapping.items()} self.valuetype = oneofdict_or(named_vals, floatrange(0, self._sm_values[-1])) def doRead(self, maxage=0): if self._attached_switch.read() == 'off': return 0 return self._mapReadValue(self._readRaw(maxage)) def doStart(self, target): # value is out of range if target not in self.mapping and not self.inRange(target): raise UsageError('Values have to be within %.2f and %.2f.' % (self.customrange[0], self.customrange[-1])) # if acq runs in dummy mode this will change the speed accordingly # otherwise this will be ignored in entangle self._attached_acq.changeDummySpeed(target) # a custom value has been requested if target not in self.mapping: self.log.warning('Moving doppler to a speed which is not in the ' 'configured setup (but within range).') for entry in self._sm_values: if target <= entry: self._startRaw((target, self.mapping[entry][1])) return def _startRaw(self, target): if self._mode != SIMULATION \ and session.daemon_device._controller.status == STATUS_INBREAK: raise UsageError('Doppler speed can not be changed when script is ' 'paused.') if self._seq_is_running(): if self._mode == SIMULATION: self._seq_thread.join() else: self._seq_thread.join(0) self._seq_thread = None if not self.waitForAcq(10): return seq = list() # to change the doppler speed it has to be stopped first seq.append(SeqDev(self._attached_switch, 'off')) seq.append(SeqCall(session.delay, 0.5)) if target[0] != 0: seq.append(SeqCall(MultiSwitcher._startRaw, self, target)) seq.append(SeqDev(self._attached_switch, 'on')) seq.append(SeqCall(self.waitForSync, target)) self._startSequence(seq) def acqIsCounting(self): return self._attached_acq.status() == (status.BUSY, 'counting') def waitForAcq(self, retries): if not self.acqIsCounting(): return True while retries: sleep(0.5) retries -= 1 if not self.acqIsCounting(): return True self.log.warning('Doppler speed can not be changed while ' 'SIS is counting.') return False def waitForSync(self, target): if session.mode == SIMULATION: return elif target[0] == 0: while True: if self.withinMargins(self._attached_acq.read()[0], 0, SPEED): return session.delay(1) else: while True: acq_speed, acq_ampl = self._attached_acq.read() speed, ampl = self._readRaw() # acq and doppler are back in sync after changing the doppler if self.withinMargins(acq_speed, speed, SPEED) \ and self.withinMargins(acq_ampl, ampl, AMPLITUDE): return session.delay(1) def withinMargins(self, value, target, name): return target - self.margins[name] < value < target + self.margins[name] def doStatus(self, maxage=0): # when the sequence is running only it's status is of interest if self._seq_status[0] != status.OK: return self._seq_status # otherwise the actual doppler status is relevant acq_speed, acq_ampl = self._attached_acq.read() speed, ampl = self._readRaw() if self._attached_switch.read() == 'off': if not self.withinMargins(acq_speed, 0, SPEED): return (status.WARN, 'detector registers movement of the ' 'doppler, although it has been stopped.') elif not self.withinMargins(acq_speed, speed, SPEED): return (status.WARN, 'detector observes a speed differing ' 'from the dopplerspeed') elif not self.withinMargins(acq_ampl, ampl, AMPLITUDE): return (status.WARN, 'detector observes an amplitude differing ' 'from the doppleramplitude') elif round(speed, 2) not in self._sm_values \ and self.inRange(speed): return status.OK, 'Doppler runs at custom speed' return MultiSwitcher.doStatus(self, maxage) def isAllowed(self, pos): if self.inRange(pos): return True, '' else: return MultiSwitcher.isAllowed(self, pos) def inRange(self, speed): return self.customrange[0] < speed < self.customrange[-1] def _mapReadValue(self, pos): value = MultiSwitcher._mapReadValue(self, pos) if value == self.fallback and self.inRange(pos[0]): value = pos[0] return value def doReadCustomrange(self): return self._attached_moveables[0].userlimits
class DoubleMotorBeckhoffNOK(DoubleMotorBeckhoff): """NOK using two axes. """ parameters = { 'mode': Param('Beam mode', type=oneof(*MODES), settable=True, userparam=True, default='ng', category='experiment'), 'nok_motor': Param('Position of the motor for this NOK', type=tupleof(float, float), settable=False, unit='mm', category='general'), 'inclinationlimits': Param('Allowed range for the positional ' 'difference', type=limits, mandatory=True), 'backlash': Param('Backlash correction in phys. units', type=float, default=0., unit='main'), 'offsets': Param('Offsets of NOK-Motors (reactor side, sample side)', type=tupleof(float, float), default=(0., 0.), settable=False, unit='main', category='offsets'), } parameter_overrides = { 'precision': Override(type=floatrange(0, 100)), 'masks': Override(type=dictwith(**{name: float for name in MODES}), unit='', mandatory=True), 'address': Override(mandatory=False), } valuetype = tupleof(float, float) _honor_stop = True def doInit(self, mode): for name, idx in [('reactor', 0), ('sample', 1)]: self.__dict__[name] = SingleMotorOfADoubleMotorNOK( '%s.%s' % (self.name, name), unit=self.unit, both=self, lowlevel=True, index=idx) def doWriteMode(self, mode): self.log.debug('DoubleMotorBeckhoffNOK arg:%s self:%s', mode, self.mode) target = self.doRead(0) self.log.debug('DoubleMotorBeckhoffNOK target %s', target) target = [pos + self.masks[mode] for pos in target] self.log.debug('DoubleMotorBeckhoffNOK target %s', target) DoubleMotorBeckhoff.doStart(self, target) def doRead(self, maxage=0): self.log.debug('DoubleMotorBeckhoffNOK read') return [ pos - self.masks[self.mode] for pos in DoubleMotorBeckhoff.doRead(self, maxage) ] def doIsAllowed(self, target): self.log.debug('DoubleMotorBeckhoffNOK doIsAllowed') target_r, target_s = (target[0] + self.offsets[0], target[1] + self.offsets[1]) incmin, incmax = self.inclinationlimits inclination = target_s - target_r if not incmin < inclination < incmax: return False, 'Inclination %.2f out of limit (%.2f, %.2f)!' % ( inclination, incmin, incmax) # no problems detected, so it should be safe to go there.... return True, '' def doIsAtTarget(self, pos, target): self.log.debug('DoubleMotorBeckhoffNOK doIsAtTarget') stat = self.status(0) if stat[0] == status.BUSY: return False return True def doStart(self, target): self.log.debug('DoubleMotorBeckhoffNOK doStart') self.log.debug('target %s %s', target, type(target)) target = [pos + self.masks[self.mode] for pos in target] self.log.debug('target %s %s', target, type(target)) DoubleMotorBeckhoff.doStart(self, target)
class AmorLogicalMotorHandler(Moveable): """ Controller for the logical motors. This class has all the equations coded and can be used to read the positions of the logical motors or to calculate the positions of the real motors when the logical motor is to be moved. """ parameter_overrides = { 'fmtstr': Override(volatile=True), 'unit': Override(mandatory=False, default='degree'), } # The real motors attached_devices = { 'soz': Attach('soz motor', EpicsMotor, missingok=True), 'com': Attach('com motor', EpicsMotor, missingok=True), 'cox': Attach('cox motor', EpicsMotor, missingok=True), 'coz': Attach('coz motor', EpicsMotor, missingok=True), 'd1b': Attach('d1b motor', EpicsMotor, missingok=True), 'd2b': Attach('d2b motor', EpicsMotor, missingok=True), 'd3b': Attach('d3b motor', EpicsMotor, missingok=True), 'd4b': Attach('d4b motor', EpicsMotor, missingok=True), 'aoz': Attach('aoz motor', EpicsMotor, missingok=True), 'aom': Attach('aom motor', EpicsMotor, missingok=True), 'd1t': Attach('d1t motor (only to read)', EpicsMotor, missingok=True), 'd2t': Attach('d2t motor (only to read)', EpicsMotor, missingok=True), 'd3t': Attach('d3t motor (only to read)', EpicsMotor, missingok=True), 'd4t': Attach('d4t motor (only to read)', EpicsMotor, missingok=True), 'distances': Attach('Device that stores the distances', DistancesHandler) } valuetype = dictwith(m2t=float, s2t=float, ath=float) status_devs = [ 'soz', 'com', 'cox', 'coz', 'd1b', 'd2b', 'd3b', 'd4b', 'aoz', 'aom' ] status_to_msg = { status.ERROR: 'Error in %s', status.BUSY: 'Moving: %s ...', status.WARN: 'Warning in %s', status.NOTREACHED: '%s did not reach target!', status.UNKNOWN: 'Unknown status in %s!', status.OK: 'Ready.' } def doInit(self, mode): # Collect all the logical motors in this variable self._logical_motors = {} def register(self, motortype, motor): self._logical_motors[motortype] = motor def doReadFmtstr(self): return ', '.join([mt + '=%(' + mt + ').3f' for mt in motortypes]) def _get_dev(self, dev): return getattr(self, '_attached_%s' % dev, None) def _read_dev(self, dev): dev = self._get_dev(dev) return dev.read(0) if dev else 0.0 def _is_active(self, component): distances = self._attached_distances return component in session.loaded_setups and \ isinstance(getattr(distances, component), number_types) def doRead(self, maxage=0): distances = self._attached_distances # Check if the sample and polariser distances are available if not isinstance(distances.sample, number_types): raise PositionError('Distances for sample and polariser unknown') soz = self._read_dev('soz') if not self._is_active('polariser'): actm2t = 0.0 else: dist = abs(distances.sample - distances.polariser) tmp = soz / dist if dist else 0 actm2t = math.degrees(-1 * math.atan(tmp)) if abs(tmp) > 1e-4 else 0.0 if self._is_active('analyser'): aom = self._read_dev('aom') aoz = self._read_dev('aoz') sah = abs(distances.analyser - distances.sample) acts2t = math.degrees(math.atan((aoz - soz) / sah)) + actm2t actath = -1 * (acts2t - actm2t - aom) else: com = self._read_dev('com') acts2t = com + actm2t aom = self._read_dev('aom') actath = aom - com return { M2T: round(actm2t, 3), S2T: round(acts2t, 3), ATH: round(actath, 3) } def doStatus(self, maxage=0): # Check for error and warning in the dependent devices devs = { dev: self._get_dev(dev) for dev in self.status_devs if self._get_dev(dev) } st_devs = multiStatus(devs, 0) devs = [n for n, d in iteritems(devs) if d.status()[0] == st_devs[0]] if st_devs[0] in self.status_to_msg: msg = self.status_to_msg[st_devs[0]] if '%' in msg: msg = msg % ', '.join(devs) return st_devs[0], msg return st_devs def doIsCompleted(self): # No attached devices, so have to manually check the doIsCompleted for dev in self.status_devs: dev = self._get_dev(dev) if dev and not dev.isCompleted(): return False return True def doIsAllowed(self, targets): # Calculate the possible motor positions using these targets motor_targets = self._get_move_list(self._get_targets(targets)) # Check if these positions are allowed and populate the # faults list with the motors that cannot be moved faults = [] for motor, target in motor_targets: allowed, _ = motor.isAllowed(target) if not allowed: faults.append(motor.name) self.log.error('%s cant be moved to %s; limits are %s', motor, motor.format(target, motor.unit), motor.abslimits) # Return false if some motors cannot reach their new target if faults: return False, '%s not movable!' % ', '.join(faults) # Return True if everything ok return True, '' def doStart(self, targets): for motor, target in self._get_move_list(self._get_targets(targets)): self.log.debug('New target for %s: %s', motor, motor.format(target, motor.unit)) motor.move(target) def _get_targets(self, targets): targets_dict = {} current = self.read(0) for mt in motortypes: target = targets.get(mt) if target is None: # If the target is not valid or not specified, fetch the # target from motor itself motor = self._logical_motors.get(mt) if not motor: self.log.debug( 'Missing the logical motor %s! ' 'Using target = %s (current position) ', mt, current[mt]) targets_dict[mt] = current[mt] elif motor.target is not None: targets_dict[mt] = round(motor.target or current[mt], 3) else: targets_dict[mt] = current[mt] else: targets_dict[mt] = round(target, 3) # Return the dictionary of motortype mapped to their targets return targets_dict def _get_move_list(self, targets): # Equations to calculate the positions of the real motors to be moved # are implemented in this function self.log.debug('Recalculating with targets: %s', targets) positions = [] distances = self._attached_distances # soz dist = abs(distances.sample - distances.polariser) soz = dist * math.tan(math.radians(-1 * targets[M2T])) positions.append((self._get_dev('soz'), soz)) # slit 1 is before the monochromator and does not need to be # driven when m2t changes. This is here to make sure that d1b # is in a feasible position. if self._is_active('slit1'): mot = self._read_dev('d1t') val = -.5 * mot positions.append((self._get_dev('d1b'), val)) # slit 2 if self._is_active('slit2'): dist = abs(distances.slit2 - distances.polariser) mot = self._read_dev('d2t') val = dist * math.tan(math.radians(-1 * targets[M2T])) - 0.5 * mot positions.append((self._get_dev('d2b'), val)) # slit 3 if self._is_active('slit3'): dist = abs(distances.slit3 - distances.polariser) mot = self._read_dev('d3t') val = dist * math.tan(math.radians(-1 * targets[M2T])) - 0.5 * mot positions.append((self._get_dev('d3b'), val)) # Analyzer if self._is_active('analyser'): com = targets[S2T] - targets[M2T] + 2 * targets[ATH] sah = abs(distances.analyser - distances.sample) aoz = soz + sah * math.tan( math.radians(targets[S2T] - targets[M2T])) aom = targets[S2T] - targets[M2T] + targets[ATH] positions.append((self._get_dev('aoz'), aoz)) positions.append((self._get_dev('aom'), aom)) # Detector when analyzer present if self._is_active('detector'): sdh = abs(distances.detector - distances.sample) positions.append((self._get_dev('com'), com)) tmp = soz - aoz sqrtsqsum = math.sqrt(sah * sah + tmp * tmp) val = sah - sqrtsqsum + (sdh - sqrtsqsum) * ( math.cos(math.radians(com)) - 1.0) positions.append((self._get_dev('cox'), -1 * val)) val = aoz + (sdh - sqrtsqsum) * math.sin(math.radians(com)) positions.append((self._get_dev('coz'), val)) else: # Detector when no analyzer present com = targets[S2T] - targets[M2T] if self._is_active('detector'): positions.append((self._get_dev('com'), com)) dist = abs(distances.detector - distances.sample) val = -1 * dist * (math.cos(math.radians(com)) - 1.0) positions.append((self._get_dev('cox'), val)) val = dist * math.sin(math.radians(com)) + soz positions.append((self._get_dev('coz'), val)) # slit 4 if self._is_active('slit4'): dist = abs(distances.slit4 - distances.sample) mot = self._read_dev('d4t') if self._is_active('analyser'): val = (soz + dist * math.tan(math.radians(targets[S2T] - targets[M2T])) - 0.5 * mot) else: val = soz + dist * math.tan(math.radians(com)) - 0.5 * mot positions.append((self._get_dev('d4b'), val)) # Return rounded targets and remove unwanted positions return [(dev, round(targ, 3)) for dev, targ in positions if dev]
class DetectorPosSwitcher(DetectorPosSwitcherMixin, SequencerMixin, MappedMoveable): hardware_access = False attached_devices = { 'det_z': Attach('Large detector Z axis', Moveable), 'bs_x': Attach('Large detector beamstop X axis', Moveable), 'bs_y': Attach('Large detector beamstop Y axis', Moveable), } parameters = { 'presets': Param('Presets that determine the mappings', type=dictof(str, dictof(str, dictwith( x=float, y=float, z=float))), mandatory=True), 'offsets': Param('Offsets to correct TOF chopper-detector length ' 'for the errors in the det_z axis value', type=dictof(float, float), mandatory=True), 'mapkey': Param('Last selector position for mapping', type=str, settable=True, internal=True), 'beamstopsettlepos': Param('Settling position for beamstop y axis', settable=True, default=400), } parameter_overrides = { 'mapping': Override(mandatory=False, settable=True, internal=True), 'fallback': Override(userparam=False, type=str, mandatory=True), } def doInit(self, mode): # check that an offset is defined for each z distance for _selpos, selpresets in iteritems(self.presets): for _pname, preset in iteritems(selpresets): if preset['z'] not in self.offsets: raise ConfigurationError( self, 'no detector offset found in configuration ' 'for detector distance of %.2f m' % preset['z']) MappedMoveable.doInit(self, mode) # apply mapping of last selector pos in case it changed if mode == MASTER: self._updateMapping(self.mapkey) def _updateMapping(self, selpos): self.log.debug('updating the detector mapping for selector ' 'setting %s' % selpos) try: pos = self.presets.get(selpos, {}) new_mapping = {k: [v['x'], v['y'], v['z']] for (k, v) in pos.items()} self.mapping = new_mapping self.mapkey = selpos self.valuetype = oneof_detector(*sorted(new_mapping, key=num_sort)) if self._cache: self._cache.invalidate(self, 'value') self._cache.invalidate(self, 'status') except Exception: self.log.warning('could not update detector mapping', exc=1) def _startRaw(self, pos): if self._seq_is_running(): if self._mode == SIMULATION: self._seq_thread.join() self._seq_thread = None else: raise MoveError(self, 'Cannot start device, sequence is still ' 'running (at %s)!' % self._seq_status[1]) seq = [] seq.append(SeqDev(self._attached_bs_y, pos[1], stoppable=True)) seq.append(SeqDev(self._attached_bs_x, pos[0], stoppable=True)) seq.append(SeqDev(self._attached_det_z, pos[2], stoppable=True)) # if z has to move, reposition beamstop y afterwards by going to # some other value (damping vibrations) and back if abs(self._attached_det_z.read(0) - pos[2]) > self._attached_det_z.precision: seq.append(SeqDev(self._attached_bs_y, self.beamstopsettlepos, stoppable=True)) seq.append(SeqSleep(30)) seq.append(SeqDev(self._attached_bs_y, pos[1], stoppable=True)) self._startSequence(seq) def _readRaw(self, maxage=0): return {n: (d.read(maxage), getattr(d, 'precision', 0)) for (n, d) in self._adevs.items()} def _mapReadValue(self, pos): def eq(posname, val): return abs(pos[posname][0] - val) <= pos[posname][1] for name, values in iteritems(self.mapping): if eq('det_z', values[2]) and eq('bs_x', values[0]) and \ eq('bs_y', values[1]): return name return self.fallback def doStatus(self, maxage=0): seq_status = SequencerMixin.doStatus(self, maxage) if seq_status[0] not in (status.OK, status.WARN): return seq_status return MappedMoveable.doStatus(self, maxage) def doReset(self): multiReset(self._adevs) def doStop(self): multiStop(self._adevs)
def doPreinit(self, mode): self._status_devs = ['slit1', 'slit2', 'slit2z', 'slit3', 'slit3z'] InterfaceLogicalMotorHandler.doPreinit(self, mode) self.valuetype = dictwith(div=float, did=float, dih=float)
class HasDisablePv(CanDisable): """ A mixin that can be used with EPICS based devices. Devices that inherit this mixin get a new property that indicates whether the device is enabled (that may mean different things in different devices): dev.isEnabled To enable or disable, use the provided methods: dev.enable() dev.disable() The link to EPICS is configured via the switchpvs and switchstates parameters. The former defines which PV to read for the status information as well as which one to write to when using the methods. The latter defines what values the PV accepts for enable and disable respectively. """ parameters = { 'switchstates': Param('Map of boolean states to underlying type', type=dictwith(enable=anytype, disable=anytype), userparam=False), 'switchpvs': Param('Read and write pv for enabling and disabling the device', type=dictwith(read=pvname, write=pvname), userparam=False) } def _get_pv_parameters(self): # Use colon prefix to prevent name clashes with # PVs specified in EpicsDevice.param switch_pvs = {'switchpv:' + pv for pv in self.switchpvs} return super()._get_pv_parameters() | switch_pvs def _get_pv_name(self, pvparam): components = pvparam.split(':', 1) if len(components) == 2 and components[0] == 'switchpv': return self.switchpvs[components[1]] return super()._get_pv_name(pvparam) @property def isEnabled(self): """ True if the device is switched on. """ raw_value = self._get_pv('switchpv:read') if raw_value not in self.switchstates.values(): raise ConfigurationError('State by attached switch device not ' 'recognized.') return raw_value == self.switchstates['enable'] @usermethod def enable(self): """ Switch the device on (writes the 'enable' of switchstates map to the write-pv specified in switchpvs). """ if not self.isEnabled: self._put_pv('switchpv:write', self.switchstates['enable']) else: self.log.info('Device is already switched on') @usermethod def disable(self): """ Switch the device off (writes the 'disable' of switchstates map to the write-pv specified in switchpvs). """ if self.isEnabled: self._put_pv('switchpv:write', self.switchstates['disable']) else: self.log.info('Device is already disabled')