class Resolution(Readable): valuetype = (float, float) attached_devices = { 'detector': Attach('Detector device with size information', Detector), 'beamstop': Attach('Beam stop device with size information', BeamStop), 'detpos': Attach('Detector position device', Readable), 'wavelength': Attach('Wavelength device', Readable), } parameter_overrides = { 'unit': Override(mandatory=False, volatile=True), 'fmtstr': Override(default='%3.f - %3.f'), } def doRead(self, maxage=0): bs = self._attached_beamstop return qrange(self._attached_wavelength.read(maxage), self._attached_detpos.read(0), bs.slots[bs.shape][1][0] / 2., self._attached_detector.size[0] / 2.) def doReadUnit(self): unit = self._attached_wavelength.unit return '%s-1' % unit
class TubeAngle(HasLimits, Moveable): """Angle of the tube controlled by the yoke.""" attached_devices = { 'yoke': Attach('Yoke device', Moveable), } parameters = { 'yokepos': Param('Position of yoke from pivot point', type=floatrange(0, 20000), unit='mm', default=11000), } parameter_overrides = { 'abslimits': Override(mandatory=False, volatile=True), 'unit': Override(mandatory=False, default='deg'), } def doRead(self, maxage=0): return degrees(atan2(self._attached_yoke.read(maxage), self.yokepos)) def doStart(self, target): self._attached_yoke.move(tan(radians(target)) * self.yokepos) def doReadAbslimits(self): yokelimits = self._attached_yoke.userlimits return (degrees(atan2(yokelimits[0], self.yokepos)), degrees(atan2(yokelimits[1], self.yokepos)))
class ChopperDiscTranslation(ChopperDiscTranslationBase, VirtualMotor): """Position of chopper disc along the x axis. Since the chopper disc 2 can be translated, the chopper speed must be low enough (around 0, defined by its precision). The change of speed must be blocked if the translation device is not at a defined position. """ parameter_overrides = { 'speed': Override(default=0.1), 'fmtstr': Override(default='%.0f'), } def doRead(self, maxage=0): try: val = VirtualMotor.doRead(self, maxage) if not self.isAtTarget(val): # This is because of cutting to int values. If the target is # lower than val the cut would give the wrong result. if self.target - val < 0: val += 1. return self.valuetype(val) except ValueError: return self.target
class MasterSlaveMotor(Moveable): """Combined master slave motor with possibility to apply a scale to the slave motor.""" attached_devices = { "master": Attach("Master motor controlling the movement", Moveable), "slave": Attach("Slave motor following master motor movement", Moveable), } parameters = { "scale": Param("Factor applied to master target position as slave " "position", type=float, default=1), } parameter_overrides = { "unit": Override(mandatory=False), "fmtstr": Override(default="%.3f %.3f"), } def _slavePos(self, pos): return self.scale * pos def doRead(self, maxage=0): return [ self._attached_master.read(maxage), self._attached_slave.read(maxage) ] def doStart(self, pos): self._attached_master.move(pos) self._attached_slave.move(self._slavePos(pos)) def doIsAllowed(self, pos): faultmsgs = [] messages = [] for dev in [self._attached_master, self._attached_slave]: allowed, msg = dev.isAllowed(pos) msg = dev.name + ': ' + msg messages += [msg] if not allowed: faultmsgs += [msg] if faultmsgs: return False, ', '.join(faultmsgs) return True, ', '.join(messages) def doReadUnit(self): return self._attached_master.unit def valueInfo(self): return Value(self._attached_master.name, unit=self.unit, fmtstr=self._attached_master.fmtstr), \ Value(self._attached_slave.name, unit=self.unit, fmtstr=self._attached_slave.fmtstr)
class ListmodeSink(FileSink): """Writer for the list mode files via QMesyDAQ itself.""" attached_devices = { 'image': Attach('Image device to set the file name', Image), } parameter_overrides = { 'settypes': Override(default=[POINT]), 'filenametemplate': Override(mandatory=False, userparam=False, default=['D%(pointcounter)07d.mdat']), } handlerclass = ListmodeSinkHandler
class Deflector(Axis): """Special axis to store additional parameters for the deflectors.""" parameters = { 'reflectivity': Param('Reflectivity of the material', type=floatrange(0, 1), settable=False, userparam=False, default=0.9), 'length': Param('Length of the deflector blades', type=floatrange(0, None), settable=False, prefercache=False, default=40., unit='mm'), 'thickness': Param('Thickness of the wafer', type=floatrange(0, None), settable=False, userparam=False, default=0.55, unit='mm'), } parameter_overrides = { 'unit': Override(mandatory=False, default='deg'), }
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 Experiment(BaseExperiment): """TOFTOF experiment. The service experiment will be filled up with instrument specific data. """ parameter_overrides = { 'envlist': Override(category='general'), } def _newPropertiesHook(self, proposal, kwds): if self.proptype == 'service': upd = { 'title': 'Maintenance', 'users': [{ 'name': session.instrument.responsible }], 'localcontacts': [{ 'name': session.instrument.responsible }], 'default_sample': 'Unknown', } kwds.update(upd) return kwds return BaseExperiment._newPropertiesHook(self, proposal, kwds)
class NOKPosition(PolynomFit, Coder): """Device to read the current Position of a NOK. The Position is determined by a ratiometric measurement between two analogue voltages measured with i7000 modules via taco. As safety measure, the reference voltage obtained is checked to be in some configurable limits. """ attached_devices = { 'measure': Attach('Sensing Device (Poti)', Readable), 'reference': Attach('Reference Device', Readable), } parameters = { 'length': Param('Length... ????', type=float, mandatory=False), # fun stuff, not really needed.... 'serial': Param('Serial number', type=int, mandatory=False), } parameter_overrides = { 'fmtstr': Override(default='%.3f'), 'unit': Override(default='mm', mandatory=False), } def doReset(self): multiReset(self._adevs) def doRead(self, maxage=0): """Read basically two (scaled) voltages. - value from the poti (times a sign correction for top mounted potis) - ref from a reference voltage, scaled by 2 (Why???) Then it calculates the ratio poti / ref. Then this is put into a correcting polynom of at least first order Result is then offset + mul * <previously calculated value> """ poti = self._attached_measure.read(maxage) ref = self._attached_reference.read(maxage) self.log.debug('Poti vs. Reference value: %f / %f', poti, ref) # apply simple scaling return self._fit(poti / ref)
class ChopperDisc2(ChopperDisc2Base, ChopperDisc): """Chopper disc 2 device.""" parameter_overrides = { 'pos': Override(type=intrange(0, 5), volatile=True, settable=True, fmtstr='%d', unit=''), }
class NOKMotorIPC(CanReference, IPCMotor): """Basically a IPCMotor with referencing.""" parameters = { 'refpos': Param('Reference position in phys. units', unit='main', type=none_or(float), mandatory=True), } parameter_overrides = { 'zerosteps': Override(default=500000, mandatory=False), 'unit': Override(default='mm', mandatory=False), 'backlash': Override(type=floatrange(0.0, 0.0)), # only 0 is allowed! 'speed': Override(default=10), 'accel': Override(default=10), 'slope': Override(default=2000), 'confbyte': Override(default=48), 'divider': Override(type=intrange(-1, 7)), } def doInit(self, mode): if mode != SIMULATION: self._attached_bus.ping(self.addr) if self._hwtype == 'single': if self.confbyte != self.doReadConfbyte(): self.doWriteConfbyte(self.confbyte) self.log.warning( 'Confbyte mismatch between setup and card' ', overriding card value to 0x%02x', self.confbyte) # make sure that the card has the right "last steps" # This should not applied at REFSANS, since it disturbs the running # TACO server settings # if self.steps != self.doReadSteps(): # self.doWriteSteps(self.steps) # self.log.warning('Resetting stepper position to last known ' # 'good value %d', self.steps) self._type = 'stepper motor, ' + self._hwtype else: self._type = 'simulated stepper' def doReference(self): bus = self._attached_bus bus.send(self.addr, 34) # always go forward (positive) bus.send(self.addr, 47, self.speed, 3) # reference with normal speed # may need to sleep a little here.... session.delay(0.1) self.wait() self.doSetPosition(self.refpos)
class TIFFLaueSink(ImageSink): parameter_overrides = { 'filenametemplate': Override(mandatory=False, settable=False, userparam=False, default=['%(proposal)s_%(pointcounter)07d.tif']), } handlerclass = TiffLaueImageSinkHandler def isActiveForArray(self, arraydesc): return len(arraydesc.shape) == 2
class NOKMonitoredVoltage(Sensor): """Return a scaled and monitored Analogue value. Also checks the value to be within certain limits, if not, complain. """ parameters = { 'reflimits': Param('None or Limits to check the reference: low, warn, ' 'high', type=none_or(tupleof(float, float, float)), settable=False, default=None), 'scale': Param('Scaling factor', type=float, settable=False, default=1.), } parameter_overrides = { 'unit': Override(default='V', mandatory=False), } def doInit(self, mode): if self.reflimits is not None: if not (0 <= self.reflimits[0] <= self.reflimits[1] <= self.reflimits[2]): raise ConfigurationError( self, 'reflimits must be in ascending' ' order!') def doRead(self, maxage=0): value = self.scale * Sensor.doRead(self, maxage) if self.reflimits is not None: if abs(value) > self.reflimits[2]: raise HardwareError( self, 'Reference voltage (%.2f) above ' 'maximum (%.2f)' % (value, self.reflimits[2])) if abs(value) < self.reflimits[0]: raise HardwareError( self, 'Reference voltage (%.2f) below ' 'minimum (%.2f)' % (value, self.reflimits[0])) if abs(value) < self.reflimits[1]: self.log.warning( 'Reference voltage (%.2f) seems rather low, ' 'should be above %.2f', value, self.reflimits[1]) return value def doStatus(self, maxage=0): try: self.doRead(maxage) return Sensor.doStatus(self, maxage) except HardwareError as err: return status.ERROR, repr(err)
class ChopperDisc(ChopperDiscBase, VirtualMotor): parameters = { 'crc': Param('Counter-rotating mode', type=int, default=1, settable=True), 'slittype': Param('Slit type', type=int, default=1, settable=True), } parameter_overrides = { 'jitter': Override(default=2), 'curvalue': Override(default=1200), 'speed': Override(default=50), } def doReadCurrent(self): speed = self.read() if abs(speed) > self.jitter: return 2 + speed * 0.001 return 0 def doPoll(self, n, maxage): self._pollParam('current')
class DebugDataSink(DataSink): """Debug data sink. This device only displays some data handling specific information during the counts and scans. It does not persist any data to files. .. note:: It should be used only for debugging purposes. """ handlerclass = DebugDataSinkHandler parameter_overrides = { 'settypes': Override(default=[POINT, SCAN, SUBSCAN, BLOCK]), }
class DigitalOutput(Pyro4Device, HasTimeout, Moveable): """ A devices that can set and read a digital value corresponding to a bitfield. """ valuetype = int parameter_overrides = { 'unit': Override(mandatory=False), 'timeout': Override(default=1.), } def doRead(self, maxage=0): with globalLock, self._dev: return self._dev.afp_state_get() def doStart(self, value): if self.read(0) != value: with globalLock, self._dev: self._dev.afp_flip_do() def doStatus(self, maxage=0): return status.OK, ""
class NexusSink(FileSink): """This is a sink for writing NeXus HDF5 files from a template. The template is a dictionary representing the structure of the NeXus file. Special elements in this dictionary are responsible for writing the various NeXus elements. The actual writing work is done in the NexusSinkHandler. This class just initializes the handler properly. """ parameters = { 'templateclass': Param('Python class implementing ' 'NexusTemplateProvider', type=str, mandatory=True), } parameter_overrides = { 'settypes': Override(type=setof(*(POINT,))), } handlerclass = NexusSinkHandler _handlerObj = None def doInit(self, mode): if mode == SLAVE: return # The default implementation creates gazillions of SinkHandlers. # For NeXus we do not want this, we want one keeping track of the whole # process. However, we want a handler object per file. def createHandlers(self, dataset): if self._handlerObj is None: if len(dataset.detectors) == 1: self._handlerObj = self.handlerclass(self, dataset, dataset.detectors[0]) else: self._handlerObj = self.handlerclass(self, dataset, None) else: self._handlerObj.dataset = dataset return [self._handlerObj] def end(self): self._handlerObj = None def loadTemplate(self): mod, cls = self.templateclass.rsplit('.', 1) mod = importlib.import_module(mod) class_ = getattr(mod, cls) return class_().getTemplate()
class FocusPoint(HasLimits, Moveable): attached_devices = { 'table': Attach('table', Moveable), 'pivot': Attach('pivot', Readable), } parameters = { 'gisansdistance': Param('GISANS distance', type=floatrange(0, None), default=10700), } parameter_overrides = { 'abslimits': Override(mandatory=True, volatile=False), } def moveToFocus(self): self._attached_table.move(self._calculation()) def doRead(self, maxage=0): return self._attached_table.read(maxage) def doStart(self, pos): # self.moveToFocus() or move table self._attached_table.move(pos) def _calculation(self, pivot=None): if pivot is None: pivot = self._attached_pivot.read(0) focus = self.gisansdistance - pivot * self._attached_pivot.grid self.log.debug('FocusPoint _calculation focus %f pivot %d', focus, pivot) return focus def doStatus(self, maxage=0): state = self._attached_table.status(maxage) if state[0] != status.OK: return state table = self._attached_table.read() focus = self._calculation() precision = 0 if hasattr(self._attached_table, '_attached_device'): precision = self._attached_table._attached_device.precision elif hasattr(self._attached_table, 'precision'): precision = self._attached_table.precision else: precision = 0 text = 'focus' if abs(table - focus) <= precision else state[1] return status.OK, text
class Collimator(Axis): """Polarization collimator device.""" parameters = { 'divergency': Param('Divergency of the collimator', type=floatrange(0, None), settable=False, userparam=False, default=0.5, unit='deg'), } parameter_overrides = { 'unit': Override(mandatory=False, default='deg'), }
class RobotMotor(Motor): """CARESS motor using the CARESS robot devices. CARESS uses a special device to control the speed. """ attached_devices = { 'speedmotor': Attach('Device to control the axis speed', Motor, multiple=False), } parameter_overrides = { 'speed': Override(volatile=True), } def doReadSpeed(self): return self._attached_speedmotor.read() def doWriteSpeed(self, value): self._attached_speedmotor.maw(value)
class GeometricBlur(Readable): """Special device to calculate geometric blur. Calculated from collimation and sample to detector distance.""" attached_devices = { 'l': Attach('Distance device', Readable), 'd': Attach('Pinhole', Readable), 'sdd': Attach('Sample Detector Distance', Readable), } parameter_overrides = { 'unit': Override(volatile=True, mandatory=False), } def doInit(self, mode): units = set(d.unit for d in self._adevs.values()) if len(units) != 1: raise ConfigurationError( self, 'different units for L, d and sdd ' '(%s vs %s vs %s)' % (self._attached_l.unit, self._attached_d.unit, self._attached_sdd.unit)) if 'mm' not in units: raise ConfigurationError( self, "attached devices units have to be " "'mm'") def doRead(self, maxage=0): try: ret = float(self._attached_sdd.read(maxage)) * \ float(self._attached_d.read(maxage)) / \ float(self._attached_l.read(maxage)) return 1000 * ret # convert to um except ValueError: ret = 0 return ret def doReadUnit(self): return 'um'
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 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 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 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 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 SANSNexusSink(NexusSink): parameter_overrides = { 'settypes': Override(settable=True), }
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) ]