class ArmController(IsController, Device): parameters = { 'minangle': Param('Minimum angle between two arms', type=floatrange(0, None), settable=False, userparam=False, default=50.), } attached_devices = { 'arm1': Attach('Arm 1 device', Moveable), 'arm2': Attach('Arm 2 device', Moveable), } parameter_overrides = { 'lowlevel': Override(default=True), } def isAdevTargetAllowed(self, adev, adevtarget): self.log.debug('%s: %s', adev, adevtarget) if adev == self._attached_arm1: target = self._attached_arm2.target if target is None: target = self._attached_arm2.read(0) absdiff = target - adevtarget else: target = self._attached_arm1.target if target is None: target = self._attached_arm1.read(0) absdiff = adevtarget - target if absdiff < 0: return False, 'Arms will cross.' dist = abs(absdiff) if dist >= self.minangle: return True, '' return False, 'Arms become too close to each other: %.3f deg, min. ' \ 'dist is %.3f' % (dist, self.minangle)
class HasMapping(DeviceMixinBase): """ Mixin class for devices that use a finite mapping between user supplied input and internal representation. This is mainly useful for devices which can only yield certain values or go to positions from a predefined set, like switching devices. Abstract classes that use this mixin are implemented in `nicos.devices.abstract.MappedReadable` and `.MappedMoveable`. """ parameters = { 'mapping': Param('Mapping of device values to raw (internal) values', unit='', settable=False, mandatory=True, type=dictof(str, anytype)), 'fallback': Param( 'Readback value if the raw device value is not in ' 'the mapping or None to disable', default=None, unit='', type=anytype, settable=False), } # mapped values usually are string constants and have no unit parameter_overrides = { 'unit': Override(mandatory=False), } def doIsAllowed(self, target): if target not in self.mapping: return False, 'unknown value: %r, must be one of %s' % \ (target, ', '.join(map(repr, sorted(self.mapping)))) return True, ''
class FITSImageSink(ImageSink): """Writes data to a FITS (Flexible image transport system) format file. NICOS headers are also written into the file using the standard FITS header facility, with HIERARCH type keys. Requires the pyfits library to be installed. """ parameter_overrides = { 'filenametemplate': Override(default=['%(pointcounter)08d.fits']), } handlerclass = FITSImageSinkHandler def doPreinit(self, _mode): # Stop creation of the FITSImageSink as it would make no sense # without pyfits. if pyfits is None: raise NicosError(self, 'pyfits module is not available. Check' ' if it is installed and in your PYTHONPATH') def isActiveForArray(self, arraydesc): return len(arraydesc.shape) == 2
class CalibratedMagnet(HasLimits, Moveable): """Base clase for magnet supplies having an bipolar current source. Use this for magnets which can be calibrated, i.e. where:: B(I) = Ic0 + c1*erf(c2*I) + c3*atan(c4*I) works reasonably well. Coefficients c0..c4 are given as 'calibration' parameter. """ attached_devices = { 'currentsource': Attach('bipolar Powersupply', Moveable), } parameters = { 'ramp': Param('Target rate of field change per minute', unit='main/min', mandatory=False, settable=True, volatile=True), 'calibration': Param( 'Coefficients for calibration ' 'function: [c0, c1, c2, c3, c4] calculates ' 'B(I) = c0*I + c1*erf(c2*I) + c3*atan(c4*I)' ' in T', type=tupleof(float, float, float, float, float), default=(1.0, 0.0, 0.0, 0.0, 0.0), settable=True, chatty=True), } parameter_overrides = { 'unit': Override(mandatory=False, default='T'), 'abslimits': Override(mandatory=False, volatile=True), } def _current2field(self, current, *coefficients): """Return field in T for given current in A. Should be monotonic and asymetric or _field2current will fail! Note: This may be overridden in derived classes. """ v = coefficients or self.calibration if len(v) != 5: self.log.warning('Wrong number of coefficients in calibration ' 'data! Need exactly 5 coefficients!') return v[0]*current + v[1]*math.erf(v[2]*current) + \ v[3]*math.atan(v[4]*current) def _field2current(self, field): """Return required current in A for requested field in T. Note: This may be overridden in derived classes. """ # binary search/bisection maxcurr = self._attached_currentsource.abslimits[1] mincurr = -maxcurr maxfield = self._current2field(maxcurr) minfield = self._current2field(mincurr) if not minfield <= field <= maxfield: raise LimitError( self, 'requested field %g %s out of range %g..%g %s' % (field, self.unit, minfield, maxfield, self.unit)) res = fsolve(lambda cur: self._current2field(cur) - field, 0)[0] self.log.debug('current for %g %s is %g', field, self.unit, res) return res def doRead(self, maxage=0): return self._current2field(self._attached_currentsource.read(maxage)) def doStart(self, target): self._attached_currentsource.start(self._field2current(target)) def doStop(self): self._attached_currentsource.stop() def doReset(self): return self._attached_currentsource.reset() def doReadRamp(self): # This is an approximation! return self.calibration[0] * abs(self._attached_currentsource.ramp) def doWriteRamp(self, newramp): # This is an approximation! self._attached_currentsource.ramp = newramp / self.calibration[0] def doReadAbslimits(self): minfield, maxfield = [ self._current2field(I) for I in self._attached_currentsource.abslimits ] # include 0 in allowed range if minfield > 0: minfield = 0 if maxfield < 0: maxfield = 0 # get configured limits if any, or take max from source limits = self._config.get('abslimits', (minfield, maxfield)) # in any way, clamp limits to what the source can handle limits = [clamp(i, minfield, maxfield) for i in limits] return min(limits), max(limits) def doWriteUserlimits(self, limits): HasLimits.doWriteUserlimits(self, limits) # all Ok, set source to max of pos/neg field current maxcurr = max(self._field2current(i) for i in limits) mincurr = min(self._field2current(i) for i in limits) self._attached_currentsource.userlimits = (mincurr, maxcurr) def doTime(self, startval, target): # get difference in current delta = abs( self._field2current(target) - self._field2current(startval)) # ramp is per minute, doTime should return seconds return 60 * delta / self._attached_currentsource.ramp @usermethod def calibrate(self, fieldcolumn, *scannumbers): """Calibrate the B to I conversion, argument is one or more scan numbers. The first argument specifies the name of the device which should be used as a measuring device for the field. Example: >>> B_mira.calibrate(Bf, 351) """ scans = session.experiment.data.getLastScans() self.log.info('determining calibration from scans, please wait...') Is = [] Bs = [] currentcolumn = self._attached_currentsource.name # XXX(dataapi): adapt to new Dataset class for scan in scans: if scan.counter not in scannumbers: continue if fieldcolumn not in scan.ynames or \ currentcolumn not in scan.xnames: self.log.info('%s is not a calibration scan', scan.counter) continue xindex = scan.xnames.index(currentcolumn) yindex = scan.ynames.index(fieldcolumn) yunit = scan.yunits[yindex] if yunit == 'T': factor = 1.0 elif yunit == 'mT': factor = 1e-3 elif yunit == 'uT': factor = 1e-6 elif yunit == 'G': factor = 1e-4 elif yunit == 'kG': factor = 1e-1 else: raise NicosError( self, 'unknown unit for B field ' 'readout: %r' % yunit) for xr, yr in zip(scan.xresults, scan.yresults): Is.append(xr[xindex]) Bs.append(yr[yindex] * factor) if not Is: self.log.error('no calibration data found') return fit = Fit('calibration', self._current2field, ['c%d' % i for i in range(len(self.calibration))], [1] * len(self.calibration)) res = fit.run(Is, Bs, [1] * len(Bs)) if res._failed: self.log.warning('fit failed') return self.calibration = res._pars[1]
class Group(Readable): valuetype = bool parameters = { 'bitlist': Param( 'Definition of a bit list', type=listof(str), settable=False, userparam=False, ), 'okmask': Param( 'Mask to define the bits results OK status', type=int, settable=False, userparam=False, ), } attached_devices = { 'shs': Attach('shs', Readable), } parameter_overrides = { 'unit': Override(default='', volatile=True, mandatory=False), 'fmtstr': Override(default='%s'), } def doReadUnit(self): return '' _register = { 'Shutter': (0, 0), # 0x0000 'Ampeltest_inv': (3, 0), # 0x0001 'Betreten_Verboten_inv': (3, 1), # 0x0002 'Hupentest_inv': (3, 2), # 0x0004 'Schluesselschalter_Wartung_inv': (3, 3), # 0x0008 'Tuer_PO_auf': (3, 6), # 0x0040 'Tuer_PO_zu': (3, 7), # 0x0080 'Schnellschluss-Shutter_auf': (3, 8), # 0x0100 'Schnellschluss-Shutter_zu': (3, 9), # 0x0200 '6-fach-Shutter_auf': (3, 10), # 0x0400 '6-fach-Shutter_zu': (3, 11), # 0x0800 'Verbindung_zu_Warte_iO': (3, 12), # 0x1000 'Freigabe_von_Warte_fuer_ESShutter': (3, 13), # 0x2000 'Instrumentenverantwortlicher': (3, 14), # 0x4000 'Not-Aus_Kreis_inv': (3, 15), # 0x8000 'Verbindungstuer': (4, 8), # 0x0100 'Tuer_SR_auf': (4, 10), # 0x0400 'Tuer_SR_zu': (4, 11), # 0x0800 'externer_User_Kontakt_A': (5, 0), # 0x0001 'externer_User_Kontakt_B': (5, 1), # 0x0002 'PO-Aus-Schalter_1': (5, 2), # 0x0004 'PO-Aus-Schalter_2': (5, 4), # 0x0008 'Drucksensor_CB': (6, 0), # 0x0001 'Drucksensor_SFK': (6, 1), # 0x0002 'Drucksensor_Tube': (6, 2), # 0x0004 'Chopper_Drehzahl': (6, 3), # 0x0008 'Druck_service_inv': (6, 4), # 0x0010 'Personenschluessel_Terminal': (6, 11), # 0x0800 'Freigabe_Taster': (6, 12), # 0x1000 'Lampentest_inv': (6, 13), # 0x2000 'Endschalter_Ex_Shutter_inv': (6, 14), # 0x4000 'Handbetrieb_tube_inv': (6, 15), # 0x8000 'Probenort_Geraeumt_inv': (14, 2), # 0x0004 'Streurohr_Geraeumt_inv': (14, 3), # 0x0008 'IV_key_1': (15, 8), # 0x0100 'IV_key_2': (15, 9), # 0x0200 'gelb_inv': (17, 3), # 0x0008 'Freigabe_EIN': (17, 10), # 0x0400 'rot_inv': (18, 8), # 0x0100 'Warnschilder': (18, 9), # 0x0200 'Keine_Freigabe_Hub_Streurohr': (18, 10), # 0x0400 # nicht konzeptionell aber geht 'Freigabe_Hub_Streurohr_inv': (18, 10), # 0x0400 'shutterzustand': (18, 11), # 0x0800 'gruen_inv': (18, 12), # 0x0800 } def _do_read_bits(self): res = 0 # take values from cache to avoid to many reads raw = self._attached_shs.read() for i, key in enumerate(self.bitlist): address, bit = self._register[key] res |= ((raw[address] & (1 << bit)) >> bit) << i return res def doRead(self, maxage=0): return self._do_read_bits() == self.okmask def doStatus(self, maxage=0): bits = self._do_read_bits() if bits == self.okmask: return status.OK, '' return status.WARN, ', '.join( self.format_statusbits(bits ^ self.okmask, self.bitlist)) def format_statusbits(self, sword, labels, start=0): """Return a list of labels according to bit state in `sword` starting with bit `start` and the first label in `labels`. """ smsg = [] for i, lbl in enumerate(labels, start): if sword & (1 << i) and lbl: smsg.append(lbl) return smsg
class StackedAxis(HasLimits, HasPrecision, Moveable): """Device were two axes stacked. Both axes can be moved individually but the result of position and target is the sum of both position. One of the axes (at the moment always the ``top`` axis) is preferred in use since it is cheaper (in time) to move it. ``HasPrecision`` is needed for the TAS monochromator device. """ attached_devices = { 'bottom': Attach('bottom axis', Moveable), 'top': Attach('top axis', Moveable), } parameter_overrides = { 'abslimits': Override(mandatory=False, volatile=True), 'unit': Override(default='deg', mandatory=False), } def doRead(self, maxage=0): self.log.debug('doRead: %s', maxage) return self._attached_bottom.read(maxage) + \ self._attached_top.read(maxage) def doStart(self, target): targets = self._calc_targets(target) # The order is normally free to choose, but in the current application # the bottom device moves blocking self._attached_top.move(targets[1]) self._attached_bottom.move(targets[0]) def doIsAllowed(self, target): targets = self._calc_targets(target) for dev, t in zip([self._attached_bottom, self._attached_top], targets): allowed = dev.isAllowed(t) if not allowed[0]: return allowed return True, '' def _calc_targets(self, target): """Calculate individual target positions from the sum target. Due to the limited range of movement of each individual axis the targets must be calculated for each axis. Since the top axis is cheaper in time it will preferred in use and even the longer way if moving both. """ targets = (0, 0) bpos = self._attached_bottom.read(0) lt = self._attached_top.userlimits # Simply move the top axis if lt[0] + bpos <= target <= lt[1] + bpos: targets = bpos, target - bpos else: tpos = self._attached_top.read(0) if target > (bpos + tpos): targets = target - lt[1], lt[1] else: targets = target - lt[0], lt[0] return targets def doReadAbslimits(self): return [ sum(x) for x in zip(self._attached_bottom.abslimits, self._attached_top.abslimits) ]
class Configuration(PyTangoDevice, PassiveChannel): """Channel that allows to configure various parameters of the DECTRIS Pilatus detector. Without this channel you cannot access any parameter of the Pilatus detector except for the exposure time (TimerChannel) out of NICOS. You can attach devices to this channel in order to read out their values and store them in the Pilatus image header via the ``mxsettings`` parameter. """ # TODO: add proper descriptions attached_devices = { 'detector_distance': Attach( 'Current detector distance to sample.', Readable, optional=True, ), 'detector_voffset': Attach( 'Current vertical detector translation.', Readable, optional=True, ), 'flux': Attach( 'Current photon flux in cps.', Readable, optional=True, ), 'filter_transmission': Attach( 'Current transmission filter factor.', Readable, optional=True, ), 'start_angle': Attach( '', Readable, optional=True, ), 'detector_2theta': Attach( 'Current two-theta position.', Readable, optional=True, ), 'polarization': Attach( '', Readable, optional=True, ), 'alpha': Attach( 'Current alpha position.', Readable, optional=True, ), 'kappa': Attach( 'Current kappa position.', Readable, optional=True, ), 'phi': Attach( 'Current phi position.', Readable, optional=True, ), 'chi': Attach( 'Current chi position.', Readable, optional=True, ), 'omega': Attach( 'Current omega position.', Readable, optional=True, ), 'start_position': Attach( '', Readable, optional=True, ), 'shutter_time': Attach( '', Readable, optional=True, ), } parameters = { 'energy': Param( 'X-ray and threshold energy in kilo electron volt. Set to "None" ' 'if the energy is either not set yet not configurable for this ' 'detector.', type=none_or( dictwith(**dict((p, float) for p in ENERGY_PARAMETERS))), settable=True, volatile=True, unit='keV', prefercache=False, chatty=True, ), 'exposures': Param( 'Number of exposures to accumulate per frame/readout.', type=intrange(1, 2**32 - 1), settable=True, volatile=True, userparam=False, unit='', ), 'imageheader': Param( 'String to be included in the image header.', type=str, settable=True, volatile=True, unit='', chatty=True, ), 'images': Param( 'Number of images for an automatic sequence.', type=intrange(1, 2**16 - 1), settable=True, volatile=True, userparam=False, unit='', ), 'mxsettings': Param( 'Crystallographic parameters reported in the image header.', type=dictof(oneof(*MX_PARAMETERS), anytype), settable=True, volatile=True, unit='', prefercache=False, ), 'period': Param( 'Exposure period in seconds (must be longer than exposure time + ' '2.28 ms readout time).', type=floatrange(1.0015, 60 * 24 * 60 * 60), # maximum: 60 days settable=True, volatile=True, userparam=False, unit='s', ), 'sensorvalues': Param( 'Relative humidity and temperature sensor values on all channels.', type=dictof(str, str), unit='', volatile=True, ), } parameter_overrides = { 'lowlevel': Override(default=True), } def _poll_all_channels(self): # update the status of all pilatus detector channels for detector in session.experiment.detectors: if isinstance(detector, Detector): detector.poll() def doRead(self, maxage=0): return [] def valueInfo(self): return () def doStatus(self, maxage=0): return PyTangoDevice.doStatus(self, maxage)[0], '' def doPrepare(self): self.doUpdateMxsettings({}) def doReadEnergy(self): values = self._dev.energy return dict(zip(ENERGY_PARAMETERS, values)) if all(values) else None def _write_energy(self, value): # only send the energy parameters to the hardware if they have changed if self.energy: for param in ENERGY_PARAMETERS: if abs(value[param] - self.energy[param]) > 0.001: self._dev.energy = [value['xray'], value['threshold']] return def doWriteEnergy(self, value): self._write_energy(value) self._poll_all_channels() def doUpdateEnergy(self, value): # only necessary for transmitting setup values to the hardware if self.doStatus()[0] == status.OK: self._write_energy(value) def doReadExposures(self): return self._dev.exposures def doReadImages(self): return self._dev.images def doWriteImages(self, value): self._dev.images = value def doReadImageheader(self): return self._dev.imageHeader def doWriteImageHeader(self, value): self._dev.imageHeader = value def doReadMxsettings(self): mx_settings = self._dev.mxSettings if not mx_settings: return {} # create dict {k1: v1, k2: v2, ...} from list [k1, v1, k2, v2, ...] mx_settings = {mx_settings[2 * i]: mx_settings[2 * i + 1] for i in range(len(mx_settings) // 2)} # convert values to tuple, float or int return {k: MX_PARAMETERS[k](v) for k, v in mx_settings.items()} def doWriteMxsettings(self, value): start_time = time() # energy update must be completed after maximum 15 seconds while time() < start_time + 15: if self.doStatus()[0] == status.OK: break self.log.info('waiting until the detector is ready') session.delay(1.5) else: self.log.error('mxsettings update could not be performed: ' 'pilatus detector is still busy') return # flatten dict {k1: v1, k2: v2, ...} to [k1, v1, k2, v2, ...] self._dev.mxSettings = [str(v) for v in list(sum(value.items(), ()))] def doUpdateMxsettings(self, value): value = dict(value) # convert to writable dict for name, adev in ((k, v) for k, v in self._adevs.items() if v): adev_value = adev.read() if name == 'filter_transmission': adev_value = 1 / adev_value value[name] = str(adev_value) if value: self.doWriteMxsettings(value) def doReadPeriod(self): return self._dev.period def doWritePeriod(self, value): self._dev.period = value def doReadSensorvalues(self): sensor_values = self._dev.sensorValues # create dict {k1: v1, k2: v2, ...} from list [k1, v1, k2, v2, ...] return {sensor_values[2 * i]: sensor_values[2 * i + 1] for i in range(len(sensor_values) // 2)} @usermethod @requires(level=USER) def setEnergy(self, radiation=None, **value): """Either load the predefined settings that are suitable for usage with silver, chromium, copper, iron oder molybdenum radiation or set the x-ray and threshold energy to any other appropriate values. :param str radiation: 'Ag', 'Cr', 'Cu', 'Fe' or 'Mo' (case insensitive) :param dict[str, float] value: {'xray': x, 'threshold': y} """ if not (radiation or value) or radiation and value: raise InvalidValueError('call either dev.SetEnergy("<radiation>") ' 'or dev.SetEnergy(xray=x, threshold=y)') if radiation: self._dev.LoadEnergySettings(radiation) self._poll_all_channels() else: self.energy = value
class SANSNexusSink(NexusSink): parameter_overrides = { 'settypes': Override(settable=True), }
class HVSwitch(SequencerMixin, MappedMoveable): """High voltage convenience switching device for the CHARM detector.""" valuetype = oneof('on', 'off', 'safe') attached_devices = { 'anodes': Attach('HV channels for the anodes', Moveable, multiple=[2, 9]), 'banodes': Attach('HV channels for the boundary anodes', Moveable, multiple=[1, 8]), 'cathodes': Attach('HV channels for the boundary cathodes', Moveable, multiple=2), 'window': Attach('HV channel for the window', Moveable, multiple=1), 'trip': Attach('Devices signaling a trip on the hardware', Readable), } parameters = { '_tripped': Param('Indicator for hardware trip', type=bool, internal=True, default=False), } parameter_overrides = { 'unit': Override(default='', mandatory=False), 'fallback': Override(default='unknown'), 'mapping': Override(type=dictof(str, dictof(str, float))), } def doInit(self, mode): if self.fallback in self.mapping: raise ConfigurationError( self, 'Value of fallback parameter is ' 'not allowed to be in the mapping!') self._devices = { dev.name: dev for dev in (self._attached_anodes + self._attached_banodes + self._attached_cathodes + self._attached_window) } if len(self._attached_anodes) != len(self._attached_banodes) + 1: raise ConfigurationError( self, 'Number of anode devices must be ' 'the number of boundary anodes + 1: %d, ' '%d' % (len(self.anodes), len(self.banodes))) # if not self.relax_mapping: # self.valuetype = oneof(*sorted(self.mapping, key=num_sort)) for value in sorted(self.mapping, key=num_sort): try: self.valuetype(value) except ValueError as err: raise ConfigurationError( self, '%r not allowed as key in mapping. %s' % (value, err)) from err def doIsAllowed(self, target): if target == 'off': ok = True else: ok = not self._tripped and not self._attached_trip.read(0) return ok, '' if ok else 'hardware is tripped' def doStop(self): if not self._tripped: SequencerMixin.doStop(self) else: raise ModeError(self, "can't be stopped, device is tripped.") def doReset(self): if self._tripped: if self.doStatus(0)[0] == status.BUSY or self._hardware_tripped(): raise ModeError(self, "can't reset device. Hardware is tripped") SequencerMixin.doReset(self) self._setROParam('_tripped', False) def doPoll(self, n, maxage): if not self._tripped and session.sessiontype == POLLER and \ self._hardware_tripped(): self._setROParam('_tripped', True) def _hardware_tripped(self): return self._attached_trip.read(0) == 'Trip' def _generateSequence(self, target): anodes = self._attached_anodes + self._attached_banodes seq = [ [ SeqDev(dev, self.mapping[target][dev.name]) for dev in self._attached_window ], [SeqDev(dev, self.mapping[target][dev.name]) for dev in anodes], [ SeqDev(dev, self.mapping[target][dev.name]) for dev in self._attached_cathodes ], ] return seq def _is_at_target(self, pos, target): # if values are exact the same if pos == target: return True for dev in pos: if not self._devices[dev].isAtTarget(pos[dev], target[dev]): return False return True def _mapReadValue(self, value): for val in self.mapping: if self._is_at_target(value, self.mapping[val]): return val if self.fallback is not None: return self.fallback else: raise PositionError(self, 'unknown unmapped position %r' % value) def _readRaw(self, maxage=0): return {dev.name: dev.read(maxage) for dev in self._devices.values()} def _startRaw(self, target): ramp = 60 * self.mapping[self.target]['ramp'] seq = self._generateSequence(self.target) if self.target in ['off', 'safe']: seq.reverse() self._startSequence([ SeqParam(dev, 'ramp', ramp) for dev in self._attached_anodes + self._attached_banodes + self._attached_cathodes + self._attached_window ] + seq)
class HasWindowTimeout(HasPrecision, HasTimeout): """ Mixin class for devices needing a more fancy timeout handling than `HasTimeout`. Basically we keep a (length limited) history of past values and check if they are close enough to the target (deviation is smaller than `precision`). The length of that history is determined by :attr:`~HasWindowTimeout.window`. In any case the last read value is used to determine `isAtTarget`. If the value is outside the defined window for longer than :attr:`~HasTimeout.timeout` seconds after the HW is no longer busy. Also we add a stabilising phase in the timeouttimes list. """ parameters = { 'window': Param('Time window for checking stabilization', unit='s', default=60.0, fmtstr='%.1f', settable=True, category='general'), } parameter_overrides = { 'precision': Override(mandatory=True, type=floatrange(1e-8)), } @lazy_property def _history(self): if self._cache: self._cache.addCallback(self, 'value', self._cacheCB) self._subscriptions.append(('value', self._cacheCB)) t = currenttime() return self._cache.history(self, 'value', t - self.window, t) return [] # use values determined by poller or waitForCompletion loop # to fill our history def _cacheCB(self, key, value, time): self._history.append((time, value)) # clean out stale values, if more than one stale = None for i, entry in enumerate(self._history): t, _ = entry if t >= time - self.window: stale = i break else: return # remove oldest entries, but keep one stale if stale > 1: del self._history[:stale - 1] def _getTimeoutTimes(self, current_pos, target_pos, current_time): """Calculates timestamps for timeouts returns an iterable of tuples (status_string, timestamp) with ascending timestamps. First timestamp has to be `current_time` which is the only argument to this. The last timestamp will be used as the final timestamp to determine if the device's movement timed out or not. Additional timestamps (in between) may be set if need for _combinedStatus returning individual status text's (e.g. to differentiate between 'ramping' and 'stabilization'). """ res = HasTimeout._getTimeoutTimes(self, current_pos, target_pos, current_time) if not res: return None # we just append the window time after the timeout time # logically wrong order, but nobody uses the strings anyway res.append(('', res[-1][1] + self.window)) return res def isAtTarget(self, val): ct = currenttime() self._cacheCB('value', val, ct) if self.target is None: return True # check subset of _history which is in window # also check if there is at least one value before window # to know we have enough datapoints hist = self._history[:] window_start = ct - self.window hist_in_window = [v for (t, v) in hist if t >= window_start] stable = all( abs(v - self.target) <= self.precision for v in hist_in_window) if 0 < len(hist_in_window) < len(hist) and stable: # pylint: disable=len-as-condition if hasattr(self, 'doIsAtTarget'): return self.doIsAtTarget(val) return True return False def doTime(self, old, new): if old is None or new is None or old == new: return 0. if 'speed' in self.parameters and self.speed != 0: return abs(new - old) / self.speed + self.window elif 'ramp' in self.parameters and self.ramp != 0: return abs(new - old) / (self.ramp / 60.) + self.window return self.window def doEstimateTime(self, elapsed): if self.status()[0] != status.BUSY: return None if self.target is None: return None if 'setpoint' not in self.parameters or self.setpoint == self.target: # ramp finished, look at history to estimate from last point # outside now = currenttime() for t, v in reversed(list(self._history)): if abs(v - self.target) > self.precision: return max(0, t + self.window - now + 1) if t < now - self.window: break return 0.0 # ramp unfinished, estimate ramp + window return self.doTime(self.read(), self.target) def _targetReached(self): """Do not call `_clearTimeout` as supposed by `HasTimeout` in order
class MicrostepMotor(BaseSequencer, NicosMotor): """Virtual motor which implements software based micro stepping for an attached Moveable device. """ STATUS_TIME = .3 # waitForCompletion 300 ms CODE_TIME = .05 # additional code execution time in polling routine attached_devices = { "motor": Attach("Motor device which will be moved.", Moveable), } parameters = { "microstep": Param("Software/Pseudo micro step size", type=float, settable=True, mandatory=False, default=0.01), "maxtime": Param("Maximum time for one micro step in seconds", type=float, settable=True, mandatory=False, default=0.8), "maxspeed": Param("Maximum speed", type=float, settable=False, mandatory=False), } parameter_overrides = {"abslimits": Override(mandatory=False)} def doInit(self, mode): self._setROParam("maxspeed", self.microstep / self.maxmovetime) self._delay = self.maxtime if self.speed < 1e-8: self.log.warning("Speed has not been set. Set maximum speed %.4f", self.maxspeed) self.speed = self.maxspeed @property def motor(self): return self._attached_motor def _maxmovetime(self, t): # maximum movement time including polling time plus additional code # execution time. return t + MicrostepMotor.STATUS_TIME + MicrostepMotor.CODE_TIME @property def maxmovetime(self): return self._maxmovetime(self.maxtime) def doReadAbslimits(self): return self._attached_motor.abslimits def doWriteAbslimits(self, value): self._attached_motor.abslimits = value def doReadUserlimits(self): return self._attached_motor.userlimits def doWriteUserlimits(self, value): self._attached_motor.userlimits = value def doReadUnit(self): return self._attached_motor.unit def doWriteUnit(self, value): self._attached_motor.unit = value def doWriteSpeed(self, value): delay = self.microstep / value self.log.debug(""" delay: %.4f maxspeed: %.4f maxtime: %.4f""", delay, self.maxspeed, self.maxtime) if value < self.maxspeed or math.fabs(self.maxspeed - value) < 1e-5: self._delay = delay else: raise LimitError( self, "Speed too high. Maximum speed is %.4f" % self.maxspeed) def doWriteMaxtime(self, value): self._setROParam("maxspeed", self.microstep / self._maxmovetime(value)) if self.speed > self.maxspeed: self.log.warning("Speed too high. Set speed to %.4f", self.maxspeed) self.speed = self.maxspeed def doWriteMicrostep(self, value): self._setROParam("maxspeed", value / self.maxmovetime) if self.speed > self.maxspeed: self.log.warning("Speed too high. Set speed to %.4f", self.maxspeed) self.speed = self.maxspeed def doRead(self, maxage=0): return self._attached_motor.read(maxage) def _generateSequence(self, target): pos = self.read(0) s = self.microstep if (target - pos) >= 0 else -self.microstep n = int((target - pos) / s) # handle floating point overflows # check whether or not one last microstep fits into movement to target. if (math.fabs((pos + (n + 1) * s) - target) < math.fabs( self.microstep / 10)): n += 1 res = [(SeqDev(self._attached_motor, pos + i * s), SeqSleep(self._delay)) for i in range(1, n)] res.append((SeqDev(self._attached_motor, target), SeqSleep(self._delay))) return res def _sequence(self, sequence): t = time.time() res = BaseSequencer._sequence(self, sequence) t = time.time() - t self.log.info("Movement finished, time elapsed %.4f.", t) return res def doStop(self): if not self._seq_is_running(): self._stopAction(-1) BaseSequencer.doStop(self) def _stopAction(self, _nr): self._attached_motor.stop()
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 ImageChannel(QMesyDAQImage, BaseImageChannel): parameters = { 'readout': Param('Readout mode of the Detector', settable=True, type=oneof('raw', 'mapped', 'amplitude'), default='mapped', mandatory=False, chatty=True), 'flipaxes': Param('Flip data along these axes after reading from det', type=listof(int), default=[], unit=''), 'transpose': Param('Whether to transpose the image', type=bool, default=False), } # Use the configuration from QMesyDAQ parameter_overrides = { 'listmode': Override(volatile=True), 'histogram': Override(volatile=True), } def doWriteListmode(self, value): self._dev.SetProperties(['writelistmode', ('%s' % value).lower()]) return self.doReadListmode() def doReadListmode(self): return { 'false': False, 'true': True }[self._getProperty('writelistmode')] def doWriteHistogram(self, value): self._dev.SetProperties(['writehistogram', ('%s' % value).lower()]) return self.doReadHistogram() def doReadHistogram(self): return { 'false': False, 'true': True }[self._getProperty('writehistogram')] def doWriteReadout(self, value): self._dev.SetProperties(['histogram', value]) return self._getProperty('histogram') def doWriteListmodefile(self, value): self._dev.SetProperties(['lastlistfile', value]) return self._getProperty('lastlistfile') # def doReadListmodefile(self): # return self._getProperty('lastlistfile') def doWriteHistogramfile(self, value): self._dev.SetProperties(['lasthistfile', value]) return self._getProperty('lasthistfile') # def doReadHistogramfile(self): # return self._getProperty('lasthistfile') def doReadConfigfile(self): return self._getProperty('configfile') def doReadCalibrationfile(self): return self._getProperty('calibrationfile') def doReadArray(self, quality): narray = BaseImageChannel.doReadArray(self, quality) if self.transpose: narray = np.transpose(narray) for axis in self.flipaxes: narray = np.flip(narray, axis) return narray def doFinish(self): self.doStatus(0) return BaseImageChannel.doFinish(self) def doStop(self): self.doStatus(0) return BaseImageChannel.doStop(self)
class DataSink(Device): """The base class for data sinks. Each sink represents one way of processing incoming data. This is a device to be instantiated once per setup so that it can be configured easily through NICOS setups. Actual processing is done by a `DataSinkHandler` class, of which one or more instances are created for each dataset that is processed with this sink. Usually, sinks are specific to a certain type of dataset (e.g. points or scans) and therefore override the `settypes` parameter with a default value that reflects this. .. attribute:: handlerclass This class attribute must be set by subclasses. It selects the subclass of `.DataSinkHandler` that is to be used for handling datasets with this sink. .. attribute:: activeInSimulation This is a class attribute that selects whether this sink can be used in simulation mode. This should only be true for sinks that write no data, such as a "write scan data to the console" sink. .. automethod:: isActive """ parameters = { 'detectors': Param('List of detector names to activate this sink ' '(default is always activated)', type=listof(str)), 'settypes': Param('List of dataset types to activate this sink ' '(default is for all settypes the sink supports)', type=setof(*SETTYPES)), } parameter_overrides = { 'lowlevel': Override(default=True, mandatory=False), } # Set to true in subclasses that are safe for simulation. activeInSimulation = False # Set this to the corresponding Handler class. handlerclass = None def isActive(self, dataset): """Should return True if the sink can and should process this dataset. The default implementation, which should always be called in overrides, checks for simulation mode and for a match with the settypes and the detectors selected by the respective parameters. Derived classes can add additional checks, such as the dataset producing an array that can be written to an image file. """ if session.mode == SIMULATION and not self.activeInSimulation: return False if self.settypes and dataset.settype not in self.settypes: return False if self.detectors and \ not ({d.name for d in dataset.detectors} & set(self.detectors)): return False return True def createHandlers(self, dataset): """Start processing the given dataset (a BaseDataset). Creates the corresponding DataSinkHandler instances to use for this dataset determined via `handlerclass` and returns them. """ if self.handlerclass is None: raise NotImplementedError('Must set an "handlerclass" attribute ' 'on %s' % self.__class__) # pylint: disable=not-callable if dataset.settype == POINT: dets = {d.name for d in dataset.detectors} if self.detectors: dets &= set(self.detectors) return [self.handlerclass(self, dataset, session.getDevice(det)) for det in dets] else: return [self.handlerclass(self, dataset, None)]
class DetAngle(HasLimits, Moveable): """Angle of the outgoing (centered) beam to detector.""" attached_devices = { 'tubeangle': Attach('Angle of the tube to the ground', Moveable), 'tubepos': Attach('Position of detector inside tube', Readable), 'pivot': Attach('Position of the pivot point', Readable), 'theta': Attach('Tilt angle of the sample', Readable, optional=True), } parameters = { 'pivot1pos': Param('Distance of the pivot point 1 from wall', type=floatrange(0), mandatory=False, default=290, unit='mm'), 'b2pos': Param('Distance of B2 aperture from wall', type=floatrange(0), mandatory=False, default=165, unit='mm'), 'b3pos': Param('Distance of B3 aperture from wall', type=floatrange(0), mandatory=False, default=285, unit='mm'), 'samplepos': Param('Distance of sample from B3', type=floatrange(0), mandatory=False, default=50, unit='mm'), 'detheight': Param('Height of the detector', type=floatrange(0), mandatory=False, default=533.715, unit='mm'), # calculated from the beamheight - pivot.height - (256 - 160) * 2.093 mm # Beam height at 0 deg is in pixel 160 counted from top pixel (256) # times pixel size (2.093 mm) 'detoffset': Param('Offset from virtual tube base to lower edge of ' 'detector', type=floatrange(0), mandatory=False, default=619.772, unit='mm'), 'beamheight': Param('Height of beam above ground level', type=floatrange(0), mandatory=False, default=1193.7, unit='mm'), 'range': Param('range of angles between upper and lower detector edge', type=tupleof(float, float), volatile=True, unit='deg, deg', internal=True, preinit=False), } parameter_overrides = { 'unit': Override(mandatory=False, default='deg'), 'abslimits': Override(mandatory=False, volatile=True), } def doPreinit(self, mode): # center of the detector in respect to the tube base line self._detcenter = self.detheight / 2 + self.detoffset self._yoffset = self.beamheight - self._attached_pivot.height def doInit(self, mode): self._func = np.vectorize(self._alpha) def doRead(self, maxage=0): self._update(maxage) beta = self._attached_tubeangle.read(maxage) self.log.debug('Tube angle: %.3f', beta) alphaf = self._alpha(beta) if self._attached_theta: alphaf -= self._attached_theta.read(maxage) return alphaf def _update(self, maxage=0): pivot = self._attached_pivot # Calculate the distance between sample and pivot point self._xoffset = self.pivot1pos + pivot.grid * ( pivot.read(maxage) - 1) - (self.samplepos + self.b3pos) self.log.debug('Sample pivot distance : %.1f', self._xoffset) self._tpos = self._attached_tubepos.read(maxage) def _alpha(self, beta): beta = radians(beta) # calculate the detector center position in respect to sample x = self._xoffset + self._tpos * cos(beta) - self._detcenter * sin( beta) # calculate the height of the detector center in respect to the ground y = self._tpos * sin(beta) + self._detcenter * cos( beta) - self._yoffset return degrees(atan2(y, x)) def doStart(self, target): self._update(0) x = np.arange(self.absmin, self.absmax + 0.01, 0.01) y = self._func(x) if self._attached_theta: target += self._attached_theta.read(0) val = np.interp(target, y, x) self.log.debug('new beta: %f', val) self._attached_tubeangle.start(val) def doReadAbslimits(self): return self._attached_tubeangle.abslimits def doReadRange(self): alpha = self.doRead(0) opening = degrees(atan2(self.detheight, self._tpos)) / 2. return (alpha - opening, alpha + opening)
class ImagePlateDrum(PyTangoDevice, Moveable): """ImagePlateDrum implements erasing, moving to expo position and readout for MAATEL Image Plate Detectors. """ DEFAULT_URL_FMT = "tango://%s/EMBL/Microdiff/General#dbase=no" tango_status_mapping = dict(PyTangoDevice.tango_status_mapping) tango_status_mapping[DevState.STANDBY] = status.OK tango_status_mapping[DevState.ALARM] = status.ERROR POS_ERASE = "erase" POS_EXPO = "expo" POS_READ = "read" valuetype = oneof(POS_ERASE, POS_EXPO, POS_READ) parameters = { "drumpos": Param("Drum position in degree", type=float, settable=True, volatile=True, category="general"), "readheadpos": Param("Read head motor position in mm", type=float, settable=True, volatile=True, category="general"), "drumexpo": Param("Drum expo position in degree", type=float, settable=True, volatile=True, category="general"), "readspeed": Param("Readout velocity for the detector drum " "in rpm", type=float, settable=True, volatile=True, category="general"), "erasespeed": Param("Erase velocity for the detector drum " "in rpm", type=float, settable=True, volatile=True, category="general"), "freqlaser": Param("Frequency for the laser diode in Hz", type=float, settable=True, volatile=True, category="general"), "timeerase": Param("Erasure time in seconds", type=float, settable=True, volatile=True, category="general"), } parameter_overrides = { "unit": Override(default="", mandatory=False, settable=False) } def doInit(self, mode): self._lastStatus = None self._moveTo = None if mode == SIMULATION: self._mapStart = {} self._mapStop = {} return self._mapStart = { ImagePlateDrum.POS_ERASE: self._dev.StartErasureProcess, ImagePlateDrum.POS_EXPO: self._dev.MoveExpoPosition, ImagePlateDrum.POS_READ: self._dev.StartReadProcess, } self._mapStop = { ImagePlateDrum.POS_ERASE: self._dev.AbortErasureProcess, ImagePlateDrum.POS_EXPO: self._dev.AbortExposureProcess, ImagePlateDrum.POS_READ: self._dev.AbortReadProcess, } def doStart(self, pos): self.log.debug("doStart: pos: %s", pos) myStatus = self.status(0) if myStatus[0] == status.OK: self._moveTo = pos self._mapStart[pos]() else: raise MoveError(self, "Movement not allowed during device status " "'%s'" % (status.statuses[myStatus[0]])) def doStop(self): self.log.debug("doStop") if self._moveTo in self._mapStop: self._mapStop[self._moveTo]() else: myStatus = self.status(0) if myStatus[0] == status.OK: self.log.warning("Device already stopped.") else: raise NicosError(self, "Internal moveTo state unknown. " "Check device status.") def doRead(self, maxage=0): return self.target def doStatus(self, maxage=0): # Workaround for status changes from busy to another state although the # operation has _not_ been completed. st, msg = PyTangoDevice.doStatus(self, maxage) if self._lastStatus == status.BUSY and st != status.BUSY: self.log.debug("doStatus: leaving busy state (%d)? %d. " "Check again after a short delay.", status.BUSY, st) session.delay(5) st, msg = PyTangoDevice.doStatus(self, 0) self.log.debug("doStatus: recheck result: %d", st) self._lastStatus = st return st, msg def doFinish(self): self._moveTo = None def doReadDrumpos(self): return self._dev.DrumPosition def doReadReadheadpos(self): return self._dev.ReadHeadPosition def doReadDrumexpo(self): return self._dev.DrumExpoPosition def doReadReadspeed(self): return self._dev.ReadingDrumJogSpeed def doReadErasespeed(self): return self._dev.ErasureDrumJogSpeed def doReadFreqlaser(self): return self._dev.LaserDiodeLevel def doReadTimeerase(self): return self._dev.ErasureDuration def doWriteDrumpos(self, value): self._dev.DrumPosition = value def doWriteReadheadpos(self, value): self._dev.ReadHeadPosition = value def doWriteDrumexpo(self, value): self._dev.DrumExpoPosition = value def doWriteReadspeed(self, value): self._dev.ReadingDrumJogSpeed = value def doWriteErasespeed(self, value): self._dev.ErasureDrumJogSpeed = value def doWriteFreqlaser(self, value): self._dev.LaserDiodeLevel = value def doWriteTimeerase(self, value): self._dev.ErasureDuration = value
class Changer(BaseSequencer): attached_devices = { 'lift': Attach('Lift moving a mono up or down between 4 ' 'positions', Moveable), 'magazine': Attach('Magazine holding the monos and having 4 ' 'positions', Moveable), 'liftclamp': Attach('Clamp holding the mono in the lift', Moveable), 'magazineclamp': Attach('Clamp holding the mono in the magazine', Moveable), 'tableclamp': Attach('Clamp holding the mono on the table', Moveable), 'inhibitrelay': Attach('Inhibit to the remaining motor controllers', Moveable), 'enable': Attach('To enable operation of the changer', Moveable), 'magazineocc': Attach('Readout for occupancy of the magazine', Readable), 'magazinestatus': Attach('Readout for status of magazine load position', Readable), } parameters = { 'mono_on_table': Param('Name of Mono on the Monotable', type=oneof('PG', 'Si', 'Cu', 'Heusler', 'None', 'empty frame'), default='None', settable=True, internal=True), 'mono_in_lift': Param('Which mono is in the lift', type=oneof('PG', 'Si', 'Cu', 'Heusler', 'None', 'empty frame'), default='None', settable=True, internal=True), 'exchangepos': Param( 'dict of device names to positional values ' 'for changing monos', type=dict, settable=False, userparam=False), 'precisionchange': Param( 'dict of device names and pairs of (normal ' 'value, change value) of the precision', type=dict, settable=False, userparam=False), } parameter_overrides = { 'requires': Override(default={'level': 'admin'}), } positions = ['101', '110', '011', '111'] # CounterClockwise monos = ['Heusler', 'PG', 'Si', 'empty frame'] # assigned monos shields = ['110', '110', '110', '110'] # which magzinslot after changing # (e.g. Put a PE dummy to 101 and set this to ('101,'*4).split(',') valuetype = oneof(*(monos + ['None'])) def PrepareChange(self): '''is checking whether all the conditions for a change of mono are met''' # if not(self.SecShutter_is_closed()): # raise UsageError(self, 'Secondary Shutter needs to be closed, ' # 'please close by hand and retry!') # if self.NotAus(): # raise UsageError(self, 'NotAus (Emergency stop) activated, ' # 'please check and retry!') # read all inputs and check for problems if not (self._attached_magazine.read() in self.positions): raise HWError(self, 'Unknown Magazine-Position !') if self._attached_lift.read() != '2': raise HWError(self, 'Lift not at parking position!') if self._attached_liftclamp.read() != 'close': raise HWError(self, 'liftclamp should be closed!') if self._attached_tableclamp.read() != 'close': raise HWError(self, 'tableclamp should be closed!') if self._attached_magazineclamp.read() != 'close': raise HWError(self, 'magazineclamp should be closed!') if self.mono_in_lift != 'None': raise HWError( self, 'There is mono %s in the lift, please change ' 'manually!' % self.mono_in_lift) # XXX TODO: store old values of positions and offsets someplace to # recover after changing back self.log.info('will change position of several devices to the ' 'changing position, moving back is not yet implemented') # enhance precision of the devices for devname, prec in self.precisionchange.items(): dev = session.getDevice(devname) dev.precision = prec[1] self.log.debug('changing precision of the %s device to the %f', devname, prec[1]) # go to the place where a change is possible devs = [] for devname, pos in self.exchangepos.items(): dev = session.getDevice(devname) # remove after implementation of moving back self.log.info('position of the %s device was %f', devname, dev()) if isinstance(dev, HasOffset): dev.start(pos - dev.offset) self.log.debug( 'move and wait %s to the position %f - offset of %f', devname, pos, dev.offset) dev.wait() else: dev.start(pos) self.log.debug('move and wait %s to the position %f', devname, pos) dev.wait() devs.append(dev) multiWait(devs) # put precision back to normal for devname, prec in self.precisionchange.items(): dev = session.getDevice(devname) dev.precision = prec[0] self.log.debug( 'changing precision of the %s device back to ' 'the %f', devname, prec[0]) try: dev = session.getDevice('focibox') self.mono_on_table = dev.read(0) dev.comm('XMA', forcechannel=False) dev.comm('YMA', forcechannel=False) dev.driverenable = False self.log.info('focus motors disabled') except NicosError as err: self.log.error('Problem disabling foci: %s', err) # switch on inhibit and enable self._attached_enable.maw(0xef16) self._attached_inhibitrelay.maw('on') self.log.info('changer enabled and inhibit active') def FinishChange(self): self._attached_inhibitrelay.maw('off') self._attached_enable.maw(0) self.log.warning('Please restart the daemon or reload the setups to ' 'init the new Mono:') self.log.warning(' > NewSetup()') self.log.info(' > NewSetup()') def CheckMagazinSlotEmpty(self, slot): # checks if the given slot in the magazin is empty self.log.info('checking of there IS NOT mono in magazine slot %r' % slot) if self._attached_magazineocc.status(0)[0] != status.OK: raise UsageError( self, 'Magazine occupancy switches are in warning state!') index = self.positions.index(slot) if not ((self._attached_magazineocc.read() >> index * 2) & 1): raise UsageError(self, 'Position %r is already occupied!' % slot) def CheckMagazinSlotUsed(self, slot): # checks if the given slot in the magazin is used (contains a monoframe) self.log.info('checking of there IS mono in magazine slot %r' % slot) if self._attached_magazineocc.status(0)[0] != status.OK: raise UsageError( self, 'Magazine occupancy switches are in warning state!') index = self.positions.index(slot) if (self._attached_magazineocc.read() >> index * 2) & 1: raise UsageError(self, 'Position %r is empty!' % slot) def _start(self, seq): if self._seq_is_running(): raise MoveError(self, 'Can not start sequence, device is still ' 'busy') self._startSequence(seq) # here is the party going on! def Transfer_Mono_Magazin2Lift(self): self._start(self._gen_mag2lift()) self.wait() def _gen_mag2lift(self, magpos=None): seq = [] if magpos is None: magpos = self._attached_magazine.read(0) else: seq.append(SeqDev(self._attached_magazine, magpos)) # check preconditions seq.append( SeqCall(self.log.info, 'checking preconditions for Magazin2Lift')) seq.append(SeqCheckStatus(self._attached_magazine, status.OK)) seq.append(SeqCheckStatus(self._attached_lift, status.OK)) seq.append(SeqCheckPosition(self._attached_lift, '2')) seq.append(SeqCheckPosition(self._attached_liftclamp, 'close')) seq.append(SeqCheckPosition(self._attached_magazine, magpos)) seq.append(SeqCheckPosition(self._attached_magazineclamp, 'close')) seq.append(SeqMethod(self, 'CheckMagazinSlotUsed', magpos)) seq.append(SeqCheckAttr(self, 'mono_in_lift', 'None')) # transfer mono to lift seq.append( SeqCall(self.log.info, 'transferring mono from magazine to lift')) seq.append(SeqDev(self._attached_liftclamp, 'open')) seq.append(SeqDev(self._attached_lift, '3')) # almost top position seq.append(SeqMethod(self._attached_liftclamp, 'start', 'close')) seq.append(SeqDev(self._attached_lift, '4')) # top position seq.append(SeqDev(self._attached_liftclamp, 'close')) seq.append(SeqMethod(self._attached_magazineclamp, 'start', 'open')) # rattle a little seq.append(SeqCall(self.log.info, 'rattle to release magazine grab')) seq.append(SeqDev(self._attached_lift, '3')) # almost top position seq.append(SeqDev(self._attached_lift, '4')) # top position seq.append(SeqDev(self._attached_lift, '3')) # almost top position seq.append(SeqDev(self._attached_lift, '4')) # top position seq.append(SeqDev(self._attached_magazineclamp, 'open')) seq.append(SeqMethod(self, 'CheckMagazinSlotEmpty', magpos)) # move (with mono) to parking position seq.append( SeqCall(self.log.info, 'moving with mono to parking position')) seq.append( SeqSetAttr(self, 'mono_in_lift', self.monos[self.positions.index(magpos)])) seq.append(SeqDev(self._attached_lift, '2')) # park position seq.append(SeqDev(self._attached_magazineclamp, 'close')) # Magazin should not contain a mono now seq.append(SeqMethod(self, 'CheckMagazinSlotEmpty', magpos)) return seq def Transfer_Mono_Lift2Magazin(self): self._start(self._gen_lift2mag()) self.wait() def _gen_lift2mag(self, magpos=None): seq = [] if magpos is None: magpos = self._attached_magazine.read(0) else: seq.append(SeqDev(self._attached_magazine, magpos)) # check preconditions seq.append( SeqCall(self.log.info, 'checking preconditions for Lift2Magazin')) seq.append(SeqCheckStatus(self._attached_magazine, status.OK)) seq.append(SeqCheckStatus(self._attached_lift, status.OK)) seq.append(SeqCheckPosition(self._attached_lift, '2')) seq.append(SeqCheckPosition(self._attached_liftclamp, 'close')) seq.append(SeqCheckPosition(self._attached_magazine, magpos)) seq.append(SeqCheckPosition(self._attached_magazineclamp, 'close')) seq.append(SeqMethod(self, 'CheckMagazinSlotEmpty', magpos)) # there needs to be a mono in the lift seq.append( SeqCall(self.log.info, 'checking if there is a mono in lift')) seq.append( SeqCheckAttr(self, 'mono_in_lift', values=[m for m in self.monos if m != 'None'])) # prepare magazin seq.append(SeqCall(self.log.info, 'testing magazin grab')) seq.append(SeqDev(self._attached_magazineclamp, 'open')) seq.append(SeqDev(self._attached_magazineclamp, 'close')) seq.append(SeqDev(self._attached_magazineclamp, 'open')) seq.append(SeqDev(self._attached_magazineclamp, 'close')) seq.append(SeqDev(self._attached_magazineclamp, 'open')) # transfer mono to lift seq.append(SeqCall(self.log.info, 'moving lift to top position')) seq.append(SeqDev(self._attached_lift, '4')) # top position seq.append( SeqCall(self.log.info, 'closing the magazin grab and rattling lift')) seq.append(SeqMethod(self._attached_magazineclamp, 'start', 'close')) # rattle a little seq.append(SeqDev(self._attached_lift, '3')) # almost top position seq.append(SeqDev(self._attached_lift, '4')) # top position seq.append(SeqDev(self._attached_lift, '3')) # almost top position seq.append(SeqDev(self._attached_lift, '4')) # top position seq.append(SeqDev(self._attached_lift, '3')) # almost top position seq.append(SeqDev(self._attached_magazineclamp, 'close')) seq.append(SeqMethod(self, 'CheckMagazinSlotUsed', magpos)) seq.append(SeqCall(self.log.info, 'opening lift clamp')) seq.append(SeqMethod(self._attached_liftclamp, 'start', 'open')) seq.append(SeqDev(self._attached_lift, '4')) # top position seq.append(SeqDev(self._attached_lift, '3')) # top position seq.append(SeqDev(self._attached_liftclamp, 'open')) seq.append(SeqCall(self.log.info, 'moving lift to park position')) seq.append(SeqDev(self._attached_lift, '2')) # park position seq.append(SeqCall(self.log.info, 'closing lift clamp')) seq.append(SeqDev(self._attached_liftclamp, 'close')) # move (without mono) to parking position seq.append(SeqSetAttr(self, 'mono_in_lift', 'None')) # Magazin should not contain a mono now seq.append(SeqMethod(self, 'CheckMagazinSlotUsed', magpos)) return seq def Transfer_Mono_Lift2Table(self): self._start(self._gen_lift2table()) self.wait() def _gen_lift2table(self): seq = [] # check preconditions seq.append( SeqCall(self.log.info, 'checking preconditions for Lift2Table')) seq.append(SeqCheckStatus(self._attached_tableclamp, status.OK)) seq.append(SeqCheckStatus(self._attached_liftclamp, status.OK)) seq.append(SeqCheckStatus(self._attached_lift, status.OK)) seq.append(SeqCheckPosition(self._attached_lift, '2')) seq.append(SeqCheckPosition(self._attached_liftclamp, 'close')) seq.append(SeqCheckPosition(self._attached_tableclamp, 'close')) # there shall be a mono in the lift! seq.append(SeqCall(self.log.info, 'check if there is a mono in Lift')) seq.append( SeqCheckAttr(self, 'mono_in_lift', values=[m for m in self.monos if m != 'None'])) seq.append(SeqCheckAttr(self, 'mono_on_table', 'None')) seq.append(SeqCall(self.log.info, 'moving down the lift')) # transfer mono to table seq.append(SeqDev(self._attached_tableclamp, 'open')) seq.append(SeqDev(self._attached_lift, '1')) # bottom position seq.append( SeqCall(self.log.info, 'closing table grab and releasing lift clamp')) seq.append(SeqDev(self._attached_tableclamp, 'close')) # move (without mono) to parking position seq.append(SeqDev(self._attached_liftclamp, 'open')) def func(self): self.mono_on_table, self.mono_in_lift = self.mono_in_lift, 'None' seq.append(SeqCall(func, self)) # seq.append(SeqSetAttr(self, 'mono_on_table', self.mono_in_lift)) # seq.append(SeqSetAttr(self, 'mono_in_lift', 'None')) seq.append(SeqCall(self.log.info, 'moving lift to park position')) seq.append(SeqDev(self._attached_lift, '2')) # park position seq.append(SeqDev(self._attached_liftclamp, 'close')) # TODO: change foci alias and reactivate foci return seq def Transfer_Mono_Table2Lift(self): self._start(self._gen_table2lift()) self.wait() def _gen_table2lift(self): # XXX TODO drive all foci to 0 and switch of motors.... # XXX TODO move mty/mtx to monospecific abholposition # hier nur das reine abholen vom Monotisch seq = [] # check preconditions seq.append( SeqCall(self.log.info, 'checking preconditions for Table2Lift')) seq.append(SeqCheckStatus(self._attached_tableclamp, status.OK)) seq.append(SeqCheckStatus(self._attached_liftclamp, status.OK)) seq.append(SeqCheckStatus(self._attached_lift, status.OK)) seq.append(SeqCheckPosition(self._attached_lift, '2')) seq.append(SeqCheckPosition(self._attached_liftclamp, 'close')) seq.append(SeqCheckPosition(self._attached_tableclamp, 'close')) # there shall be no mono in the lift, but one on the table seq.append( SeqCall(self.log.info, 'checking if there IS NOT a mono in Lift')) seq.append(SeqCheckAttr(self, 'mono_in_lift', 'None')) seq.append( SeqCheckAttr(self, 'mono_on_table', values=[m for m in self.monos if m != 'None'])) # transfer mono to lift seq.append(SeqCall(self.log.info, 'moving down the lift')) seq.append(SeqDev(self._attached_liftclamp, 'open')) seq.append(SeqDev(self._attached_lift, '1')) # bottom position seq.append(SeqCall(self.log.info, 'grabbing the monochromator')) seq.append(SeqDev(self._attached_liftclamp, 'close')) seq.append(SeqDev(self._attached_tableclamp, 'open')) # move (with mono) to parking position def func(self): self.mono_on_table, self.mono_in_lift = 'None', self.mono_on_table seq.append(SeqCall(func, self)) # seq.append(SeqSetAttr(self, 'mono_on_table', 'None')) # seq.append(SeqSetAttr(self, 'mono_in_lift', self.mono_on_table)) seq.append( SeqCall(self.log.info, 'moving the lift with mono to parking position')) seq.append(SeqDev(self._attached_lift, '2')) # park position seq.append(SeqDev(self._attached_tableclamp, 'close')) return seq def doStart(self, target): self.change(self.mono_on_table, target) def change(self, old, whereto): ''' cool kurze Wechselroutine Der Knackpunkt ist in den Hilfsroutinen!''' if not (old in self.monos + ['None']): raise UsageError( self, '\'%s\' is illegal value for Mono, use one ' 'of ' % old + ', '.join(self.monos + ['None'])) if not (whereto in self.monos + ['None']): raise UsageError( self, '\'%s\' is illegal value for Mono, use one ' 'of ' % whereto + ', '.join(self.monos + ['None'])) self.PrepareChange() if self.monos.index(whereto) == self.monos.index(old): # Nothing to do, requested Mono is supposed to be on the table return # Ok, we have a good state, the only thing we do not know is which mono # is on the table...... # for this we use the (cached) parameter mono_on_table if self.mono_on_table != old: raise UsageError( self, 'Mono %s is not supposed to be on the ' 'table, %s is!' % (old, self.mono_on_table)) seq = [] # 0) move magazine to mono position magpos_to_put = self.positions[self.monos.index(old)] seq.append(SeqMethod(self, 'CheckMagazinSlotEmpty', magpos_to_put)) seq.append(SeqDev(self._attached_magazine, magpos_to_put)) # 1) move away the old mono, if any if old != 'None': seq.extend(self._gen_table2lift()) seq.extend( self._gen_lift2mag(self.positions[self.monos.index(old)])) # 2) fetch the new mono (if any) from the magazin if whereto != 'None': seq.extend( self._gen_mag2lift(self.positions[self.monos.index(whereto)])) seq.extend(self._gen_lift2table()) seq.append( SeqDev(self._attached_magazine, self.shields[self.monos.index(whereto)])) # seq.append(SeqDev(self._attached_enable, 0)) - will be done in FinishChange seq.append(SeqMethod(self, 'FinishChange')) self._start(seq) self.wait() @usermethod def printstatusinfo(self): self.log.info( 'PLC is %s', 'enabled' if self._attached_enable.read() == 0xef16 else 'disabled, you need to set enable_word to the ' 'correct value') self.log.info('Inhibit_relay is %s', self._attached_inhibitrelay.read()) liftposnames = { '0': 'unknown, error in switches', '1': 'Monotable loading', '2': 'Park position', '3': 'Bottom storage', '4': 'Upper storage' } self.log.info('lift is at %s', liftposnames[self._attached_lift.read()]) try: magpos = self._attached_magazine.read() self.log.info('magazine is at %r which is assigned to %s', magpos, self.monos[self.positions.index(magpos)]) except Exception: self.log.error('magazine is at an unknown position !!!') for n in 'liftclamp magazineclamp tableclamp'.split(): self.log.info('%s is %s', n, self._adevs[n].read()) occ = self._attached_magazineocc.read(0) for i in range(4): self.log.info( 'magazine slot %r is %sempty and its readouts are %sbroken', self.positions[i], '' if (occ >> (i * 2)) & 1 else 'not ', '' if (occ >> (i * 2 + 1)) & 1 else 'not ') if self._attached_magazinestatus.read() == 'free': self.log.info('magazine is currently free to load monochromator') else: self.log.info('magazine is currently occupied with monochromator ' 'and cannot load another') def doRead(self, maxage=0): return ''