class ImageKafkaDataSink(ProducesKafkaMessages, DataSink): """ Data sink which writes images to Kafka after serializing them. The parameter *channeltostream* provides a dict of all the image channels from which the data is to be be forwarded mapped to a tuple of (kafka topic, message source name) """ parameters = { 'maximagesize': Param('Expected max array size of the image', type=int, default=5e7), 'channeltostream': Param( 'Dict of image channel name(to be forwarded) -> (topic, source)', type=dictof(str, tupleof(str, str)), mandatory=True), } parameter_overrides = { 'settypes': Override(default=[POINT]), } handlerclass = ImageKafkaDataSinkHandler serializer = HistogramFlatbuffersSerializer() def doInit(self, mode): # Increase the maximum message size that the producer can send self._setProducerConfig(max_request_size=self.maximagesize)
class ReshapeHistogramImageChannel(HistogramImageChannel): """ An image channel which can reshape the data. This is sometimes necessary. """ parameters = { 'dimensions': Param( 'Desired shape of the data', type=dictof(str, int), ), } _shape = None _HM_dim_desc = None def doInit(self, mode): self._shape = tuple(self.dimensions.values()) res = [] for name, dim in self.dimensions.items(): res.append(HistogramDimDesc(dim, name, '')) self._HM_dim_desc = res def doReadArray(self, quality): data = HistogramImageChannel.doReadArray(self, quality) if len(data) >= numpy.prod(self.shape): return data.reshape(self.shape, order='C') return data @property def shape(self): return self._shape def _dimDesc(self): return self._HM_dim_desc
class CaressScanfileSink(FileSink): """A data sink that writes the CARESS file format.""" handlerclass = CaressScanfileSinkHandler parameters = { # Since some devices gives strings back as read values, but the CARESS # format does not accept strings they must be mapped to numbers 'mapping': Param('Mapping of (string) device values to float values', unit='', settable=False, type=dictof(str, float), default={ 'on': 1, 'off': 0, 'open': 1, 'closed': 0 }), } parameter_overrides = { 'settypes': Override(default=[SCAN, SUBSCAN]), # 'settypes': Override(default=[SCAN, SUBSCAN, POINT]), 'filenametemplate': Override( default=['m2%(pointcounter)08d.dat', 'm2%(scancounter)08d.dat']), }
class DigitalValue(Readable): attached_devices = { 'iodev': Attach('IO Device', VSDIO), } parameters = { 'channel': Param('Channel for readout', type=oneof(*VSDIO._HW_DigitalChannels), settable=True, preinit=True), 'mapping': Param('Mapping of 0/1 to sensible strings', type=dictof(str, oneof(0, 1)), mandatory=True), } parameter_overrides = { 'unit': Override(mandatory=False, settable=False, default=''), } def doInit(self, mode): self._revmapping = {v: k for k, v in self.mapping.items()} def doRead(self, maxage=0): ofs, bit = self._attached_iodev._HW_DigitalChannels[self.channel] # ofs is in Bytes, we need it in words! => /2 if self._attached_iodev._readU16(ofs // 2) & (1 << bit): return self._revmapping[1] return self._revmapping[0] def doStatus(self, maxage=0): return status.OK, ''
class NamedDigitalOutput(DigitalOutput): """ A DigitalOutput with numeric values mapped to names. """ parameters = { 'mapping': Param('A dictionary mapping state names to integer values', type=dictof(str, int), mandatory=True), } def doInit(self, mode): self._reverse = {v: k for (k, v) in self.mapping.items()} # oneofdict: allows both types of values (string/int), but normalizes # them into the string form self.valuetype = oneofdict(self._reverse) def doStart(self, target): value = self.mapping.get(target, target) self._dev.value = value def doRead(self, maxage=0): value = self._dev.value return self._reverse.get(value, value)
class MantidDevice(Readable): parameters = { 'args': Param('Additional arguments to MoveInstrumentComponent.', type=dictof(str, anytype)), 'algorithm': Param('Mantid algorithm name.', type=str, settable=False, userparam=False, mandatory=True), } parameter_overrides = {'unit': Override(mandatory=False)} valuetype = dictof(str, anytype)
class MirrorSample(BaseMirrorSample): _misalignments = {} parameters = { 'alignerrors': Param( 'Errors to simulate the sample misalignment', type=dictof(str, floatrange(0)), settable=False, userparam=False, default={ 'sample_y': 2, 'omega': 0.1, 'detarm': 0.2, 'chi': 1, }, ), } def doInit(self, mode): if mode == MASTER: self._misalign() def _applyParams(self, number, parameters): BaseMirrorSample._applyParams(self, number, parameters) self._misalign() def _misalign(self): for n, err in self.alignerrors.items(): self._misalignments[n] = random.uniform(-err, err)
class NamedDigitalInput(DigitalInput): """ A DigitalInput with numeric values mapped to names. """ parameters = { 'mapping': Param( 'A dictionary mapping state names to integers - ' 'if not given, read the mapping from the Tango ' 'device if possible', type=dictof(str, int), mandatory=False), } def doInit(self, mode): if self.mapping: self._reverse = {v: k for (k, v) in self.mapping.items()} return try: self._reverse = parse_mapping(self._getProperty('mapping'))[0] except Exception: self.log.warning('could not parse value mapping from Tango', exc=1) self._reverse = {} def doRead(self, maxage=0): value = self._dev.value return self._reverse.get(value, value)
class SelectorSwitcher(KWS1SelectorSwitcher): """Switcher whose mapping is determined by a list of presets.""" parameter_overrides = { 'presets': Override(type=dictof( str, dictwith(lam=float, speed=float, spread=float, tilted=bool))), }
class Authenticator(BaseAuthenticator): """Authenticates against the GhOST REST API. The resulting user object carries around the GhOST wrapper object as metadata, so that further queries can be made with it while the user is logged in. """ parameters = { 'ghosthost': Param('Host of the GhOST system to authenticate against', type=nonemptystring, mandatory=True), 'instrument': Param('Name of the instrument in GhOST', type=str), 'checkexp': Param( 'If true, check that users are either affiliated ' 'with the current experiment, or registered ' 'local contacts for the instrument', type=bool, default=True), 'aliases': Param( 'Map of short user names to GhOST email addresses ' 'and their desired user level', type=dictof(nonemptystring, UserLevelAuthEntry)), } def authenticate(self, username, password): if username in self.aliases: email, level = self.aliases[username] else: email, level = username, USER ghost = GhostWrapper('https://' + self.ghosthost, self.log) try: realname = ghost.login(self.instrument, email, password, strict=self.checkexp) except ghostapi.errors.GhostAccessError: self.log.exception('during authentication') raise AuthenticationError('unknown user or wrong password') \ from None except AuthenticationError: self.log.exception('during authentication') raise except Exception as err: self.log.exception('during authentication') raise AuthenticationError('exception during authenticate(): %s' % err) from None if email == username: # try to extract the user's real name from the user data username = realname return User(username, level, { 'ghost': ghost, 'keepalive': ghost.keepalive })
class DetectorPosSwitcherMixin(DeviceMixinBase): parameters = { 'offsets': Param('Offsets to correct TOF chopper-detector length ' 'for the errors in the det_z axis value', type=dictof(float, float), mandatory=True), 'mapkey': Param('Last selector position for mapping', type=str, settable=True, internal=True), }
class AmorNexusUpdater(NexusFileWriterStatus): """Updates the nexus files once they are written.""" parameters = { 'fileupdates': Param('Dict of hdf path mapped to tupleof: ' '(dev, dtype, unit)', type=dictof(str, tuple)), 'binpaths': Param('Paths in file where bin information is stored', type=listof(str)) } def _on_close(self, jobid, dataset): NexusFileWriterStatus._on_close(self, jobid, dataset) filename = dataset.filepaths[0] try: nxfile = h5py.File(filename, 'r+') except (ValueError, OSError): self.log.error('Unable to edit file for dataset #%d!', dataset.counter) return # Remove the last bins for path in self.binpaths: if path in nxfile: arr = numpy.delete(nxfile[path].value, -1) axis = nxfile[path].attrs['axis'] unit = nxfile[path].attrs['units'] del nxfile[path] ds = nxfile.create_dataset(path, arr.shape, dtype=str(arr.dtype), data=arr) ds.attrs['axis'] = axis ds.attrs['units'] = unit for path in self.fileupdates: dev, dtype, unit = self.fileupdates[path] if dev in dataset.values: value = dataset.values[dev] shape = numpy.atleast_1d(value).shape if path in nxfile: del nxfile[path] nxdset = nxfile.create_dataset(path, shape, dtype=dtype, data=value) nxdset.attrs['units'] = unit nxdset.attrs['nicos_name'] = dev nxfile.close()
class NamedDigitalInput(DigitalInput): """A DigitalInput with numeric values mapped to names.""" parameters = { 'mapping': Param('A dictionary mapping state names to integers', type=dictof(str, int)), } def doInit(self, mode): self._reverse = {v: k for (k, v) in self.mapping.items()} def doRead(self, maxage=0): value = self._taco_guard(self._dev.read) return self._reverse.get(value, value)
class Trigger(PyTangoDevice, Moveable): """sends a preconfigured string upon move to the configured StringIO This should only be used with SCPI devices and the string to be send should *not* provoke an answer. """ parameters = { 'strings': Param('mapping of nicos-value to pre-configured string', type=dictof(str, str), settable=True, unit=''), 'safesetting': Param('selection of a \'safe\' setting', type=str, settable=True, unit=''), } parameter_overrides = { 'unit': Override(default='', mandatory=False), } def doInit(self, mode): self.valuetype = oneof(*self.strings.keys()) if self.target not in self.strings: self._setROParam('target', self.safesetting) def doStart(self, value): # !ALWAYS! send selected string self._dev.WriteLine(self.strings[value]) # wait until our buffer is empty self._dev.Flush() # send a query and wait for the response, # which will only come after the above string was fully processed # note: this relies on the fact that the selected string above will NOT # provoke an answer, but just set parameters (base it on the *LRN? output) self._dev.Communicate('SYST:ERROR?') def doStatus(self, maxage=0): return status.OK, 'indeterminate' def read(self, maxage=0): # fix bad overwrite from StringIO return Moveable.read(self, maxage) def doRead(self, maxage=0): # no way to read back! return self.target
class HVSwitcher(Switcher): """Override precision to work with lists. Also sets all relevant PV values on the eight-packs when starting the detector. """ hardware_access = True parameters = { 'pv_values': Param('PV values to set when starting the detector', type=dictof(str, list), mandatory=True), } def _mapReadValue(self, pos): prec = self.precision for name, value in iteritems(self.mapping): if prec: if all(abs(p - v) <= prec for (p, v) in zip(pos, value)): return name elif pos == value: return name return self.fallback def doTime(self, old, new): if old != new: session.clock.tick(90) return 0 def doStart(self, target): # on start, also set all configured parameters if target == 'on' and self.read() != 'on': self.transferSettings() Switcher.doStart(self, target) @usermethod def transferSettings(self): import epics for epicsid, pvs in iteritems(self.pv_values): for pvname, pvvalue in pvs: fullpvname = '%s:%s_W' % (epicsid, pvname) self.log.debug('setting %s = %s' % (fullpvname, pvvalue)) epics.caput(fullpvname, pvvalue) session.delay(2)
class NamedDigitalOutput(DigitalOutput): """ A DigitalOutput with numeric values mapped to names. """ parameters = { 'mapping': Param( 'A dictionary mapping state names to integers - ' 'if not given, read the mapping from the Tango ' 'device if possible', type=dictof(str, int), mandatory=False), } def doInit(self, mode): if self.mapping: self._reverse = {v: k for (k, v) in self.mapping.items()} # oneofdict: allows both types of values (string/int), but # normalizes them into the string form self.valuetype = oneofdict(self._reverse) self._forward = self.mapping return try: self._reverse, self._forward = \ parse_mapping(self._getProperty('mapping')) # we don't build the valuetype from self._reverse since it should # only contain the write-able values self.valuetype = oneofdict( {v: k for (k, v) in self._forward.items()}) except Exception: self.log.warning('could not parse value mapping from Tango', exc=1) self._reverse = self._forward = {} def doStart(self, target): value = self._forward.get(target, target) self._dev.value = value def doRead(self, maxage=0): value = self._dev.value return self._reverse.get(value, value)
class LokiSample(Sample): """Device that collects the various sample properties specific to samples at LoKI. """ parameters = { 'position': Param( 'Mapping of devices to positions for driving to this' ' sample\'s position', type=dictof(str, anytype), settable=True), 'thickness': Param('Sample thickness (info only)', type=float, settable=True, unit='mm', category='sample'), 'comment': Param('Sample comment', type=str, settable=True, category='sample'), }
class FlexRegulator(TemperatureController): """Temperature controller with varying setup for software and hardware regulation.""" parameters = { 'dummy': Param('Dummy input device', type=tangodev, mandatory=True), 'configs': Param('Possible setups', type=dictof(str, entry), mandatory=True), 'config': Param('Current configuration', type=str, volatile=True, settable=True), } def doReadConfig(self): props = self._dev.GetProperties() indev = props[props.index('indev') + 1] outdev = props[props.index('outdev') + 1] for (cfgname, config) in iteritems(self.configs): if config[0] == outdev: if config[1] is None: if indev == self.dummy: return cfgname elif indev == config[1][0]: return cfgname return '<unknown>' def doWriteConfig(self, value): cfg = self.configs[value] props = ['outdev', cfg[0]] if cfg[1]: indev, outmin, outmax, initp, initi, initd = cfg[1] props += [ 'indev', indev, 'software', 'True', 'outmin', str(outmin), 'outmax', str(outmax), 'initpid', '[%s, %s, %s]' % (initp, initi, initd) ] else: props += ['indev', self.dummy, 'software', 'False'] self._dev.SetProperties(props)
class SelectorSwitcher(MultiSwitcher): """Switcher whose mapping is determined by a list of presets.""" parameters = { 'presets': Param('Presets that determine the mapping', type=dictof(str, dictwith(lam=float, speed=float, spread=float)), mandatory=True), } attached_devices = { 'det_pos': Attach('Detector preset device', DetectorPosSwitcherMixin), } def doUpdateValue(self, position): self._attached_det_pos._updateMapping(position) def _getWaiters(self): return self._attached_moveables def _start_unchecked(self, position): MultiSwitcher._start_unchecked(self, position) self._attached_det_pos._updateMapping(position)
class SingleSlit(PseudoNOK, HasOffset, Moveable): """Slit using one axis.""" hardware_access = False attached_devices = { 'motor': Attach('moving motor', Moveable), } parameters = { 'mode': Param('Beam mode', type=oneof(*MODES), settable=True, userparam=True, default='slit', category='general'), '_offsets': Param('List of offsets per mode position', settable=False, internal=True, type=dictof(str, float), default={}), 'opmode': Param('Mode of operation for the slit', type=oneof(CENTERED), userparam=True, settable=True, default=CENTERED, category='experiment'), } parameter_overrides = { 'masks': Override(type=dictwith(**{name: float for name in MODES}), unit='', mandatory=True), } valuetype = float def doWriteOffset(self, value): HasOffset.doWriteOffset(self, value) # deep copy is need to be able to change the values d = self._offsets.copy() d[self.mode] = value self._setROParam('_offsets', d) def doRead(self, maxage=0): return self._attached_motor.read(maxage) - self.masks[self.mode] - \ self.offset def doIsAllowed(self, target): return self._attached_motor.isAllowed(target + self.masks[self.mode]) def doStop(self): self._attached_motor.stop() def doStart(self, target): self._attached_motor.start(target + self.masks[self.mode] + self.offset) def doWriteMode(self, mode): self._attached_motor.start( self._attached_motor.read(0) + self.masks[mode] - self.masks[self.mode]) # update the offset parameter from offset mapping self._setROParam('offset', self._offsets.get(mode, 0.)) self.log.debug('New offset is now: %f', self.offset)
class DistancesHandler(BaseSequencer): """ AMOR component handling module. These distances along the optical bench are measured with the dimetix laser distance measurement device. To this purpose each component has a little mirror attached to it, at a different height for each component. The dimetix is sitting on a translation that can be moved up and down and thus can measure the distance of a given component by selecting the appropriate height. The calculation of distance for each component involves following properties: - Known offset of the attached mirror to the actual component - Offset in the calculation of the distance (occurs when laser is not at 0) - Value read from the laser The actual value is then given with the following equation: S = d - S' - ls where d is scale offset, S' is the value read by laser and ls is the known offset. """ valuetype = list parameters = { 'components': Param( 'Components mapped to tuple of their offsets ' '(mark_offset, scale_offset)', type=dictof(str, tuple), userparam=False), 'fixedcomponents': Param('Fixed components mapped to their distances', type=dictof(str, float)), 'rawdistances': Param('Calculated distances of components', type=dictof(str, float), userparam=False, settable=True), 'order': Param('Order of componenets for display/mesaurment', type=listof(str), userparam=False) } attached_devices = { 'switch': Attach('Switch to turn laser on/off', SpsSwitch), 'positioner': Attach('Positions laser to measure various components', Moveable), 'dimetix': Attach('Measures and returns the distance', Readable) } def __call__(self, components=None): # Print the distances of specified components from # Laser, Sample and Chopper if not components: components = self._components() sample = getattr(self, 'sample', None) chopper = getattr(self, 'chopper', None) table = [] self.log.info('Horizontal distances of components:') for component in components: distance = getattr(self, component, None) row = [component] if isinstance(distance, number_types): if isinstance(sample, number_types): row.append(str(sample - distance)) else: row.append('-') if isinstance(chopper, number_types): row.append(str(chopper - distance)) else: row.append('-') table.append(row) printTable(['Component', 'From SAMPLE', 'From CHOPPER'], table, self.log.info, rjust=True) def doInit(self, mode): self._update() def _update(self): unknown = [] inactive_loaded = [] for component in self._components(): self._update_component(component) val = getattr(self, component) if val == 'UNKNOWN': unknown.append(component) elif val == 'NOT ACTIVE' and component in session.loaded_setups: inactive_loaded.append(component) if unknown: self.log.warning('Distances for following components unknown:') self.log.warning('** ' + ', '.join(unknown)) self.log.warning(' ') if inactive_loaded: self.log.warning('Following components are inactive but loaded in ' 'setups:') self.log.warning('** ' + ', '.join(inactive_loaded)) self.log.warning('Do one of the following:') self.log.warning('Unload these setups OR Run: %s.mesaure() to ' 'redo distances!' % self.name) def doInfo(self): ret = [] for component in self._components(): ret.append((component, getattr(self, component), str(getattr(self, component)), 'mm', 'general')) return ret def doRead(self, maxage=0): return '' def doStatus(self, maxage=0): if self._seq_is_running(): return status.BUSY, 'Measuring' return status.OK, '' def doIsAtTarget(self, pos): return True def _update_component(self, component, printValues=False): if component in self.fixedcomponents: self.__dict__[component] = self.fixedcomponents.get(component) if printValues: self._logvalues( [component, "", getattr(self, component), "FIXED"]) return if component in self.components: raw = self.rawdistances.get(component) if raw is None: self.__dict__[component] = 'UNKNOWN' if printValues: self._logvalues([component, "", "", "UNKNOWN"]) return if raw > 8000: self.__dict__[component] = 'NOT ACTIVE' if printValues: self._logvalues([component, raw, "", "NOT ACTIVE"]) return offsets = self.components[component] if not isinstance(offsets, tuple): offsets = tuple(offsets) scaleoffset = offsets[1] if len(offsets) > 1 else 0.0 markoffset = offsets[0] self.__dict__[component] = abs(scaleoffset - raw - markoffset) if printValues: self._logvalues([component, raw, getattr(self, component), ""]) def _logvalues(self, values, isheader=False): if isheader: values = ['{0: <13}'.format(val) for val in values] printTable(values, [], self.log.info) else: values = ['{0: >13}'.format(val) for val in values] printTable([], [values], self.log.info) def _components(self): # Return the ordered list of components components = self.components.keys() + self.fixedcomponents.keys() # Add the missing components in the order dict ordered = self.order for comp in components: if comp not in ordered: ordered.append(comp) return sorted(components, key=ordered.index) def _update_raw_distance(self, component): distances = {} if self.rawdistances: distances.update(self.rawdistances) # Get the value from cox try: cox = session.getDevice('cox') except ConfigurationError: coxval = 0.0 else: coxval = cox.read(0) distances[component] = self._attached_dimetix.read(0) + coxval self.rawdistances = distances def _generateSequence(self, target): if not target: target = self._components() # Add everything to be done in the seq list seq = [] # If the laser is now on, turn it on if self._attached_switch.read(0) != 'ON': seq.append(SeqDev(self._attached_switch, 'ON')) seq.append(SeqSleep(5)) seq.append( SeqCall(self._logvalues, ['Component', 'Read', 'Final', 'Comments'], True)) for component in target: if component not in self._components(): comments = 'Skipping! Component not valid..' seq.append( SeqCall(self._logvalues, [component, '', '', comments])) continue if component in self.fixedcomponents: comments = 'Skipping! Component fixed..' seq.append( SeqCall(self._logvalues, [component, '', '', comments])) continue if component not in self._attached_positioner.mapping: comments = 'Skipping! Height not configured..' seq.append( SeqCall(self._logvalues, [component, '', '', comments])) continue # Move the laser to the component height seq.append(SeqDev(self._attached_positioner, component)) # Sleep for few seconds before reading the value seq.append(SeqSleep(3)) # Read in and change the distance measured by laser seq.append(SeqCall(self._update_raw_distance, component)) seq.append(SeqCall(self._update_component, component, True)) seq.append(SeqCall(self.log.info, 'Parking and turning off laser..')) seq.append(SeqDev(self._attached_positioner, 'park')) seq.append(SeqDev(self._attached_switch, 'OFF')) seq.append(SeqCall(self.log.info, 'Done! Summary below:')) seq.append(SeqCall(self.__call__, target)) seq.append(SeqCall(self._update)) return seq # User Methods @usermethod def measure(self, components=None): """Routine to calculate the distances of various components in AMOR. The laser is moved to the height of a component and the distance is then measured. NOTE: The following components are allowed: analyzer, detector, polarizer, filter, sample, slit1, slit2, slit3, slit4, selene Example: Following command calculates distances for all the components >>> CalculateComponentDistances() Following command calculates the distances for specified components >>> CalculateComponentDistances(['analyzer', 'sample']) """ if components is None: components = [] self.maw(components)
class Slit(CanReference, Moveable): """A rectangular slit consisting of four blades. The slit can operate in four "opmodes", controlled by the `opmode` parameter: * '4blades' -- all four blades are controlled separately. Values read from the slit are lists in the order ``[left, right, bottom, top]``; for ``move()`` the same list of coordinates has to be supplied. * '4blades_opposite' -- like '4blades', but left/right and bottom/top have opposite coordinate systems, i.e. [5, 5, 5, 5] is an opening of 10x10. * 'centered' -- only width and height are controlled; the slit is centered at the zero value of the left-right and bottom-top coordinates. Values read and written are in the form ``[width, height]``. * 'offcentered' -- the center and width/height are controlled. Values read and written are in the form ``[centerx, centery, width, height]``. Normally, the lower level ``right`` and ``left`` as well as the ``bottom`` and ``top`` devices need to share a common coordinate system, i.e. when ``right.read() == left.read()`` the slit is closed. A different convention can be selected when setting `coordinates` to ``"opposite"``: in this case, the blades meet at coordinate 0, and both move in positive direction when they open. All instances have attributes controlling single dimensions that can be used as devices, for example in scans. These attributes are: * `left`, `right`, `bottom`, `top` -- controlling the blades individually, independent of the opmode * `centerx`, `centery`, `width`, `height` -- controlling "logical" coordinates of the slit, independent of the opmode Example usage:: >>> move(slit.centerx, 5) # move slit center >>> scan(slit.width, 0, 1, 6) # scan over slit width from 0 to 5 mm """ attached_devices = { 'left': Attach('Left blade', HasPrecision), 'right': Attach('Right blade', HasPrecision), 'bottom': Attach('Bottom blade', HasPrecision), 'top': Attach('Top blade', HasPrecision), } parameters = { 'opmode': Param('Mode of operation for the slit', type=oneof('4blades', '4blades_opposite', 'centered', 'offcentered'), settable=True), 'coordinates': Param('Coordinate convention for left/right and ' 'top/bottom blades', default='equal', type=oneof('equal', 'opposite')), 'fmtstr_map': Param('A dictionary mapping operation modes to format ' 'strings (used for internal management).', type=dictof(str, str), settable=False, mandatory=False, userparam=False, default={ '4blades': '%.2f %.2f %.2f %.2f', '4blades_opposite': '%.2f %.2f %.2f %.2f', 'centered': '%.2f x %.2f', 'offcentered': '(%.2f, %.2f) %.2f x %.2f', }), 'parallel_ref': Param('Set to True if the blades\' reference drive ' 'can be done in parallel.', type=bool, default=False), } parameter_overrides = { 'fmtstr': Override(volatile=True), 'unit': Override(mandatory=False), } valuetype = tupleof(float, float, float, float) hardware_access = False _delay = 0.25 # delay between starting to move opposite blades def doInit(self, mode): self._axes = [self._attached_left, self._attached_right, self._attached_bottom, self._attached_top] self._axnames = ['left', 'right', 'bottom', 'top'] for name in self._axnames: self.__dict__[name] = self._adevs[name] for name, cls in [ ('centerx', CenterXSlitAxis), ('centery', CenterYSlitAxis), ('width', WidthSlitAxis), ('height', HeightSlitAxis) ]: self.__dict__[name] = cls(self.name+'.'+name, slit=self, unit=self.unit, lowlevel=True) def doShutdown(self): for name in ['centerx', 'centery', 'width', 'height']: if name in self.__dict__: self.__dict__[name].shutdown() def _getPositions(self, target): if self.opmode == '4blades': if len(target) != 4: raise InvalidValueError(self, 'arguments required for 4-blades ' 'mode: [left, right, bottom, top]') positions = list(target) elif self.opmode == '4blades_opposite': if len(target) != 4: raise InvalidValueError(self, 'arguments required for 4-blades ' 'mode: [left, right, bottom, top]') positions = [-target[0], target[1], -target[2], target[3]] elif self.opmode == 'centered': if len(target) != 2: raise InvalidValueError(self, 'arguments required for centered ' 'mode: [width, height]') positions = [-target[0]/2., target[0]/2., -target[1]/2., target[1]/2.] else: if len(target) != 4: raise InvalidValueError(self, 'arguments required for ' 'offcentered mode: [xcenter, ycenter, ' 'width, height]') positions = [target[0] - target[2]/2., target[0] + target[2]/2., target[1] - target[3]/2., target[1] + target[3]/2.] return positions def doIsAllowed(self, target): return self._doIsAllowedPositions(self._getPositions(target)) def _isAllowedSlitOpening(self, positions): ok, why = True, '' if positions[1] < positions[0]: ok, why = False, 'horizontal slit opening is negative' elif positions[3] < positions[2]: ok, why = False, 'vertical slit opening is negative' return ok, why def _doIsAllowedPositions(self, positions): f = self.coordinates == 'opposite' and -1 or +1 for ax, axname, pos in zip(self._axes, self._axnames, positions): if axname in ('left', 'bottom'): pos *= f ok, why = ax.isAllowed(pos) if not ok: return ok, '[%s blade] %s' % (axname, why) return self._isAllowedSlitOpening(positions) def doStart(self, target): self._doStartPositions(self._getPositions(target)) def _doStartPositions(self, positions): f = self.coordinates == 'opposite' and -1 or +1 tl, tr, tb, tt = positions # determine which axes to move first, so that the blades can # not touch when one moves first cl, cr, cb, ct = [d.read(0) for d in self._axes] cl *= f cb *= f al, ar, ab, at = self._axes if tr < cr and tl < cl: # both move to smaller values, need to start right blade first al.move(tl * f) session.delay(self._delay) ar.move(tr) elif tr > cr and tl > cl: # both move to larger values, need to start left blade first ar.move(tr) session.delay(self._delay) al.move(tl * f) else: # don't care ar.move(tr) al.move(tl * f) if tb < cb and tt < ct: ab.move(tb * f) session.delay(self._delay) at.move(tt) elif tb > cb and tt > ct: at.move(tt) session.delay(self._delay) ab.move(tb * f) else: at.move(tt) ab.move(tb * f) def doReset(self): multiReset(self._axes) multiWait(self._axes) def doReference(self): multiReference(self, self._axes, self.parallel_ref) def _doReadPositions(self, maxage): cl, cr, cb, ct = [d.read(maxage) for d in self._axes] if self.coordinates == 'opposite': cl *= -1 cb *= -1 return [cl, cr, cb, ct] def doRead(self, maxage=0): positions = self._doReadPositions(maxage) l, r, b, t = positions if self.opmode == 'centered': if abs((l+r)/2.) > self._attached_left.precision or \ abs((t+b)/2.) > self._attached_top.precision: self.log.warning('slit seems to be offcentered, but is ' 'set to "centered" mode') return [r-l, t-b] elif self.opmode == 'offcentered': return [(l+r)/2, (t+b)/2, r-l, t-b] elif self.opmode == '4blades_opposite': return [-l, r, -b, t] else: return positions def doPoll(self, n, maxage): # also poll sub-AutoDevices we created for dev in devIter(self.__dict__, baseclass=AutoDevice): dev.poll(n, maxage) def valueInfo(self): if self.opmode == 'centered': return Value('%s.width' % self, unit=self.unit, fmtstr='%.2f'), \ Value('%s.height' % self, unit=self.unit, fmtstr='%.2f') elif self.opmode == 'offcentered': return Value('%s.centerx' % self, unit=self.unit, fmtstr='%.2f'), \ Value('%s.centery' % self, unit=self.unit, fmtstr='%.2f'), \ Value('%s.width' % self, unit=self.unit, fmtstr='%.2f'), \ Value('%s.height' % self, unit=self.unit, fmtstr='%.2f') else: return Value('%s.left' % self, unit=self.unit, fmtstr='%.2f'), \ Value('%s.right' % self, unit=self.unit, fmtstr='%.2f'), \ Value('%s.bottom' % self, unit=self.unit, fmtstr='%.2f'), \ Value('%s.top' % self, unit=self.unit, fmtstr='%.2f') def doStatus(self, maxage=0): return multiStatus(list(zip(self._axnames, self._axes))) def doReadUnit(self): return self._attached_left.unit def doWriteFmtstr(self, value): # since self.fmtstr_map is a readonly dict a temp. copy is created # to update the dict and then put to cache back tmp = dict(self.fmtstr_map) tmp[self.opmode] = value self._setROParam('fmtstr_map', tmp) def doReadFmtstr(self): return self.fmtstr_map[self.opmode] def doWriteOpmode(self, value): if self._cache: self._cache.invalidate(self, 'value') def doUpdateOpmode(self, value): if value == 'centered': self.valuetype = tupleof(float, float) else: self.valuetype = tupleof(float, float, float, float)
class KWSSample(Sample): """Device that collects the various sample properties specific to samples at KWS. """ parameters = { 'aperture': Param('Aperture (x, y, w, h) for position', type=tupleof(float, float, float, float), unit='mm', settable=True), 'position': Param( 'Mapping of devices to positions for driving ' 'to this sample\'s position', type=dictof(str, anytype), settable=True), 'timefactor': Param('Measurement time factor for this sample', type=float, settable=True, category='sample'), 'thickness': Param('Sample thickness (info only)', type=float, settable=True, unit='mm', category='sample'), 'detoffset': Param('Detector offset (info only)', type=float, settable=True, unit='mm', category='sample'), 'comment': Param('Sample comment', type=str, settable=True, category='sample'), 'apname': Param('Name of sample aperture', type=str, mandatory=False, default='ap_sam'), } def _applyKwsParams(self, parameters): # these keys must be present when dealing with KWS1 samples, but are # *not* present in the dummy sample created on experiment start self.aperture = parameters.get('aperture', (0, 0, 0, 0)) self.position = parameters.get('position', {}) self.timefactor = parameters.get('timefactor', 1.0) self.thickness = parameters.get('thickness', 0.0) self.detoffset = parameters.get('detoffset', 0.0) self.comment = parameters.get('comment', '') def clear(self): Sample.clear(self) self._applyKwsParams({}) def _applyParams(self, number, parameters): Sample._applyParams(self, number, parameters) self._applyKwsParams(parameters) self.log.info('moving to position of sample %s (%s)...', number, parameters['name'] or 'unnamed') waitdevs = [] if self.aperture != (0, 0, 0, 0): ap = session.getDevice(self.apname) ap.opmode = 'offcentered' # to be sure ap.move(self.aperture) waitdevs.append(ap) for devname, devpos in iteritems(self.position): dev = session.getDevice(devname) dev.move(devpos) waitdevs.append(dev) multiWait(waitdevs) def set(self, number, parameters): for key in ('name', 'aperture', 'position', 'timefactor', 'thickness', 'detoffset', 'comment'): if key not in parameters: raise ConfigurationError( self, 'missing key %r in sample entry' % key) Sample.set(self, number, parameters)
class MiezeMaster(Moveable): parameters = { 'setting': Param('Current setting', type=int, settable=True), 'tuning': Param('Current tuning', type=str, settable=True, category='instrument', default=''), 'curtable': Param('Current tuning table', settable=True, type=listof(dictof(str, anytype))), 'tunetables': Param('Tuning tables for MIEZE settings', type=dictof(str, listof(dictof(str, anytype))), settable=True), } parameter_overrides = { 'unit': Override(mandatory=False, default=''), 'fmtstr': Override(default='%s'), } def doRead(self, maxage=0): if not self.tuning: return ['<no tuning selected>', 0.0] return [self.curtable[self.setting]['_name_'], self.curtable[self.setting]['_tau_']] def valueInfo(self): return Value('mieze', fmtstr='%s'), \ Value('tau', fmtstr='%.2f', unit='ps') def _findsetting(self, target): if not self.tuning: raise NicosError(self, 'no tuning selected, use %s.usetuning(name)' ' to select a tuning table' % self) if not isinstance(target, int): for idx, setting in enumerate(self.curtable): if setting['_name_'] == target: target = idx break else: raise NicosError(self, 'no such MIEZE setting: %r' % (target,)) if not 0 <= target < len(self.curtable): raise NicosError(self, 'no such MIEZE setting: %r' % target) return target def doStart(self, target): index = self._findsetting(target) setting = self.curtable[index] devs = sorted(setting.items()) for devname, devvalue in devs: if devname.startswith('amp'): self.log.debug('moving %r to %r', devname, 0) dev = session.getDevice(devname) dev.move(0) for devname, devvalue in devs: if devname.startswith('_') or devname.startswith('amp'): continue self.log.debug('moving %r to %r', devname, devvalue) dev = session.getDevice(devname) dev.move(devvalue) for devname, devvalue in devs: if not devname.startswith('amp'): continue self.log.debug('moving %r to %r', devname, devvalue) dev = session.getDevice(devname) dev.move(devvalue) self.setting = index def doWriteTuning(self, value): if value not in self.tunetables: raise NicosError(self, 'no such tuning: %r' % value) self.curtable = self.tunetables[value] @usermethod def usetuning(self, name): """Use the given tuning table.""" self.tuning = name @usermethod def listtunings(self): """List all existing tuning tables.""" data = [] for name, table in self.tunetables.items(): data.append((name, ', '.join(setting['_name_'] for setting in table))) self.log.info('all tuning tables:') printTable(('name', 'settings'), data, session.log.info) @usermethod def printtuning(self): """Print out the current tuning table.""" data = [] valueidx = {} all_values = set() for setting in self.curtable: all_values.update(key for key in setting if not key.startswith('_')) all_values = sorted(all_values) valueidx = {val: idx for idx, val in enumerate(all_values)} for idx, setting in enumerate(self.curtable): values = ['---'] * len(all_values) for devname, devvalue in setting.items(): if not devname.startswith('_'): values[valueidx[devname]] = str(devvalue)[:15] data.append((str(idx), setting['_name_'], '%.3f' % setting['_tau_']) + tuple(values)) self.log.info('current MIEZE settings (%s):', self.tuning) printTable(('#', 'name', 'tau (ps)') + tuple(all_values), data, session.log.info) @usermethod def savetuning(self, name): """Save the current tuning table.""" if name in self.tunetables: raise NicosError(self, 'tuning with this name exists already, ' 'please use removetuning() first') tables = self.tunetables.copy() tables[name] = copy.deepcopy(self.curtable) self.tunetables = tables self.log.info('current tuning saved as %r', name) @usermethod def removetuning(self, name): """Delete a tuning table.""" if name not in self.tunetables: raise NicosError(self, 'tuning %r not found in tables' % name) tables = self.tunetables.copy() del tables[name] self.tunetables = tables self.log.info('tuning %r removed', name) @usermethod def newsetting(self, name, **values): """Add a new setting to the current tuning table.""" table = self.curtable[:] try: index = self._findsetting(name) setting = table[index].copy() except NicosError: if not self.tuning: raise index = len(table) table.append({'_name_': name, '_tau_': 0}) setting = table[index] for devname, devvalue in values.items(): setting[devname] = devvalue table[index] = setting self.curtable = table self.log.info('created new MIEZE setting %r with index %s', name, index) @usermethod def updatesetting(self, name, **values): """Update a setting in the current tuning table.""" index = self._findsetting(name) table = self.curtable[:] setting = table[index].copy() for devname, devvalue in values.items(): setting[devname] = devvalue table[index] = setting self.curtable = table self.log.info('tuning for MIEZE setting %r updated', table[index]['_name_']) @usermethod def removesetting(self, name): """Remove a setting from the current tuning table.""" index = self._findsetting(name) table = self.curtable[:] setting = table.pop(index) self.curtable = table self.log.info('removed MIEZE setting %r', setting['_name_']) @usermethod def ordersettings(self, *names): """Reorder the settings in the current tuning table. Usage example:: mieze.ordersettings('46_69', '72_108', '200_300') """ indices = [self._findsetting(n) for n in names] other_indices = set(range(len(self.curtable))) - set(indices) new_table = [self.curtable[i] for i in chain(indices, other_indices)] self.curtable = new_table
class Authenticator(BaseAuthenticator): """Authenticates against the configured LDAP server. Basically it tries to bind on the server with the given userdn. Per default, all ldap users are rejected when there is no user level definition inside the 'roles' dictionary. """ BIND_METHODS = { 'none': ldap3.AUTO_BIND_NONE, 'no_tls': ldap3.AUTO_BIND_NO_TLS, 'tls_before_bind': ldap3.AUTO_BIND_TLS_BEFORE_BIND, 'tls_after_bind': ldap3.AUTO_BIND_TLS_AFTER_BIND, } parameters = { 'uri': Param('LDAP connection URI', type=str, mandatory=True), 'bindmethod': Param('LDAP port', type=oneof(*BIND_METHODS), default='no_tls'), 'userbasedn': Param('Base dn to query users.', type=str, mandatory=True), 'userfilter': Param('Filter for querying users. Must contain ' '"%(username)s"', type=str, default='(&(uid=%(username)s)' '(objectClass=posixAccount))'), 'groupbasedn': Param('Base dn to query groups', type=str, mandatory=True), 'groupfilter': Param( 'Filter for querying groups. ' 'Must contain "%(gidnumber)s"', type=str, default='(&(gidNumber=%(gidnumber)s)' '(objectClass=posixGroup))'), 'usergroupfilter': Param( 'Filter groups of a specific user. ' 'Must contain "%(username)s"', type=str, default='(&(memberUid=%(username)s)' '(objectClass=posixGroup))'), 'userroles': Param('Dictionary of allowed users with their ' 'associated role', type=dictof(str, oneof(*ACCESS_LEVELS.values()))), 'grouproles': Param('Dictionary of allowed groups with their ' 'associated role', type=dictof(str, oneof(*ACCESS_LEVELS.values()))), } def doInit(self, mode): self._access_levels = { value: key for key, value in iteritems(ACCESS_LEVELS) } def authenticate(self, username, password): userdn = self._get_user_dn(username) # first of all: try a bind to check user existence and password try: connection = ldap3.Connection( self.uri, user=userdn, password=password, auto_bind=self.BIND_METHODS[self.bindmethod]) except ldap3.core.exceptions.LDAPException as e: raise AuthenticationError('LDAP connection failed (%s)' % e) userlevel = -1 # check if the user has explicit rights if username in self.userroles: userlevel = self._access_levels[self.userroles[username]] return User(username, userlevel) # if no explicit user right was given, check group rights groups = self._get_user_groups(connection, username) for group in groups: if group in self.grouproles: userlevel = max(userlevel, self._access_levels[self.grouproles[group]]) if userlevel >= 0: return User(username, userlevel) raise AuthenticationError('Login not permitted for the given user') def _get_user_groups(self, connection, username): # start with the users default group groups = [self._get_default_group(connection, username)] # find additional groups of the user filter_str = self.usergroupfilter % {'username': username} connection.search(self.groupbasedn, filter_str, ldap3.LEVEL, attributes=ldap3.ALL_ATTRIBUTES) for group in connection.response: groups.append(group['attributes']['cn'][0]) return groups def _get_default_group(self, connection, username): filter_str = self.userfilter % {'username': username} if not connection.search(self.userbasedn, filter_str, ldap3.LEVEL, attributes=ldap3.ALL_ATTRIBUTES): raise AuthenticationError('User not found in LDAP directory') return self._get_group_name( connection, connection.response[0]['attributes']['gidNumber']) def _get_group_name(self, connection, gid): filter_str = self.groupfilter % {'gidnumber': gid} if not connection.search(self.groupbasedn, filter_str, ldap3.LEVEL, attributes=ldap3.ALL_ATTRIBUTES): raise AuthenticationError('Group %s not found' % gid) return connection.response[0]['attributes']['cn'][0] def _get_user_dn(self, username): return 'uid=%s,%s' % (username, self.userbasedn)
class DLSDetector(Measurable): attached_devices = { 'cards': Attach('DLS cards', DLSCard, multiple=True), 'shutter': Attach('Shutter to open before measuring', Moveable), 'limiters': Attach('The filter wheel adjusters', Moveable, multiple=True), 'lasersel': Attach('The laser wavelength selector', Readable, optional=True), } parameters = { 'duration': Param('Duration of a single DLS measurement.', default=10, unit='s', settable=True), 'intensity': Param('Intensity to aim for when adjusting filter wheels.', default=100, unit='kHz', settable=True), 'wavelengthmap': Param('Laser wavelength depending on selector', unit='nm', type=dictof(str, float)), # these should be sample properties instead... 'viscosity': Param('Sample viscosity', unit='cp', settable=True), 'refrindex': Param('Sample refractive index', settable=True), } def doInit(self, mode): self._adjusting = -1 self._measuring = False self._nfinished = 1 self._nstarted = 1 self._ntotal = 1 def _get_wavelength(self): if self._attached_lasersel: return self.wavelengthmap.get( self._attached_lasersel.read(), -1) else: return self.wavelengthmap.get('', -1) def doRead(self, maxage=0): return [] def valueInfo(self): return () def doReadArrays(self, quality): result = [] for card in self._attached_cards: result.append(card.readArray(quality)) result.append(card.readAbscissa()) result.append(card.readIntensity()) return result def arrayInfo(self): result = [] for card in self._attached_cards: result.append(card.arraydesc) result.append(card.abscissa_arraydesc) result.append(card.intensity_arraydesc) return result def doStatus(self, maxage=0): return BUSY if self._measuring else OK, \ '%d of %d done' % (self._nfinished, self._ntotal) def doSetPreset(self, **preset): if 't' in preset: self._ntotal = int(preset['t'] / self.duration) or 1 def presetInfo(self): return ('t',) def doStart(self): self._attached_shutter.start(1) self._adjusting = 0 self._nfinished = 0 self._nstarted = 0 self._measuring = True self._attached_limiters[0].start(self.intensity) def doFinish(self): self.doStop() def doStop(self): self._measuring = False for card in self._attached_cards: try: card.stop() except Exception: pass self._attached_shutter.start(0) for limiter in self._attached_limiters: limiter.maw(0) # close wheels def duringMeasureHook(self, elapsed): retval = None if self._adjusting > -1: adj_lim = self._attached_limiters[self._adjusting] if adj_lim.status(0)[0] == BUSY: return self.log.info('reached %s adjustment: %.1f kHz', adj_lim, adj_lim.read(0)) if self._adjusting == len(self._attached_limiters) - 1: self._adjusting = -1 else: self._adjusting += 1 self._attached_limiters[self._adjusting].start(self.intensity) return # start new measurements when needed if all(c.status(0)[0] != BUSY for c in self._attached_cards): if self._nstarted > self._nfinished: # read out now, start new measurement next time retval = INTERMEDIATE self._nfinished += 1 elif self._nfinished < self._ntotal: self.log.info('starting new DLS measurement') for card in self._attached_cards: card.setMode() card.preselection = self.duration card.start() self._nstarted += 1 else: self._measuring = False return retval
class NexusFileWriterSink(ProducesKafkaMessages, FileSink): """ Sink for writing NeXus files using the FileWriter. The file writer accepts commands on a Kafka topic. This command is in the form of a JSON object which contains properties such as start time, brokers and the nexus structure. The details are provided in the github repository of the FileWriter: https://github.com/ess-dmsc/kafka-to-nexus Topic on which the FileWriter accepts the commands is given by the parameter *cmdtopic*. The module containing various nexus templates is provided using the parameter *templatesmodule*. A default template can be chosen from these nexus templates using the parameter *templatename* Rules for the template: * Groups should be represented as: <group_name>:<NX_class> in the keys * NX_class should be one of the standard NeXus groups * Datasets should be represented as: <dataset_name> in the keys * Dataset value should be a instance of class NXDataset. * Attribute value can either be numerical or instance of class NXattribute * Detector event streams are marked using the class EventStream * Device values that are to be streamed are marked with DeviceStream Example template: template = { "entry1:NXentry": { "INST:NXinstrument": { "name": NXDataset("Instrument"), "detector:NXdetector": { "data": EventStream(topic="EventTopic", source="SrcName") }, }, "sample:NXsample": { "height": DeviceDataset('dev'), "property": DeviceDataset('dev', 'param', unit='K'), }, } } """ parameters = { 'cmdtopic': Param('Kafka topic where status commands are written', type=str, settable=False, preinit=True, mandatory=True), 'templatesmodule': Param('Python module containing NeXus nexus_templates', type=str, mandatory=True), 'templatename': Param('Template name from the nexus_templates module', type=str, mandatory=True), 'lastsinked': Param('Saves the counter, start and end time of sinks', type=tupleof(int, float, float, dictof(tuple, tuple)), settable=True, internal=True), 'useswmr': Param('Use SWMR feature when writing HDF files', type=bool, settable=False, userparam=False, default=True), 'start_fw_file': Param( 'JSON file containing the command for starting ' 'the filewriter (for testing)', type=str, default=None), 'title': Param('Title to set in NeXus file', type=str, settable=True, userparam=True, default=""), 'cachetopic': Param('Kafka topic for cache messages', type=str), } parameter_overrides = { 'settypes': Override(default=[POINT]), 'filenametemplate': Override(default=['%(proposal)s_%(pointcounter)08d.hdf']), } attached_devices = { 'status_provider': Attach('Device that provides file writing status', NexusFileWriterStatus, optional=True), } handlerclass = NexusFileWriterSinkHandler template = None def doInit(self, mode): self.log.info(self.templatesmodule) self._templates = __import__(self.templatesmodule, fromlist=[self.templatename]) self.log.info('Finished importing nexus_templates') self.set_template(self.templatename) def set_template(self, val): """ Sets the template from the given template modules. Parses the template using *parserclass* method parse. The parsed root, event kafka streams and device placeholders are then set. :param val: template name """ if not hasattr(self._templates, val): raise NicosError('Template %s not found in module %s' % (val, self.templatesmodule)) self.template = getattr(self._templates, val) if self.templatename != val: self._setROParam('templatename', val) @property def status_provider(self): """ Returns the class that provides status of this sink :return: Attached NexusFileWriterStatus """ return self._attached_status_provider def dataset_started(self, dataset): """ Capture that a dataset has started and change the status of the dataset :param dataset: the dataset that has been started """ if self.status_provider: jobid = str(dataset.uid) self.status_provider._on_issue(jobid, dataset) def dataset_ended(self, dataset, rewriting): """ Capture the end of the dataset. Record the counter number, start and end time of this dataset in cache using the *lastsinked* parameter. :param dataset: the dataset that was ended :param rewriting: Is this dataset being rewritten """ if self.status_provider: jobid = str(dataset.uid) self.status_provider._on_stop(jobid, dataset) # If rewriting, the dataset was already written in the lastsinked if not rewriting: self.lastsinked = (dataset.counter, dataset.started, dataset.finished, dataset.metainfo)
class SXTalSample(Sample): parameters = { 'cell': Param('Unit cell with matrix', type=SXTalCellType, settable=True, mandatory=False, default=SXTalCell.fromabc(5)), 'a': Param('a', type=float, category='sample'), 'b': Param('b', type=float, category='sample'), 'c': Param('c', type=float, category='sample'), 'alpha': Param('alpha', type=floatrange(1., 179.), category='sample'), 'beta': Param('beta', type=floatrange(1., 179.), category='sample'), 'gamma': Param('gamma', type=floatrange(1., 179.), category='sample'), 'rmat': Param('rmat', type=listof(listof(float)), default=None, userparam=False), 'ubmatrix': Param('UB matrix (rmat^T)', type=listof(listof(float)), category='sample'), 'bravais': Param('Bravais lattice', type=oneof(*symmetry.Bravais.conditions), settable=True, default='P', category='sample'), 'laue': Param('Laue group', type=oneof(*symmetry.symbols), settable=True, default='1', category='sample'), 'peaklists': Param('Lists of peaks for scanning', internal=True, type=dictof(str, list), settable=True), 'poslists': Param('Lists of positions for indexing', internal=True, type=dictof(str, list), settable=True), } def clear(self): """Clear experiment-specific information.""" Sample.clear(self) self.cell = SXTalCell.fromabc(5) self.peaklists = {} self.poslists = {} def new(self, parameters): """Accepts several ways to spell new cell params.""" lattice = parameters.pop('lattice', None) if lattice is not None: try: parameters['a'], parameters['b'], parameters['c'] = lattice except Exception: self.log.warning('invalid lattice spec ignored, should be ' '[a, b, c]') angles = parameters.pop('angles', None) if angles is not None: try: parameters['alpha'], parameters['beta'], \ parameters['gamma'] = angles except Exception: self.log.warning('invalid angles spec ignored, should be ' '[alpha, beta, gamma]') a = parameters.pop('a', None) if a is None: if 'cell' not in parameters: self.log.warning('using dummy lattice constant of 5 A') a = 5.0 b = parameters.pop('b', a) c = parameters.pop('c', a) alpha = parameters.pop('alpha', 90.0) beta = parameters.pop('beta', 90.0) gamma = parameters.pop('gamma', 90.0) # TODO: map spacegroup/bravais/laue with triple-axis bravais = parameters.pop('bravais', 'P') laue = parameters.pop('laue', '1') if 'cell' not in parameters: parameters['cell'] = [a, b, c, alpha, beta, gamma, bravais, laue] Sample.new(self, parameters) def _applyParams(self, number, parameters): Sample._applyParams(self, number, parameters) if 'cell' in parameters: self.cell = parameters['cell'] def doReadBravais(self): return self.cell.bravais.bravais def doWriteBravais(self, value): self.cell.bravais = symmetry.Bravais(value) def doReadLaue(self): return self.cell.laue.laue def doWriteLaue(self, value): self.cell.laue = symmetry.Laue(value) def doWriteCell(self, cell): params = cell.cellparams() self._setROParam('a', params.a) self._setROParam('b', params.b) self._setROParam('c', params.c) self._setROParam('alpha', params.alpha) self._setROParam('beta', params.beta) self._setROParam('gamma', params.gamma) self._setROParam('rmat', cell.rmat.tolist()) self._setROParam('ubmatrix', cell.rmat.T.tolist()) self._setROParam('bravais', cell.bravais.bravais) self._setROParam('laue', cell.laue.laue) self.log.info('New sample cell set. Parameters:') self.log.info('a = %8.3f b = %8.3f c = %8.3f', params.a, params.b, params.c) self.log.info('alpha = %8.3f beta = %8.3f gamma = %8.3f', params.alpha, params.beta, params.gamma) self.log.info('Bravais: %s Laue: %s', cell.bravais.bravais, cell.laue.laue) self.log.info('UB matrix:') for row in cell.rmat.T: self.log.info('%8.4f %8.4f %8.4f', *row)
class SamplePos(Moveable): """Control selector speed via SPS I/O devices.""" attached_devices = { 'active_ap': Attach('Alias for active aperture', DeviceAlias), 'active_x': Attach('Alias for active x translation', DeviceAlias), 'active_y': Attach('Alias for active y translation', DeviceAlias), } parameters = { 'alloweddevs': Param('List of allowed devices for presets', type=listof(str)), 'presets': Param('Presets for sample position switching', type=dictof(str, dictof(str, anytype))), } parameter_overrides = { 'unit': Override(default='', mandatory=False), } def doInit(self, mode): self.valuetype = oneof(*sorted(self.presets, key=num_sort)) self._waitdevs = [] self._aliases = {} self._devpos = {} for setting, values in self.presets.items(): values = dict(values) try: self._aliases[setting] = (values.pop('active_ap'), values.pop('active_x'), values.pop('active_y')) except KeyError: raise ConfigurationError( self, 'setting %r needs active_ap, active_x and active_y ' 'settings' % setting) from None try: for name in self._aliases[setting]: session.getDevice(name) except NicosError as exc: raise ConfigurationError( self, 'could not create/find alias targets for setting %r' % setting) from exc for key in values: if key not in self.alloweddevs: raise ConfigurationError( self, 'device %s is not allowed ' 'to be moved by sample_pos' % key) self._devpos[setting] = values def doRead(self, maxage=0): current_targets = ( self._attached_active_ap.alias, self._attached_active_x.alias, self._attached_active_y.alias, ) setting = None for setting, targets in self._aliases.items(): if targets == current_targets: break else: return 'unknown' ok = True for devname, devpos in self._devpos[setting].items(): dev = session.getDevice(devname) devval = dev.read(maxage) if isinstance(dev, HasPrecision): ok &= abs(devval - devpos) <= dev.precision elif isinstance(dev, Slit): ok &= all(abs(v - w) <= 0.1 for (v, w) in zip(devval, devpos)) else: ok &= devval == devpos if ok: return setting return 'unknown' def doStart(self, target): aliases = self._aliases[target] self._attached_active_ap.alias = aliases[0] self._attached_active_x.alias = aliases[1] self._attached_active_y.alias = aliases[2] self._waitdevs = [] for dev, devpos in self._devpos[target].items(): dev = session.getDevice(dev) dev.move(devpos) self._waitdevs.append(dev) def _getWaiters(self): if self._waitdevs: return self._waitdevs return self._adevs
class Resolution(MultiSwitcher): """Switcher for the resolution.""" parameters = { 'presets': Param('Available presets', type=dictof(anytype, anytype)), }