class KafkaSubscriber(DeviceMixinBase): """ Receives messages from Kafka, can subscribe to a topic and get all new messages from the topic if required via a callback method *new_message_callback*. """ parameters = { 'brokers': Param('List of kafka hosts to be connected', type=listof(host(defaultport=9092)), default=['localhost'], preinit=True, userparam=False) } def doPreinit(self, mode): self._consumer = kafka.KafkaConsumer( bootstrap_servers=self.brokers, auto_offset_reset='latest' # start at latest offset ) # Settings for thread to fetch new message self._stoprequest = True self._updater_thread = None def doShutdown(self): if self._updater_thread is not None: self._stoprequest = True if self._updater_thread.is_alive(): self._updater_thread.join() self._consumer.close() @property def consumer(self): return self._consumer def subscribe(self, topic): """ Create the thread that provides call backs on new messages """ # Remove all the assigned topics self._consumer.unsubscribe() topics = self._consumer.topics() if topic not in topics: raise ConfigurationError('Provided topic %s does not exist' % topic) # Assign the partitions partitions = self._consumer.partitions_for_topic(topic) if not partitions: raise ConfigurationError('Cannot query partitions for %s' % topic) self._consumer.assign( [kafka.TopicPartition(topic, p) for p in partitions]) self._stoprequest = False self._updater_thread = createThread('updater_' + topic, self._get_new_messages) self.log.debug('subscribed to updates from topic: %s' % topic) def _get_new_messages(self): while not self._stoprequest: sleep(self._long_loop_delay) messages = {} data = self._consumer.poll(5) for records in data.values(): for record in records: messages[record.timestamp] = record.value if messages: self.new_messages_callback(messages) else: self.no_messages_callback() self.log.debug("KafkaSubscriber thread finished") def new_messages_callback(self, messages): """This method is called whenever a new messages appear on the topic. The subclasses should define this method if a callback is required when new messages appear. :param messages: dict of timestamp and raw message """ def no_messages_callback(self): """This method is called if no messages are on the topic.
class Detector(Measurable): """Detector using multiple (synchronized) channels. Each channel can have a "preset" set, which means that measurement stops if the channel's value (or an element thereof, for channels with multiple read values) has reached the preset value. Passive channels can only stop the measurement via soft presets (presets that are checked by NICOS) during the countloop and therefore may be overshot by some nontrivial amount. In contrast, the derived `ActiveChannel` is able to stop by itself, usually implemented in hardware, so that the preset is reached exactly, or overshot by very little. In the detector, channels with a preset are called "masters", while channels without are called "slaves". Which channels are masters and slaves can change with every count cycle. """ attached_devices = { 'timers': Attach('Timer channel', PassiveChannel, multiple=True, optional=True), 'monitors': Attach('Monitor channels', PassiveChannel, multiple=True, optional=True), 'counters': Attach('Counter channels', PassiveChannel, multiple=True, optional=True), 'images': Attach('Image channels', ImageChannelMixin, multiple=True, optional=True), 'others': Attach('Channels that return e.g. filenames', PassiveChannel, multiple=True, optional=True), } parameters = { 'liveinterval': Param( 'Interval to read out live images (None ' 'to disable live readout)', type=none_or(float), unit='s', settable=True), 'saveintervals': Param( 'Intervals to read out intermediate images ' '(empty to disable); [x, y, z] will read out ' 'after x, then after y, then every z seconds', type=listof(float), unit='s', settable=True), 'postprocess': Param( 'Post processing list containing tuples of ' '(PostprocessPassiveChannel, ' 'ImageChannelMixin or PassiveChannel, ...)', type=listof(tuple)), } parameter_overrides = { 'fmtstr': Override(volatile=True), } hardware_access = False multi_master = True _last_live = 0 _last_save = 0 _last_save_index = 0 _last_preset = None _user_comment = '' def doInit(self, _mode): self._masters = [] self._slaves = [] self._channel_presets = {} self._postprocess = [] self._postpassives = [] for tup in self.postprocess: if tup[0] not in session.configured_devices: self.log.warning( "device %r not found but configured in " "'postprocess' parameter. No " "post processing for this device. Please " "check the detector setup.", tup[0]) continue postdev = session.getDevice(tup[0]) img_or_passive_devs = [session.getDevice(name) for name in tup[1:]] if not isinstance(postdev, PostprocessPassiveChannel): raise ConfigurationError("Device '%s' is not a " "PostprocessPassiveChannel" % postdev.name) if postdev not in self._channels: raise ConfigurationError("Device '%s' has not been configured " "for this detector" % postdev.name) for dev in img_or_passive_devs: if dev not in self._channels: raise ConfigurationError("Device '%s' has not been " "configured for this detector" % dev.name) elif isinstance(dev, PassiveChannel): self._postpassives.append(dev) self._postprocess.append((postdev, img_or_passive_devs)) # allow overwriting in derived classes def _presetiter(self): """Yield (name, device, type) tuples for all 'preset-able' devices.""" # a device may react to more than one presetkey.... for i, dev in enumerate(self._attached_timers): if i == 0: yield ('t', dev, 'time') for preset in dev.presetInfo(): yield (preset, dev, 'time') for dev in self._attached_monitors: for preset in dev.presetInfo(): yield (preset, dev, 'monitor') for i, dev in enumerate(self._attached_counters): if i == 0: yield ('n', dev, 'counts') for preset in dev.presetInfo(): yield (preset, dev, 'counts') for dev in self._attached_images + self._attached_others: for preset in dev.presetInfo(): yield (preset, dev, 'counts') yield ('live', None, None) def doPreinit(self, mode): presetkeys = {} for name, dev, typ in self._presetiter(): # later mentioned presetnames dont overwrite earlier ones presetkeys.setdefault(name, (dev, typ)) self._channels = uniq(self._attached_timers + self._attached_monitors + self._attached_counters + self._attached_images + self._attached_others) self._presetkeys = presetkeys self._getMasters() def _getMasters(self): """Internal method to collect all masters.""" masters = [] slaves = [] for ch in self._channels: if ch.ismaster: masters.append(ch) else: slaves.append(ch) self._masters, self._slaves = masters, slaves def _getPreset(self, preset): """Returns previous preset if no preset has been set.""" if not preset and self._last_preset: return self._last_preset if 'live' not in preset: # do not store live as previous preset self._last_preset = preset return preset def doSetPreset(self, **preset): self._user_comment = preset.pop('info', '') preset = self._getPreset(preset) if not preset: # keep old settings return for master in self._masters: master.ismaster = False self._channel_presets = {} for (name, value) in iteritems(preset): if name in self._presetkeys and name != 'live': dev = self._presetkeys[name][0] dev.setChannelPreset(name, value) self._channel_presets.setdefault(dev, []).append((name, value)) self._getMasters() if set(self._masters) != set(self._channel_presets): if not self._masters: self.log.warning('no master configured, detector may not stop') else: self.log.warning( 'master setting for devices %s ignored by ' 'detector', ', '.join(set(self._channel_presets) - set(self._masters))) self.log.debug(" presets: %s", preset) self.log.debug("presetkeys: %s", self._presetkeys) self.log.debug(" masters: %s", self._masters) self.log.debug(" slaves: %s", self._slaves) def doPrepare(self): for slave in self._slaves: slave.prepare() for master in self._masters: master.prepare() def doStart(self): # setting this to -interval, instead of 0, will send some live data at # the very start of each count, clearing the "live" data from last time self._last_live = -(self.liveinterval or 0) self._last_save = 0 self._last_save_index = 0 for slave in self._slaves: slave.start() for master in self._masters: master.start() def doTime(self, preset): self.doSetPreset(**preset) # okay in simmode return self.doEstimateTime(0) or 0 def doPause(self): success = True for slave in self._slaves: success &= slave.pause() for master in self._masters: success &= master.pause() return success def doResume(self): for slave in self._slaves: slave.resume() for master in self._masters: master.resume() def doFinish(self): for master in self._masters: master.finish() for slave in self._slaves: slave.finish() def doStop(self): for master in self._masters: master.stop() for slave in self._slaves: slave.stop() def doRead(self, maxage=0): ret = [] for ch in self._channels: ret.extend(ch.read()) return ret def doReadArrays(self, quality): arrays = [img.readArray(quality) for img in self._attached_images] results = [dev.read(0) for dev in self._postpassives] for postdev, img_or_passive_devs in self._postprocess: postarrays, postresults = [], [] for dev in img_or_passive_devs: if isinstance(dev, ImageChannelMixin): postarrays.append(arrays[self._attached_images.index(dev)]) else: # PassiveChannel postresults.append(results[self._postpassives.index(dev)]) postdev.setReadResult(postarrays, postresults, quality) return arrays def duringMeasureHook(self, elapsed): if self.liveinterval is not None: if self._last_live + self.liveinterval < elapsed: self._last_live = elapsed return LIVE intervals = self.saveintervals if intervals: if self._last_save + intervals[self._last_save_index] < elapsed: self._last_save_index = min(self._last_save_index + 1, len(intervals) - 1) self._last_save = elapsed return INTERMEDIATE return None def doSimulate(self, preset): self.doSetPreset(**preset) # okay in simmode return self.doRead() def doStatus(self, maxage=0): st, text = multiStatus(self._getWaiters(), maxage) if st == status.ERROR: return st, text for master in self._masters: for (name, value) in self._channel_presets.get(master, ()): if master.presetReached(name, value, maxage): return status.OK, text return st, text def doReset(self): for ch in self._channels: ch.reset() def valueInfo(self): ret = [] for ch in self._channels: ret.extend(ch.valueInfo()) return tuple(ret) def arrayInfo(self): return tuple(img.arraydesc for img in self._attached_images) def doReadFmtstr(self): return ', '.join('%s = %s' % (v.name, v.fmtstr) for v in self.valueInfo()) def presetInfo(self): return {'info'} | set(self._presetkeys) def doEstimateTime(self, elapsed): eta = {master.estimateTime(elapsed) for master in self._masters} eta.discard(None) if eta: # first master stops, so take min return min(eta) return None def doInfo(self): ret = [] if self._user_comment: ret.append(('usercomment', self._user_comment, self._user_comment, '', 'general')) presets = [] for (_dev, devpresets) in iteritems(self._channel_presets): for (key, value) in devpresets: presets.append((self._presetkeys[key][1], value)) if len(presets) > 1: mode = ' or '.join(p[0] for p in presets) ret.append(('mode', mode, mode, '', 'presets')) for (mode, value) in presets: ret.append( ('preset_%s' % mode, value, str(value), '', 'presets')) elif presets: mode, value = presets[0] return ret + [('mode', mode, mode, '', 'presets'), ('preset', value, str(value), '', 'presets')] return ret
class GatedDetector(Detector): """A Detector which enables some 'gates' before the measurment and disables them afterwards """ attached_devices = { 'gates': Attach('Gating devices', Moveable, multiple=True, optional=True), } parameters = { 'enablevalues': Param('List of values to enable the gates', type=listof(anytype), default=[]), 'disablevalues': Param('List of values to disable the gates', type=listof(anytype), default=[]), } def _enable_gates(self): self.log.debug('enabling gates') for dev, val in zip(self._attached_gates, self.enablevalues): dev.move(val) multiWait(self._attached_gates) self.log.debug('gates enabled') def _disable_gates(self): self.log.debug('disabling gates') for dev, val in zip(reversed(self._attached_gates), reversed(self.disablevalues)): dev.move(val) multiWait(self._attached_gates) self.log.debug('gates disabled') def doStart(self): self._enable_gates() Detector.doStart(self) def doResume(self): # check first gate to see if we need to (re-)enable them if self._attached_gates[0].read(0) != self.enablevalues[0]: self._enable_gates() Detector.doResume(self) def doPause(self): res = Detector.doPause(self) if res: self._disable_gates() return res def doStop(self): Detector.doStop(self) self._disable_gates() def doFinish(self): Detector.doFinish(self) self._disable_gates()
class KafkaCacheDatabase(MemoryCacheDatabase): """ Cache database that stores cache in Kafka topics without History. Current key and value pairs are stored in Kafka using log compaction. The `CacheEntry` values can be serialized using the attached device. This is then used to encode and decode entry instances while producing to the Kafka topic and while consuming from it. The data is stored in the Kafka topics using log compaction. Log compaction ensures that Kafka will always retain at least the last known value for each message key within the log of data for a single topic partition. The keys for partitioning are the keys received from cache server and the timestamp of the message is same as the time of the cache entry. If the provided `topic` does not exist, the cache database will not be able to proceed. Use the command line kafka tool to create the topic. This database will connect to the kafka and zookeeper services so they should be running in background on the provided `hosts`. For a basic getting started with kafka one can use the following guide: https://kafka.apache.org/quickstart Note that the minimum kafka broker version required for this database to run properly is 0.11.0. Before this version timestamps for messages could not be set. The default behavior of Kafka does not ensure that the topic will be cleaned up using the log compaction policy. For this to happen one of the following two is to be done: * Create topic by CLI Kafka tools using --config cleanup.policy=compact * Set in server.properties log.cleanup.policy=compact History is NOT supported with this database. """ parameters = { 'currenttopic': Param('Kafka topic where the current values of cache are streamed', type=str, mandatory=True), 'brokers': Param('List of Kafka bootstrap servers.', type=listof(host(defaultport=9092)), default=['localhost']), } attached_devices = { 'serializer': Attach('Device to serialize the cache entry values', CacheEntrySerializer, optional=False) } def doInit(self, mode): MemoryCacheDatabase.doInit(self, mode) # Create the producer self._producer = KafkaProducer(bootstrap_servers=self.brokers) # Create the consumer self._consumer = KafkaConsumer( bootstrap_servers=self.brokers, auto_offset_reset='earliest' # start at earliest topic ) # Give up if the topic does not exist if self.currenttopic not in self._consumer.topics(): raise ConfigurationError( 'Topic "%s" does not exit. Create this topic and restart.' % self.currenttopic) # Assign the partitions partitions = self._consumer.partitions_for_topic(self.currenttopic) self._consumer.assign( [TopicPartition(self.currenttopic, p) for p in partitions]) # Cleanup thread configuration self._stoprequest = False self._cleaner = createThread('cleaner', self._clean, start=False) def doShutdown(self): self._consumer.close() self._producer.close() # Stop the cleaner thread self._stoprequest = True self._cleaner.join() def initDatabase(self): self.log.info('Reading messages from kafka topic - %s', self.currenttopic) now = currenttime() message_count = 0 end = self._consumer.end_offsets(list(self._consumer.assignment())) for partition in self._consumer.assignment(): while self._consumer.position(partition) < end[partition]: msg = next(self._consumer) message_count += 1 if msg.value is not None: _, entry = self._attached_serializer.decode(msg.value) if entry is not None and entry.value is not None: # self.log.debug('%s (%s): %s -> %s', msg.offset, # msg.timestamp, msg.key, entry) if entry.ttl and entry.time + entry.ttl < now: entry.expired = True self._db[msg.key] = [entry] self._cleaner.start() self.log.info('Processed %i messages.', message_count) def _clean(self): def cleanonce(): with self._db_lock: for key, entries in iteritems(self._db): entry = entries[-1] if not entry.value or entry.expired: continue time = currenttime() if entry.ttl and (entry.time + entry.ttl < time): entry.expired = True for client in self._server._connected.values(): client.update(key, OP_TELLOLD, entry.value, time, None) while not self._stoprequest: sleep(self._long_loop_delay) cleanonce() def _update_topic(self, key, entry): # This method is responsible to communicate and update all the # topics that should be updated. Subclasses can (re)implement it # if there are messages to be produced to other topics self.log.debug('Writing: %s -> %s', key, entry.value) # For the log-compacted topic key deletion happens when None is # passed as the value for the key value = None if entry.value is not None: # Only when the key deletion is not required value = self._attached_serializer.encode(key, entry) self._producer.send(topic=self.currenttopic, value=value, key=bytes(key), timestamp_ms=int(entry.time * 1000)) # clear all local buffers and produce pending messages self._producer.flush() def tell(self, key, value, time, ttl, from_client): if value is None: # deletes cannot have a TTL ttl = None send_update = True always_send_update = False # remove no-store flag if key.endswith(FLAG_NO_STORE): key = key[:-len(FLAG_NO_STORE)] always_send_update = True try: category, subkey = key.rsplit('/', 1) except ValueError: category = 'nocat' subkey = key newcats = [category] if category in self._rewrites: newcats.extend(self._rewrites[category]) for newcat in newcats: key = newcat + '/' + subkey with self._db_lock: entries = self._db.setdefault(key, []) if entries: lastent = entries[-1] if lastent.value == value and not lastent.expired: # not a real update send_update = False thisent = CacheEntry(time, ttl, value) entries[:] = [thisent] if send_update: self._update_topic(key, thisent) if send_update or always_send_update: for client in self._server._connected.values(): if client is not from_client and client.is_active(): client.update(key, OP_TELL, value or '', time, ttl)
class DSPec(PyTangoDevice, Measurable): attached_devices = { 'gates': Attach('Gating devices', Moveable, multiple=True, optional=True), } parameters = { 'prefix': Param('prefix for filesaving', type=str, settable=False, mandatory=True, category='general'), 'ecalslope': Param('Energy calibration slope', type=float, mandatory=False, settable=True, volatile=True, default=0), 'ecalintercept': Param('Energy calibration interception', type=float, mandatory=False, settable=True, volatile=True, default=0), 'poll': Param('Polling time of the TANGO device driver', type=float, settable=False, volatile=True), 'cacheinterval': Param('Interval to cache intermediate spectra', type=float, unit='s', settable=True, default=1800), 'enablevalues': Param('List of values to enable the gates', type=listof(anytype), default=[], settable=True, category='general'), 'disablevalues': Param('List of values to disable the gates', type=listof(anytype), default=[]), } def _enable_gates(self): self.log.debug('enabling gates') for dev, val in zip(self._attached_gates, self.enablevalues): dev.move(val) multiWait(self._attached_gates) self.log.debug('gates enabled') def _disable_gates(self): self.log.debug('disabling gates') for dev, val in zip(reversed(self._attached_gates), reversed(self.disablevalues)): dev.move(val) multiWait(self._attached_gates) self.log.debug('gates disabled') # XXX: issues with ortec API -> workarounds and only truetime and livetime # working. def doReadEcalslope(self): ec = self._dev.EnergyCalibration.strip() return float(ec.split()[1]) def doReadEcalintercept(self): ec = self._dev.EnergyCalibration.strip() return float(ec.split()[2]) def doReadArrays(self, quality): spectrum = None try: spectrum = self._dev.Value except NicosError: # self._comment += 'CACHED' if self._read_cache is not None: self.log.warning('using cached spectrum') spectrum = self._read_cache else: self.log.warning('no spectrum cached') return [spectrum] def doRead(self, maxage=0): ret = [ self._dev.TrueTime[0], self._dev.LiveTime[0], sum(self._dev.Value.tolist()) ] return ret def doReadPoll(self): return self._dev.PollTime[0] def _clear(self): self._started = None self._lastread = 0 self._comment = '' self._name_ = '' self._stop = None self._preset = {} self._dont_stop_flag = False self._read_cache = None def doReset(self): self._clear() self._dev.Init() def doInit(self, mode): self._clear() self.arraydesc = ArrayDesc('data', (1, 16384), np.uint32) def doFinish(self): self.doStop() # reset preset values self._clear() def doReadIsmaster(self): pass def presetInfo(self): return { 'info', 'Filename', 'TrueTime', 'LiveTime', 'ClockTime', 'counts' } def doSetPreset(self, **preset): self._clear() if 'TrueTime' in preset: try: self._dev.SyncMode = 'RealTime' self._dev.SyncValue = preset['TrueTime'] * 1000 except NicosError: try: self.doStop() self._dev.Init() except NicosError: return self._dev.SyncMode = 'RealTime' self._dev.SyncValue = preset['TrueTime'] * 1000 elif 'LiveTime' in preset: try: self._dev.SyncMode = 'LiveTime' self._dev.SyncValue = preset['LiveTime'] * 1000 except NicosError: try: self.doStop() self._dev.Init() except NicosError: return self._dev.SyncMode = 'LiveTime' self._dev.SyncValue = preset['LiveTime'] * 1000 elif 'ClockTime' in preset: self._stop = preset['ClockTime'] elif 'counts' in preset: pass self._preset = preset def doTime(self, preset): if 'TrueTime' in preset: return preset['TrueTime'] elif 'LiveTime' in preset: return preset['LiveTime'] elif 'ClockTime' in preset: return abs(float(preset['ClockTime']) - currenttime()) elif 'counts' in preset: return 1 return None def doEstimateTime(self, elapsed): if self.doStatus()[0] == status.BUSY: if 'TrueTime' in self._preset: return self._preset['TrueTime'] - elapsed elif 'LiveTime' in self._preset: return self._preset['LiveTime'] - elapsed elif 'ClockTime' in self._preset: return abs(float(self._preset['ClockTime']) - currenttime()) return None def doStart(self): self._enable_gates() try: self._dev.Stop() self._dev.Clear() self._dev.Start() except NicosError: try: self._dev.Stop() self._dev.Init() self._dev.Clear() self._dev.Start() except NicosError: pass self._started = currenttime() self._lastread = currenttime() def doPause(self): self._dev.Stop() self._disable_gates() return True def doResume(self): # check first gate to see if we need to (re-)enable them if self._attached_gates[0].read(0) != self.enablevalues[0]: self._enable_gates() try: self._dev.Start() except NicosError: self._dev.Init() self._dev.Stop() self._dev.Start() def doStop(self): if self._dont_stop_flag: self._dont_stop_flag = False return try: self._dev.Stop() except NicosError: self._dev.Init() self._dev.Stop() self._disable_gates() def duringMeasureHook(self, elapsed): if (elapsed - self._lastread) > self.cacheinterval: try: self._read_cache = self._dev.Value self.log.debug('spectrum cached') except NicosError: self.log.warning('caching spectrum failed') finally: self._lastread = elapsed return LIVE return None def doSimulate(self, preset): for t in 'TrueTime', 'LiveTime', 'ClockTime': if t in preset: return [preset[t], preset[t], 0] if 'counts' in preset: return [1, 1, preset['counts']] return [0, 0, 0] def doIsCompleted(self): if self._started is None: return True if self._dont_stop_flag is True: return (currenttime() - self._started) >= self._preset['value'] if self._stop is not None: if currenttime() >= self._stop: return True if 'TrueTime' in self._preset: return self._dev.TrueTime >= self._preset['TrueTime'] elif 'LiveTime' in self._preset: return self._dev.LiveTime >= self._preset['LiveTime'] elif 'counts' in self._preset: return self.doRead(0)[2] >= self._preset['counts'] try: # self.log.warning('poll') stop = self.poll[0] return stop < 0 except NicosError: self._dont_stop_flag = True # self.log.warning('read poll time failed, waiting for other ' # 'detector(s)...') return False return False def valueInfo(self): return ( Value(name='truetim', type='time', fmtstr='%.3f', unit='s'), Value(name='livetim', type='time', fmtstr='%.3f', unit='s'), Value('DSpec', type='counter', fmtstr='%d', unit='cts'), ) def arrayInfo(self): return (self.arraydesc, )
class BaseCryopad(Moveable): """Controls the whole state of the cryopad. How to determine the wavelength and sample angles must be implemented in subclasses, via the _getInstr* methods. """ attached_devices = { 'nut_in': Attach('Incoming nutator axis', Moveable), 'prec_in': Attach('Incoming precession current', Moveable), 'nut_out': Attach('Outgoing nutator axis', Moveable), 'prec_out': Attach('Outgoing precession current', Moveable), } parameters = { # 01/2017: [108.27, 0, 0, 80.73, 0, 0] 'coefficients': Param('Coefficients for calculating currents', type=listof(float), settable=True, default=[108.46, 0, 0, 80.77, 0, 0]), 'meissnercorrection': Param('Correct for curved Meissner shield?', type=bool, default=True), } parameter_overrides = { 'fmtstr': Override(default='%.1f %.1f / %.1f %.1f'), } valuetype = tupleof(float, float, float, float) def _getWaiters(self): return [ self._attached_nut_in, self._attached_nut_out, self._attached_prec_in, self._attached_prec_out ] def _getInstrPosition(self, maxage=None): """Return (lambda_in, lambda_out, gamma, sense) of the instrument. Wavelength must be in Angstrom, the angles must be in degrees. """ raise NotImplementedError def _getInstrTarget(self): """Return targets (lambda_in, lambda_out, gamma, sense) of the instrument. """ raise NotImplementedError def doRead(self, maxage=None): lam_in, lam_out, gamma, sense = self._getInstrPosition(maxage) prec_in = self._attached_prec_in.read(maxage) prec_out = self._attached_prec_out.read(maxage) theta_in = self._attached_nut_in.read(maxage) theta_out = self._attached_nut_out.read(maxage) if self.meissnercorrection: theta_out -= calc.curved_meissner_correction(theta_out) chi_in, chi_out = calc.currents_to_angles(self.coefficients, prec_in, prec_out, lam_in, lam_out, gamma) self.log.debug('chi_in=%s, chi_out=%s', chi_in, chi_out) xyz_in = calc.pol_in_from_angles(sense, theta_in, chi_in, lam_in, lam_out, gamma) xyz_out = calc.pol_out_from_angles(sense, theta_out, chi_out, lam_in, lam_out, gamma) self.log.debug('xyz_in=%s, xyz_out=%s', xyz_in, xyz_out) polar_in = calc.polar_angles_from_xyz(*xyz_in) polar_out = calc.polar_angles_from_xyz(*xyz_out) return polar_in + polar_out # a 4-tuple def doStart(self, target): lam_in, lam_out, gamma, sense = self._getInstrTarget() polar_th_in, polar_phi_in, polar_th_out, polar_phi_out = target theta_in, chi_in = calc.cryopad_in(sense, lam_in, lam_out, gamma, polar_th_in, polar_phi_in) theta_in, chi_in = calc.optimize_angles(theta_in, chi_in) theta_out, chi_out = calc.cryopad_out(sense, lam_in, lam_out, gamma, polar_th_out, polar_phi_out) theta_out, chi_out = calc.optimize_angles(theta_out, chi_out) if self.meissnercorrection: theta_out += calc.curved_meissner_correction(theta_out) prec_in, prec_out = calc.angles_to_currents(self.coefficients, chi_in, chi_out, lam_in, lam_out, gamma) self._attached_nut_in.start(theta_in) self._attached_nut_out.start(theta_out) self._attached_prec_in.start(prec_in) self._attached_prec_out.start(prec_out)
class Detector(MeasureSequencer): """SPODI specific detector. The detector moves around an axis (2theta) in a number of steps (`resosteps`) over a range of 2 degrees (given in `range`). The starting position is current position + (`resosteps` - 1) * `range` / `resosteps`. The detector data (image) will be stored in an accumulated image array: The first columns of each step will be stored in a sequence, the second columns will follow, and so on. At the end the image will be a refined picture of a single histogram. """ attached_devices = { 'motor': Attach('Axis to perform the reso(lution) steps', Moveable), 'detector': Attach('Standard detector device', GenericDetector), } parameters = { 'resosteps': Param('Number of steps performed by the motor to ' 'accumulate a single spectrum', type=oneof(1, 2, 4, 5, 8, 10, 20, 25, 40, 50, 80, 100), default=40, settable=True, userparam=True, category='general', ), 'range': Param('Fullrange for the resosteps', type=float, settable=False, default=2.0, category='instrument', ), 'numinputs': Param('Number of detector channels', type=int, default=80, settable=False, category='general', ), 'liveinterval': Param('Interval to read out live images (None to ' 'disable live readout)', type=none_or(float), unit='s', settable=True, default=0.5, ), 'rates': Param('The rates detected by the detector', settable=False, type=listof(float), internal=True, category='status', ), '_startpos': Param('Store the starting position', type=float, settable=True, internal=True, category='instrument', ), '_step': Param('Store the current step', type=intrange(0, 100), settable=True, internal=True, ), } parameter_overrides = { 'fmtstr': Override(volatile=True), } _last_live = 0 _step = 0 _time_preset = 0 _mon_preset = 0 _arraydesc = None _det_run = False def doInit(self, mode): self._data = [0] * len(self._attached_detector.valueInfo()) self._set_resosteps(self.resosteps) def doInfo(self): ret = self._attached_detector.doInfo() return ret def doPrepare(self): MeasureSequencer.doPrepare(self) self._attached_detector.doPrepare() def doStart(self): self._startpos = self._attached_motor.read() + \ (self.resosteps - 1) * self._step_size self.log.debug('det._startpos: %r', self._startpos) self._setROParam('rates', [0., 0., 0.]) session.experiment.data.updateMetainfo() self._last_live = 0 self._step = 0 self._array_data.fill(0) self._data = [0] * len(self._attached_detector.valueInfo()) MeasureSequencer.doStart(self) def doSetPreset(self, **preset): self._tths = None if preset: self._time_preset = preset.get('t', 0) self._mon_preset = preset.get('mon1', preset.get('mon2', 0)) if 'resosteps' in preset: self.resosteps = int(preset.pop('resosteps')) if 'tths' in preset: self._tths = float(preset.pop('tths')) self._attached_detector.doSetPreset(**preset) def _read_value(self): ret = self._attached_detector.read() self._data = [sum(x) for x in zip(self._data, ret)] # Detector is not busy anymore, but to early to consider it as # 'not busy' self._det_run = False imgret = self._attached_detector.readArrays(FINAL)[0].astype('<u4', order='F') # self.log.info('%r', imgret) if self._mode != SIMULATION: if imgret.shape[0] == self._array_data.shape[1]: self._array_data[self._step::self.resosteps] = np.transpose( imgret) else: self._array_data[self._step::self.resosteps] = imgret def _incStep(self): if self._step < self.resosteps - 1: self._step += 1 def _startDet(self): """Start the detector and mark it running. Since the detector is not really in BUSY mode after start, we need an additional flag to mark the detector started. """ self._attached_detector.prepare() waitForCompletion(self._attached_detector) self._attached_detector.start() self._det_run = True def doReadArrays(self, quality): self.log.debug('doReadArrays: %d/%d: %d, %r', self._step, self.resosteps, self._array_data.sum(), self._array_data.shape) if quality == LIVE: imgret = self._attached_detector.readArrays(FINAL)[0].astype( '<u4', order='F') self.log.debug('Shapes:%r %r', imgret.shape, self._array_data.shape) if imgret.shape[0] == self._array_data.shape[1]: self._array_data[self._step::self.resosteps] = np.transpose( imgret) else: self._array_data[self._step::self.resosteps] = imgret return [self._array_data] def _generateSequence(self): seq = [] if self._tths: self._startpos = self._tths seq.append(SeqDev(self._attached_motor, self._startpos, stoppable=True)) for step in range(self.resosteps): pos = self._startpos - step * self._step_size seq.append(SeqDev(self._attached_motor, pos, stoppable=True)) seq.append(SeqCall(self._startDet)) seq.append(SeqWait(self._attached_detector)) seq.append(SeqCall(self._read_value)) seq.append(SeqCall(self._incStep)) return seq def doRead(self, maxage=0): if self._step < self.resosteps: if self._attached_detector.status(0)[0] == status.BUSY \ or self._det_run: ret = [self._step + 1] + \ [sum(x) for x in zip(self._data, self._attached_detector.doRead(maxage))] else: if self._step == 1 and \ MeasureSequencer.status(self, 0)[0] != status.BUSY: ret = [self._step] + self._data else: ret = [self._step + 1] + self._data else: ret = [self._step] + self._data # ret = [step, meastime, mon1, mon2, counts] meastime = ret[1] if meastime > 0.: ctrrate = ret[-1] / meastime mon1rate = ret[2] / meastime mon2rate = 0 if len(self._attached_detector._attached_monitors) > 1: mon2rate = ret[-2] / meastime self._setROParam('rates', [mon1rate, mon2rate, ctrrate]) return ret def doReset(self): self._det_run = False self._last_live = 0 self._step = 0 self._array_data.fill(0) self._attached_detector.doReset() self._data = [0] * len(self._attached_detector.valueInfo()) MeasureSequencer.doReset(self) # self._attached_motor.maw(self._startpos) def doPause(self): self._attached_detector.doPause() def doResume(self): self._attached_detector.doResume() def doFinish(self): self._attached_detector.doFinish() def doSimulate(self, preset): return [self.resosteps] + self._attached_detector.doSimulate(preset) def _set_resosteps(self, value): # TODO: shape should be (y, x) not (x, y) image = self._attached_detector._attached_images[0] if hasattr(image, 'size'): height = image.size[1] elif hasattr(image, 'sizes'): height = image.sizes[1] else: height = 256 shape = (value * self.numinputs, height) self._step_size = self.range / value if not self._arraydesc: self._arraydesc = ArrayDesc('data', shape=shape, dtype='<u4') self._array_data = np.zeros(shape, dtype='<u4', order='F') else: self._arraydesc.shape = shape self._array_data = np.resize(self._array_data, shape).astype('<u4', order='F') self._array_data.fill(0) if self._mode != SIMULATION: self._cache.put(self, 'fmtstr', self._fmtstr(value)) self.log.debug('%r', self._arraydesc) self.log.debug('stepsize: %f', self._step_size) def doWriteResosteps(self, value): self._set_resosteps(value) def _fmtstr(self, value): return 'step = %d' + '/%d, ' % value + \ self._attached_detector.doReadFmtstr() def doReadFmtstr(self): return self._fmtstr(self.resosteps) def doEstimateTime(self, elapsed): # TODO calculate the estimated time better in case of monitor counting # the _time_preset value is only value for time counting mode mspeed = self._attached_motor.speed or 1.0 steptime = (self.range / mspeed) / self.resosteps if MeasureSequencer.status(self, 0)[0] == status.BUSY: step = int(abs(self._attached_motor.read() - self._startpos) / self._step_size) ret = (steptime + self._time_preset) * (self.resosteps - step) else: ret = (steptime + self._time_preset) * self.resosteps detTime = self._attached_detector.doEstimateTime(elapsed) ret += detTime if detTime is not None else 0. return ret def valueInfo(self): _val_info = Value('step', unit='', type='other', fmtstr='%d' + '/%d' % self.resosteps), return _val_info + self._attached_detector.valueInfo() def arrayInfo(self): return self._arraydesc, def presetInfo(self): return {'resosteps', 'tths'} | self._attached_detector.presetInfo() def duringMeasureHook(self, elapsed): if self.liveinterval is not None: if self._last_live + self.liveinterval < elapsed: self._last_live = elapsed return LIVE return None def _stopAction(self, nr): self.log.debug('_stopAction at step: %d', nr) self._attached_detector.stop() def _cleanUp(self): if self._seq_was_stopped: self._seq_was_stopped = False self._set_seq_status(status.OK, 'idle')
class Watchdog(BaseCacheClient): """Main device for running the watchdog service.""" parameters = { 'watch': Param('The configuration of things to watch', type=listof(dictof(str, anytype))), 'mailreceiverkey': Param( 'Cache key that updates the receivers for ' 'any mail notifiers we have configured', type=str), # these would be adevs, but adevs don't allow this flexible # configuration with dictionaries 'notifiers': Param('Configures the notifiers for each warning type', type=dictof(str, listof(str))), } parameter_overrides = { 'prefix': Override(mandatory=False, default='nicos/'), } def doInit(self, mode): BaseCacheClient.doInit(self, mode) # cache of all interesting keys with current values self._keydict = LCDict() # put status constants in key dict to simplify status conditions for stval, stname in status.statuses.items(): self._keydict[stname.upper()] = stval # set to true during connect action self._process_updates = False # current setups self._setups = set() # mapping entry ids to entrys self._entries = {} # (mangled) key to update mail receivers self._mailreceiverkey = self.mailreceiverkey.replace('/', '_').lower() # mapping cache keys to entries that check this key self._keymap = {'session_mastersetup': set()} if self._mailreceiverkey: self._keymap[self._mailreceiverkey] = set() # current warnings: mapping entry ids to the string description self._warnings = OrderedDict() # current count loop pause reasons: mapping like self._warnings self._pausecount = OrderedDict() # create all notifier devices self._all_notifiers = [] self._notifiers = {'': []} for key, devnames in iteritems(self.notifiers): self._notifiers[key] = notiflist = [] for devname in devnames: dev = session.getDevice(devname, Notifier) notiflist.append(dev) self._all_notifiers.append(dev) # process entries in the default watchlist for entry_dict in self.watch: self._add_entry(entry_dict, 'watchdog') # start a thread checking for modification of the setup file createThread('refresh checker', self._checker) def _checker(self): setupname = session.explicit_setups[0] fn = session._setup_info[setupname]['filenames'] watchFileContent(fn, self.log) self.log.info('setup file changed; restarting watchdog process') os.execv(sys.executable, [sys.executable] + sys.argv) def _add_entry(self, entryd, source): entryd = dict(entryd) # convert back from readonlydict logprefix = 'entry %s from setup %r' % (entryd, source) # sanity-check the input if not entryd.get('condition'): self.log.warning('%s: missing "condition" key', logprefix) return if not entryd.get('message'): self.log.warning('%s: missing "message" key', logprefix) return if entryd.get('scriptaction') not in (None, 'pausecount', 'stop', 'immediatestop'): self.log.warning( '%s: scriptaction is invalid, needs ' "to be one of 'pausecount', 'stop' or " "'immediatestop'", logprefix) entryd.pop('scriptaction') entry = Entry(entryd) entry.from_setup = source if entry.id in self._entries: self.log.error('%s: duplicate entry, ignoring', logprefix) return if entry.type and entry.type not in self._notifiers: log_msg = ( '%s: the condition type %r is not valid, must be ' 'one of %r' % (logprefix, entry.type, ', '.join(map(repr, self._notifiers)))) if 'default' in self._notifiers: self.log.warning(log_msg + '; using default') entry.type = 'default' else: self.log.warning(log_msg + '; ignoring notifiers') entry.type = '' try: cond = Expression(self.log, entry.condition, entry.setup) if entry.gracetime: cond = DelayedTrigger(self.log, cond, entry.gracetime) if entry.precondition: precond = Expression(self.log, entry.precondition, entry.setup) if entry.precondtime: precond = DelayedTrigger(self.log, precond, entry.precondtime) cond = Precondition(self.log, precond, cond) if not entry.enabled: cond.enabled = False entry.cond_obj = cond except Exception: self.log.error( '%s: could not construct condition, ignoring ' 'this condition', logprefix) return for key in cond.interesting_keys(): self._keymap.setdefault(key, set()).add(entry) self._entries[entry.id] = entry def _remove_entry(self, eid): entry = self._entries.pop(eid, None) if entry: for key in entry.cond_obj.interesting_keys(): self._keymap[key].discard(entry) # cache client API def _connect_action(self): # inhibit direct processing of updates self._process_updates = False try: BaseCacheClient._connect_action(self) # now process all keys we got time = currenttime() for key in list(self._keydict): try: self._process_key(time, key, self._keydict[key]) except Exception: self.log.warning('error handling first update for key %s', key, exc=1) finally: self._process_updates = True self.storeSysInfo('watchdog') self._queue.put('watchdog/%s\n' % OP_SUBSCRIBE) self._publish_config() def _wait_data(self): t = currenttime() for entry in itervalues(self._entries): if entry.cond_obj.tick(t) or entry.cond_obj.is_expired(t): self._check_state(entry, t) def _handle_msg(self, time, ttlop, ttl, tsop, key, op, value): if key.startswith('watchdog/'): self._handle_control_msg(key, cache_load(value), time, op != OP_TELL) return key = key[len(self._prefix):].replace('/', '_').lower() # do we need the key for conditions? if key not in self._keymap: return expired = op == OP_TELLOLD or not value value = None if expired else cache_load(value) if not expired: self._keydict[key] = value else: self._keydict.pop(key, None) time = float(time) if self._process_updates: self._process_key(time, key, value) # internal watchdog API def _handle_control_msg(self, key, value, time, expired): if key == 'watchdog/enable': time = currenttime() for (eid, enabled) in value[1]: entry = self._entries.get(eid) if entry: entry.cond_obj.enabled = enabled entry.cond_obj.update(time, self._keydict) self._check_state(entry, time) self.log.info('updated enabled conditions by user request') elif key == 'watchdog/reset': # reset all condition enables to their initial state # (e.g. due to NewExperiment) for entry in itervalues(self._entries): if entry.enabled != entry.cond_obj.enabled: entry.cond_obj.enabled = entry.enabled entry.cond_obj.update(time, self._keydict) self._check_state(entry, time) self.log.info('enable status of all conditions reset') self._publish_config() def _publish_config(self): # publish current condition info in the cache self._put_message('configured', None, [c.serialize() for c in itervalues(self._entries)]) def _process_key(self, time, key, value): # check setups? if key == 'session_mastersetup' and value: self._setups_updated(time, set(value)) return # update notification targets? if key == self._mailreceiverkey and value: self._update_mailreceivers(value) return for entry in self._keymap.get(key, ()): entry.cond_obj.update(time, self._keydict) self._check_state(entry, time) def _setups_updated(self, time, new_setups): prev_setups, self._setups = self._setups, new_setups # check if we need to remove some conditions for entry in listvalues(self._entries): if entry.from_setup != 'watchdog': if entry.from_setup not in self._setups: self._remove_entry(entry.id) # check if we need to add some conditions session.readSetups() # refresh setup info for new_setup in self._setups - prev_setups: info = session._setup_info.get(new_setup) if info and info['watch_conditions']: self.log.info('adding conditions from setup %s', new_setup) for entry_dict in info['watch_conditions']: self._add_entry(entry_dict, new_setup) # trigger an update of all conditions for entry in itervalues(self._entries): entry.cond_obj.new_setups(self._setups) entry.cond_obj.update(time, self._keydict) self._check_state(entry, time) # update everyone els self._publish_config() self.log.info('new setups list: %s', ', '.join(self._setups)) def _check_state(self, entry, time): """Check if the state of this entry changed and we need to emit a warning or a clear it. """ eid = entry.id if entry.cond_obj.is_expired(time): if eid not in self._warnings or self._warnings[eid][0]: self._emit_expired_warning(entry) elif entry.cond_obj.triggered: if eid not in self._warnings or not self._warnings[eid][0]: self._emit_warning(entry) else: if eid in self._warnings: self._clear_warning(entry) self.log.debug('new conditions: %s', self._warnings) def _emit_warning(self, entry): """Emit a warning that this condition is now triggered.""" self.log.info('got a new warning for %r', entry) warning_desc = strftime('%Y-%m-%d %H:%M') + ' -- ' + entry.message if entry.action: warning_desc += ' -- executing %r' % entry.action if entry.scriptaction == 'pausecount': self._pausecount[entry.id] = entry.message self._update_pausecount_str() warning_desc += ' -- counting paused' elif entry.scriptaction: self._put_message('scriptaction', entry, (entry.scriptaction, entry.message)) if entry.type: for notifier in self._notifiers[entry.type]: notifier.send('New warning from NICOS', warning_desc) self._put_message('warning', entry, entry.message) self._warnings[entry.id] = (True, warning_desc) self._update_warnings_str() if entry.action: self._put_message('action', entry, entry.action) self._spawn_action(entry.action) def _emit_expired_warning(self, entry): """Emit a warning that this condition has missing information.""" self.log.info('value(s) missing for %r', entry) warning_desc = (strftime('%Y-%m-%d %H:%M') + ' -- current value(s) missing, ' 'cannot check condition %r' % entry.condition) # warn that we cannot check the condition anymore if entry.type: for notifier in self._notifiers[entry.type]: notifier.send('New warning from NICOS', warning_desc) self._put_message('warning', entry, warning_desc) self._warnings[entry.id] = (False, warning_desc) self._update_warnings_str() def _clear_warning(self, entry): """Clear a previously emitted warning for this condition.""" self.log.info('condition %r normal again', entry) was_real_warning = self._warnings.pop(entry.id)[0] if entry.scriptaction == 'pausecount': self._pausecount.pop(entry.id, None) self._update_pausecount_str() self._update_warnings_str() if was_real_warning: self._put_message('resolved', entry, '') if entry.okmessage and entry.type: msg = strftime('%Y-%m-%d %H:%M -- ') msg += '%s\n\nWarning was: %s' % (entry.okmessage, entry.message) for notifier in self._notifiers[entry.type]: notifier.send('NICOS warning resolved', msg) if entry.okaction: self._put_message('action', entry, entry.okaction) self._spawn_action(entry.okaction) # internal helper methods def _put_message(self, msgtype, entry, message): if entry is not None: message = [currenttime(), message, entry.id] self._queue.put('watchdog/%s%s%s\n' % (msgtype, OP_TELL, cache_dump(message))) def _update_mailreceivers(self, emails): self.log.info('updating any Mailer receivers to %s', emails) for notifier in self._all_notifiers: if isinstance(notifier, Mailer): # we're in slave mode, so _setROParam is necessary to set params notifier._setROParam('receivers', emails) def _update_warnings_str(self): self._put_message('warnings', None, '\n'.join(v[1] for v in self._warnings.values())) def _update_pausecount_str(self): self._put_message('pausecount', None, ', '.join(self._pausecount.values())) def _spawn_action(self, action): self.log.warning('will execute action %r', action) script = path.join(config.nicos_root, 'bin', 'nicos-script') createSubprocess([ sys.executable, script, '-M', # start in maintenance mode '-S', '60', # abort after 60 seconds '-A', 'watchdog-action', # appname for the logfiles ','.join(self._setups), # setups to load action ]) # code to execute
class SatBox(HasTimeout, Moveable): """ Device Object for PANDA's Attenuator controlled by a WUT-device via a ModBusTCP interface via a ModBus TACO Server. """ valuetype = int attached_devices = { 'input': Attach('Endswitch input', Readable), 'output': Attach('Output', Moveable), } parameters = { 'blades': Param('Thickness of the blades, starting with lowest bit', type=listof(floatrange(0, 1000)), mandatory=True), 'readout': Param('Determine blade state from output or from switches', type=oneof('switches', 'outputs'), mandatory=True, default='output', chatty=True), } parameter_overrides = { 'timeout': Override(default=5), } def _readOutputs(self, maxage=0): # just read back the OUTPUT values return bits(self._attached_output.read(), len(self.blades)) def _readSwitches(self): # deduce blade state from switches state = bits(self._attached_input.read(), len(self.blades) * 2) realstate = [] for i in range(0, len(state), 2): bladestate = state[i:i + 2] if bladestate == (0, 1): realstate.append(1) elif bladestate == (1, 0): realstate.append(0) else: realstate.append(None) return tuple(realstate) def doRead(self, maxage=0): bladestate = self._readSwitches() if self.readout == 'switches' else \ self._readOutputs() # only sum up blades which are used for sure (0/None->ignore) return sum(b * r for b, r in zip(self.blades, bladestate) if r) def doStatus(self, maxage=0): if self.readout == 'outputs': return status.OK, '' if self._readSwitches() == self._readOutputs(): return status.OK, '' return status.BUSY, 'moving' def doStart(self, rpos): if rpos > sum(self.blades): raise InvalidValueError( self, 'Value %d too big!, maximum is %d' % (rpos, sum(self.blades))) which = 0 pos = rpos # start with biggest blade and work downwards, ignoring disabled blades for bladewidth in reversed(self.blades): if bladewidth and pos >= bladewidth: which |= 1 << self.blades.index(bladewidth) pos -= bladewidth if pos != 0: self.log.warning('Value %d impossible, trying %d instead!', rpos, rpos + 1) return self.start(rpos + 1) self._attached_output.move(which) if self.readout == 'output': # if we have no readback, give blades time to react session.delay(1) def doIsAllowed(self, target): if not (0 <= target <= sum(self.blades)): return False, 'Value outside range 0..%d' % sum(self.blades) return True, ''
class Chopper(Moveable): """Switcher for the TOF setting. This controls the chopper phase and opening angle, as well as the TOF slice settings for the detector. Presets depend on the target wavelength as well as the detector position. """ hardware_access = False attached_devices = { 'selector': Attach('Selector preset switcher', SelectorSwitcher), 'det_pos': Attach('Detector preset switcher', DetectorPosSwitcherMixin), 'daq': Attach('KWSDetector device', KWSDetector), 'params': Attach('Chopper param device', Moveable), } parameters = { 'resolutions': Param('Possible values for the resolution', unit='%', type=listof(float), mandatory=True), 'channels': Param('Desired number of TOF channels', # TODO: max channels? type=intrange(1, 1024), default=64, settable=True), 'shade': Param('Desired overlap of spectrum edges', type=floatrange(0.0, 1.0), default=0.0, settable=True, category='general'), 'tauoffset': Param('Additional offset for time of flight', type=floatrange(0.0), default=0.0, settable=True, category='general'), 'nmax': Param('Maximum number of acquisition frames', type=intrange(1, 128), default=25, settable=True), 'calcresult': Param('Last calculated setting', type=tupleof(float, float, int), settable=True, internal=True), } parameter_overrides = { 'unit': Override(default='', mandatory=False), } def doInit(self, mode): self.valuetype = oneof('off', 'manual', *('%.1f%%' % v for v in self.resolutions)) def _getWaiters(self): return [self._attached_params] def doUpdateChannels(self, value): # invalidate last calculated result when changing these parameters if self._mode == MASTER and 'channels' in self._params: self.calcresult = (0, 0, 0) def doUpdateShade(self, value): if self._mode == MASTER and 'shade' in self._params: self.calcresult = (0, 0, 0) def doUpdateTauoffset(self, value): if self._mode == MASTER and 'tauoffset' in self._params: self.calcresult = (0, 0, 0) def doUpdateNmax(self, value): if self._mode == MASTER and 'nmax' in self._params: self.calcresult = (0, 0, 0) def doStart(self, value): if value == 'off': if self._attached_daq.mode == 'tof': # don't touch realtime self._attached_daq.mode = 'standard' self._attached_params.start((0, 0)) return elif value == 'manual': if self._attached_daq.mode == 'standard': self._attached_daq.mode = 'tof' return reso = float(value.strip('%')) / 100.0 sel_target = self._attached_selector.target det_target = self._attached_det_pos.target try: lam = self._attached_selector.presets[sel_target]['lam'] spread = self._attached_selector.presets[sel_target]['spread'] det_z = self._attached_det_pos.presets[sel_target][det_target]['z'] det_offset = self._attached_det_pos.offsets[det_z] except KeyError: raise PositionError(self, 'cannot calculate chopper settings: ' 'selector or detector device not at preset') self.log.debug('chopper calc inputs: reso=%f, lam=%f, spread=%f, ' 'det_z=%f', reso, lam, spread, det_z) freq, opening = calculate(lam, spread, reso, self.shade, 20.0 + det_z + det_offset, self.tauoffset, self.nmax) self.log.debug('calculated chopper settings: freq=%f, opening=%f', freq, opening) interval = int(1000000.0 / (freq * self.channels)) self.calcresult = freq, opening, interval self._attached_params.start((freq, opening)) self._attached_daq.mode = 'tof' self._attached_daq.tofchannels = self.channels self._attached_daq.tofinterval = interval self._attached_daq.tofprogression = 1.0 # linear channel widths self._attached_daq.tofcustom = [] # no custom channels def doRead(self, maxage=0): if self.target == 'manual': return 'manual' params = self._attached_params.read(maxage) if params[0] < 1.0: return 'off' # TODO: take from isAtTarget if abs(params[0] - self.calcresult[0]) < self._attached_params._attached_freq1.precision and \ abs(params[1] - self.calcresult[1]) < self._attached_params._attached_phase1.precision: if self._attached_daq.mode == 'tof' and \ self._attached_daq.tofchannels == self.channels and \ self._attached_daq.tofinterval == self.calcresult[2] and \ self._attached_daq.tofprogression == 1.0 and \ self._attached_daq.tofcustom == []: return self.target return 'unknown' def doStatus(self, maxage=0): move_status = self._attached_params.status(maxage) if move_status[0] not in (status.OK, status.WARN): return move_status r = self.read(maxage) if r == 'unknown': return (status.NOTREACHED, 'unconfigured chopper frequency/phase ' 'or still moving') return status.OK, ''
class Notifier(Device): """Base class for all notification systems. This class has a general notion of a "receiver" as an entity that notifications can be sent to. If this is an address, a channel name, or something else, is determined by the specific subclass. Do not use directly. """ parameters = { 'minruntime': Param( 'Minimum runtime of a command before a failure ' 'is sent over the notifier', type=float, unit='s', default=300, settable=True), 'ratelimit': Param('Minimum time between sending two notifications', default=60, type=floatrange(30), unit='s', settable=True), 'private': Param('True if notifier receivers are private, ' 'not user-settable', type=bool, default=False), 'receivers': Param('Receiver addresses', type=listof(str), settable=True), 'copies': Param( 'Addresses that get a copy of messages, a list of ' 'tuples: (address, messagelevel: "all" or ' '"important")', type=listof(tupleof(str, oneof('all', 'important'))), settable=True), 'subject': Param('Subject prefix', type=str, default='NICOS'), } parameter_overrides = { 'lowlevel': Override(default=True, mandatory=False), } _lastsent = 0 def _checkRateLimit(self): """Implement rate limiting.""" ct = currenttime() if ct - self._lastsent < self.ratelimit: self.log.info('rate limiting: not sending notification') return False self._lastsent = ct return True @usermethod def send(self, subject, body, what=None, short=None, important=True): """Send a notification.""" raise NotImplementedError('send() must be implemented in subclasses') @usermethod def sendConditionally(self, runtime, subject, body, what=None, short=None, important=True): """Send a notification if the given runtime is large enough.""" if runtime > self.minruntime: self.send(subject, body, what, short, important) def reset(self): """Reset experiment-specific configuration. Does nothing by default.""" def doWriteReceivers(self, value): if self.private and not session.checkAccess(ADMIN): raise AccessError( self, 'Only admins can change the receiver list ' 'for this notifier') return value def doWriteCopies(self, value): if self.private and not session.checkAccess(ADMIN): raise AccessError( self, 'Only admins can change the receiver list ' 'for this notifier') return value def _getAllRecipients(self, important): receivers = list(self.receivers) receivers.extend(addr for (addr, level) in self.copies if level == 'all' or important) return receivers
class CacheKafkaForwarder(ForwarderBase, Device): parameters = { 'brokers': Param('List of kafka hosts to be connected', type=listof(host(defaultport=9092)), mandatory=True, preinit=True, userparam=False ), 'output_topic': Param('The topic to send data to', type=str, userparam=False, settable=False, mandatory=True, ), 'dev_ignore': Param('Devices to ignore; if empty, all devices are ' 'accepted', default=[], type=listof(str), ), 'update_interval': Param('Time interval (in secs.) to send regular updates', default=10.0, type=float, settable=False) } parameter_overrides = { # Key filters are irrelevant for this collector 'keyfilters': Override(default=[], settable=False), } def doInit(self, mode): self._dev_to_value_cache = {} self._dev_to_status_cache = {} self._dev_to_timestamp_cache = {} self._producer = None self._lock = Lock() self._initFilters() self._queue = queue.Queue(1000) self._worker = createThread('cache_to_kafka', self._processQueue, start=False) self._regular_update_worker = createThread( 'send_regular_updates', self._poll_updates, start=False) while not self._producer: try: self._producer = \ KafkaProducer(bootstrap_servers=self._config['brokers']) except Exception as error: self.log.error( 'Could not connect to Kafka - will try again soon: %s', error) time.sleep(5) self.log.info('Connected to Kafka brokers %s', self._config['brokers']) def _startWorker(self): self._worker.start() self._regular_update_worker.start() def _poll_updates(self): while True: with self._lock: for dev_name in set(self._dev_to_value_cache.keys()).union( self._dev_to_status_cache.keys()): if self._relevant_properties_available(dev_name): self._push_to_queue( dev_name, self._dev_to_value_cache[dev_name], self._dev_to_status_cache[dev_name], self._dev_to_timestamp_cache[dev_name]) time.sleep(self.update_interval) def _checkKey(self, key): if key.endswith('/value') or key.endswith('/status'): return True return False def _checkDevice(self, name): if name not in self.dev_ignore: return True return False def _putChange(self, timestamp, ttl, key, value): if value is None: return dev_name = key[0:key.index('/')] if not self._checkKey(key) or not self._checkDevice(dev_name): return self.log.debug('_putChange %s %s %s', key, value, time) with self._lock: if key.endswith('value'): self._dev_to_value_cache[dev_name] = value else: self._dev_to_status_cache[dev_name] = convert_status(value) timestamp_ns = int(float(timestamp) * 10 ** 9) self._dev_to_timestamp_cache[dev_name] = timestamp_ns # Don't send until have at least one reading for both value and status if self._relevant_properties_available(dev_name): self._push_to_queue( dev_name, self._dev_to_value_cache[dev_name], self._dev_to_status_cache[dev_name], timestamp_ns,) def _relevant_properties_available(self, dev_name): return dev_name in self._dev_to_value_cache \ and dev_name in self._dev_to_status_cache \ and dev_name in self._dev_to_timestamp_cache def _push_to_queue(self, dev_name, value, status, timestamp): try: self._queue.put_nowait((dev_name, value, status, timestamp)) except queue.Full: self.log.error('Queue full, so discarding older value(s)') self._queue.get() self._queue.put((dev_name, value, status, timestamp)) self._queue.task_done() def _processQueue(self): while True: name, value, status, timestamp = self._queue.get() try: # Convert value from string to correct type value = cache_load(value) if not isinstance(value, str): # Policy decision: don't send strings via f142 buffer = to_f142(name, value, status, timestamp) self._send_to_kafka(buffer) except Exception as error: self.log.error('Could not forward data: %s', error) self._queue.task_done() def doShutdown(self): self._producer.close() def _send_to_kafka(self, buffer): self._producer.send(self.output_topic, buffer) self._producer.flush(timeout=3)
class SXTalSample(Sample): parameters = { 'a': Param('a', type=float, category='sample', settable=True), 'b': Param('b', type=float, category='sample', settable=True), 'c': Param('c', type=float, category='sample', settable=True), 'alpha': Param('alpha', type=floatrange(1., 179.), category='sample', settable=True), 'beta': Param('beta', type=floatrange(1., 179.), category='sample', settable=True), 'gamma': Param('gamma', type=floatrange(1., 179.), category='sample', settable=True), 'ubmatrix': Param('UB matrix', type=listof(float), category='sample', settable=True, userparam=True), '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'), 'reflist': Param('The name of the default reflection ' 'list to operate upon', type=str, userparam=True), } attached_devices = { 'reflists': Attach('List of available reflection lists', devclass=ReflexList, multiple=True, optional=False), } def clear(self): """Clear experiment-specific information.""" Sample.clear(self) self.ubmatrix = None for rfl in self._attached_reflists: rfl.clear() def new(self, parameters): self.clear() # pylint: disable=pointless-string-statement """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]') self.a = parameters.pop('a', None) if self.a is None: if 'cell' not in parameters: self.log.warning('using dummy lattice constant of 5 A') self.a = 5.0 self.b = parameters.pop('b', self.a) self.c = parameters.pop('c', self.a) self.alpha = parameters.pop('alpha', 90.0) self.beta = parameters.pop('beta', 90.0) self.gamma = parameters.pop('gamma', 90.0) self.bravais = parameters.pop('bravais', 'P') self.laue = parameters.pop('laue', '1') Sample.new(self, parameters) def _applyParams(self, number, parameters): Sample._applyParams(self, number, parameters) def doWriteUbmatrix(self, ub): if not ub: return if not isinstance(ub, list): raise ValueError('Expecting list of 9 values') if len(ub) != 9: raise ValueError('Expected 9 values') def doWriteReflist(self, name): if name not in self._adevs: raise InvalidValueError('% is no known reflection list' % name) if not isinstance(self._adevs[name], ReflexList): raise InvalidValueError('%s is not a reflection list' % name) def getCell(self): return Cell(self.a, self.b, self.c, self.alpha, self.beta, self.gamma) def getUB(self): if self.ubmatrix: ub = np.array(self.ubmatrix, dtype='float64') ub = ub.reshape((3, 3)) return ub # Return B instead when no UB available cell = self.getCell() ub = calculateBMatrix(cell) if isinstance(session.experiment, TASSXTal): return ub else: return ub / (2. * np.pi) def getRefList(self, name=None): if not name: name = self.reflist for reflist in self._attached_reflists: if reflist.name == name: return reflist
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 NiagShutter(HasTimeout, MappedMoveable): """ Shutter device for the NIAG beam lines. The shutter devices operate using 4 digital IOs (2 outputs and 2 inputs): - The digital outputs are used to open and close the shutter by sending a pulse - The digital inputs indicate whether the shutter is opened, closed or in interstage (in the latter case, both inputs are 0) An additional digital input indicates whether the shutter is enabled Part of the code used for this class was copied or adapted from the MultiSwitcher class written by Georg Brandl and Enrico Faulhaber. """ # the 5 digital IOs (2 pulse outputs, 3 inputs) used to control the shutter attached_devices = { 'do_open': Attach('Output to open the shutter', Moveable), 'do_close': Attach('Output to close the shutter', Moveable), 'is_open': Attach('Input to check if shutter is open', Readable), 'is_closed': Attach('Input to check if shutter is closed', Readable), 'is_enabled': Attach('Input to check if shutter is enabled', Readable), } """ copied from the MultiSwitcher class """ parameters = { 'blockingmove': Param('Should we wait for the move to finish?', mandatory=False, default=True, settable=True, type=bool), } """ copied from the MultiSwitcher class """ parameter_overrides = { 'mapping': Override(description='Mapping of state names to N values ' 'to move the moveables to', type=dictof(anytype, listof(anytype))), 'fallback': Override(userparam=False, type=none_or(anytype), mandatory=False), } """ copied from the MultiSwitcher class """ hardware_access = False """ first value in target indicates whether to open the shutter, second value whether to close it """ def _startRaw(self, target): self._attached_do_open.start(target[0]) self._attached_do_close.start(target[1]) # returns a tuple mad of the (opened?, closed?) values def _readRaw(self, maxage=0): return tuple([ self._attached_is_open.read(maxage), self._attached_is_closed.read(maxage) ]) # based on the method definition from the MultiSwitcher class, simplified # because there is no need to define the precision for digital inputs def _mapReadValue(self, pos): """maps a tuple to one of the configured values""" for name, values in self.mapping.items(): if tuple(pos) == tuple(values): return name if self.fallback is not None: return self.fallback raise PositionError( self, 'unknown position of %s: %s' % (', '.join(str(d) for d in self._adevs), ', '.join( d.format(p) for (p, d) in zip(pos, self._adevs)))) # completion is checked by verifying the feedback values, because # the "motion" of the outputs (pulses) is finished before the shutter # reaches its final position def doIsCompleted(self): reached = self.doRead() == self.target if reached: return True else: # if the position was not reached, consider that the motion # is not complete, unless an error is present """ stat = self.doStatus() return stat[0] < status.BUSY # based on the definition of the MultiSwitcher class, extended for the # needs of the shutter control def doStatus(self, maxage=0): move_status = multiStatus(self._adevs, maxage) # if any of the underlying devices has an error, return it if move_status[0] > status.BUSY: return move_status try: # if the enable bit is not active, return "disabled" status if not self._attached_is_enabled.read(maxage): return status.DISABLED, 'disabled' r = self.doRead(maxage) # the the device is in the fallback position, it is considered # still moving if r == self.fallback: return status.BUSY, 'target not yet reached' return status.OK, 'idle' except PositionError as e: return status.NOTREACHED, str(e) # only allow to start if the enable bit is active def doStart(self, pos): if self._attached_is_enabled.read(): return MappedMoveable.doStart(self, pos) raise MoveError(self, 'Device is disabled') def doReset(self): multiReset(self._adevs)
class NicosDaemon(Device): """ This class abstracts the main daemon process. """ attached_devices = { 'authenticators': Attach('The authenticator devices to use for ' 'validating users and passwords', Authenticator, multiple=True), } parameters = { 'server': Param('Address to bind to (host or host[:port])', type=host(defaultport=DEFAULT_PORT), mandatory=True, ext_desc='The default port is ``1301``.'), 'servercls': Param('Server class used for creating transports ' 'to each client', type=str, mandatory=False, default='nicos.services.daemon.proto.classic.' 'Server'), 'serializercls': Param('Serializer class used for serializing ' 'messages transported from/to the server', type=str, mandatory=False, default='nicos.protocols.daemon.classic.' 'ClassicSerializer'), 'maxlogins': Param('Maximum number of simultaneous clients ' 'served', type=int, default=10), 'updateinterval': Param('Interval for watch expressions checking and' ' sending updates to the clients', type=float, unit='s', default=0.2), 'trustedhosts': Param('A list of trusted hosts allowed to log in', type=listof(str), ext_desc='An empty list means all hosts are ' 'allowed.'), 'simmode': Param('Whether to always start in dry run mode', type=bool), 'autosimulate': Param('Whether to simulate scripts when running them', type=bool, default=False) } def doInit(self, mode): # import server and serializer class servercls = importString(self.servercls) serialcls = importString(self.serializercls) self._stoprequest = False # the controller represents the internal script execution machinery if self.autosimulate and not config.sandbox_simulation: raise ConfigurationError('autosimulation configured but sandbox' ' deactivated') self._controller = ExecutionController(self.log, self.emit_event, 'startup', self.simmode, self.autosimulate) # cache log messages emitted so far self._messages = [] host, port = parseHostPort(self.server, DEFAULT_PORT) # create server (transport + serializer) self._server = servercls(self, (host, port), serialcls()) self._watch_worker = createThread('daemon watch monitor', self._watch_entry) def _watch_entry(self): """ This thread checks for watch value changes periodically and sends out events on changes. """ # pre-fetch attributes for speed ctlr, intv, emit, sleep = self._controller, self.updateinterval, \ self.emit_event, time.sleep lastwatch = {} while not self._stoprequest: sleep(intv) # new watch values? watch = ctlr.eval_watch_expressions() if watch != lastwatch: emit('watch', watch) lastwatch = watch def emit_event(self, event, data): """Emit an event to all handlers.""" self._server.emit(event, data) def emit_event_private(self, event, data): """Emit an event to only the calling handler.""" handler = self._controller.get_current_handler() if handler: self._server.emit(event, data, handler) def statusinfo(self): self.log.info('got SIGUSR2 - current stacktraces for each thread:') active = threading._active for tid, frame in listitems(sys._current_frames()): if tid in active: name = active[tid].getName() else: name = str(tid) self.log.info('%s: %s', name, ''.join(traceback.format_stack(frame))) def start(self): """Start the daemon's server.""" self.log.info('NICOS daemon v%s started, starting server on %s', nicos_version, self.server) # startup the script thread self._controller.start_script_thread() self._worker = createThread('daemon server', self._server.start, args=(self._long_loop_delay,)) def wait(self): while not self._stoprequest: time.sleep(self._long_loop_delay) self._worker.join() def quit(self, signum=None): self.log.info('quitting on signal %s...', signum) self._stoprequest = True self._server.stop() self._worker.join() self._server.close() def current_script(self): return self._controller.current_script def current_user(self): return getattr(self._controller.thread_data, 'user', system_user) def get_authenticators(self): return self._attached_authenticators
class Stargate(tango.DigitalOutput): """Device for controlling the MIRA-Stargate blocks.""" valuetype = listof(int) parameters = { 'offset_in': Param('Offset of digital input values', type=int, mandatory=True), 'offset_out': Param('Offset of digital output values', type=int, mandatory=True), 'chevron_att_angles': Param('att angle for shielding elements', type=listof(listof(int)), mandatory=True), } _started = 0 def doRead(self, maxage=0): words = self._dev.ReadOutputWords([self.offset_in, 5]) bitvals = [words[0], words[2], words[4]] chevrons = [] for bitval in bitvals: for _ in range(4): chevrons.append(int(bitval & 0b11 == 0b01)) bitval >>= 2 return chevrons[:11] def doStatus(self, maxage=0): if self._started and self._started + 3 > currenttime(): return status.BUSY, 'moving/waiting' return status.OK, '' def doStart(self, value): bitvals = [0, 0, 0] for curidx in range(len(value)): curval = value[curidx] byteidx = curidx // 4 bitidx = (curidx % 4) * 2 if curval: bitvals[byteidx] |= (1 << bitidx) else: bitvals[byteidx] |= (1 << (bitidx+1)) self._dev.WriteOutputWords([self.offset_out] + bitvals) self._started = currenttime() def doIsAllowed(self, value): if len(value) != 11: raise InvalidValueError(self, 'list must have 11 entries') # map everything to 0 or 1 value = [bool(v) for v in value] # check allowed positions if value == [True] * 11: # open everything is allowed return True, '' if sum(value) > 2: return False, 'cannot open more than 2 chevrons' if value[0] or value[10]: return False, 'cannot open first or last chevron' return True, '' def doReadFmtstr(self): return '[' + ', '.join(['%d'] * 11) + ']' def get_chevrons_for_att(self, att): chevrons = [] for curidx in range(len(self.chevron_att_angles)): maxmin = self.chevron_att_angles[curidx] if len(maxmin) < 2: chevrons.append(0) continue if maxmin[1] < att < maxmin[0]: chevrons.append(1) else: chevrons.append(0) return chevrons
class BoaTable(Device): parameters = { 'standard_devices': Param('Standard Devices', type=listof(nicosdev), userparam=False), 'setups': Param('setups on this table', type=listof(str), userparam=False, settable=True, default=[]), 'additional_devices': Param('additional devices attached ' 'to the table', type=listof(nicosdev), default=[], settable=True, userparam=False), } def doInit(self, mode): self.hardware_access = False @usermethod def addSetup(self, name): if name not in session.loaded_setups: raise ValueError('%s not a loaded setup' % name) if name not in self.setups: tmp = list(self.setups) tmp.append(name) self.setups = tmp @usermethod def removeSetup(self, name): if name in self.setups: tmp = list(self.setups) tmp.remove(name) self.setups = tmp @usermethod def addDevice(self, name): if name not in session.configured_devices: raise ValueError('device %s is not available' % name) if name not in self.additional_devices: tmp = list(self.additional_devices) tmp.append(name) self.additional_devices = tmp @usermethod def removeDevice(self, name): if name in self.additional_devices: tmp = list(self.additional_devices) tmp.remove(name) self.additional_devices = tmp @usermethod def show(self): txt = 'Table %s Configuration:\n' % self.doReadName() txt += 'Standard Devices:\n' txt += '\t %s\n' % ', '.join(self.standard_devices) txt += 'Setups:\n' txt += '\t%s\n' % ', '.join(self.setups) txt += 'Additional Devices\n' txt += '\t%s\n' % ', '.join(self.additional_devices) txt += 'Total Devices\n' txt += '\t%s\n' % ', '.join(self.getTableDevices()) session.log.info(txt) def getTableDevices(self): result = list( itertools.chain(self.standard_devices, self.additional_devices)) setupInfo = session.getSetupInfo() for setup in self.setups: info = setupInfo[setup] result = list(itertools.chain(result, info['devices'].keys())) return result
class Collimation(Moveable): """Controlling the collimation guides and slits together.""" hardware_access = False attached_devices = { 'guides': Attach('guides', Moveable), 'slits': Attach('slit devices', CollimationSlit, multiple=True), } parameters = { 'slitpos': Param('Positions of the attached slits', unit='m', type=listof(int), mandatory=True), 'mapping': Param('Maps position name to guide and slit w/h', type=dictof(str, tupleof(int, float, float)), mandatory=True), } parameter_overrides = { 'fmtstr': Override(default='%s'), 'unit': Override(mandatory=False, default=''), } def doInit(self, mode): self.valuetype = oneof(*sorted(self.mapping, key=num_sort)) if len(self._attached_slits) != len(self.slitpos): raise ConfigurationError( self, 'number of elements in slitpos ' 'parameter must match number of attached ' 'slit devices') def doRead(self, maxage=0): def matches(v1, v2): return abs(v1 - v2) < 1.0 guidelen = self._attached_guides.read(maxage) if guidelen not in self.slitpos: return 'unknown' slitvals = [slit.read(maxage) for slit in self._attached_slits] for (posname, (pos_guidelen, pos_w, pos_h)) in self.mapping.items(): if pos_guidelen != guidelen: continue ok = True for (slitpos, slit, (w, h)) in zip(self.slitpos, self._attached_slits, slitvals): if slitpos == pos_guidelen: ok &= matches(w, pos_w) and matches(h, pos_h) else: ok &= matches(w, slit.openpos[0]) and \ matches(h, slit.openpos[1]) if ok: return posname return 'unknown' def doStart(self, target): pos_guidelen, pos_w, pos_h = self.mapping[target] self._attached_guides.start(pos_guidelen) for (slitpos, slit) in zip(self.slitpos, self._attached_slits): if slitpos == pos_guidelen: slit.start((pos_w, pos_h)) else: slit.start(slit.openpos)
class Image(BaseChannel, QMesyDAQImage): """Channel for QMesyDAQ that returns the last image.""" parameters = { 'readout': Param('Readout mode of the Detector', settable=True, type=oneof('raw', 'mapped', 'amplitude'), default='mapped', mandatory=False, chatty=True), 'flipaxes': Param('Flip data along these axes after reading from det', type=listof(int), default=[], unit=''), } taco_class = Detector def doInit(self, mode): if mode == MASTER: self.readArray(FINAL) # also set arraydesc def doStart(self): self.readresult = [0] BaseChannel.doStart(self) def doRead(self, maxage=0): return self.readresult def doReadArray(self, quality): # read data via taco and transform it res = self._taco_guard(self._dev.read) # first 3 values are sizes of dimensions # evaluate shape return correctly reshaped numpy array if (res[1], res[2]) in [(1, 1), (0, 1), (1, 0), (0, 0)]: # 1D array self.arraydesc = ArrayDesc('data', shape=(res[0], ), dtype='<u4') data = np.fromiter(res[3:], '<u4', res[0]) self.readresult = [data.sum()] elif res[2] in [0, 1]: # 2D array self.arraydesc = ArrayDesc('data', shape=(res[0], res[1]), dtype='<u4') data = np.fromiter(res[3:], '<u4', res[0] * res[1]) self.readresult = [data.sum()] data = data.reshape((res[0], res[1]), order='C') else: # 3D array self.arraydesc = ArrayDesc('data', shape=(res[0], res[1], res[2]), dtype='<u4') data = np.fromiter(res[3:], '<u4', res[0] * res[1] * res[3]) self.readresult = [data.sum()] data = data.reshape((res[0], res[1], res[2]), order='C') for axis in self.flipaxes: data = np.flip(data, axis) return data def doReadIsmaster(self): return False def doWriteListmode(self, value): self._taco_update_resource('writelistmode', '%s' % value) return self._taco_guard(self._dev.deviceQueryResource, 'writelistmode') def doWriteHistogram(self, value): self._taco_update_resource('writehistogram', '%s' % value) return self._taco_guard(self._dev.deviceQueryResource, 'writehistogram') def doWriteReadout(self, value): self._taco_update_resource('histogram', '%s' % value) return self._taco_guard(self._dev.deviceQueryResource, 'histogram') def doWriteListmodefile(self, value): self._taco_update_resource('lastlistfile', '%s' % value) return self._taco_guard(self._dev.deviceQueryResource, 'lastlistfile') def doReadConfigfile(self): return self._taco_guard(self._dev.deviceQueryResource, 'configfile') def doReadCalibrationfile(self): return self._taco_guard(self._dev.deviceQueryResource, 'calibrationfile') # def doReadListmodefile(self): # return self._taco_guard(self._dev.deviceQueryResource, 'lastlistfile') def doWriteHistogramfile(self, value): self._taco_update_resource('lasthistfile', '%s' % value) return self._taco_guard(self._dev.deviceQueryResource, 'lasthistfile')
class CharmPowerSupply(StringIO, Readable): parameters = { 'transitions': Param('transition choices as per entangle device configuration', type=listof(str)), } parameter_overrides = { 'pollinterval': Override(default=5), # every 5 seconds } # pylint: disable=unused-argument # pylint: disable=C:invalid-name def doInit(self, mode): n_items = 0 x = super().status() if x[0] == status.OK: n_items = self.availablelines delays = [] cmds = [] availabletransitions = [] for i in range(n_items): delays.append(0) cmd = "TR" + str(i) cmds.append(cmd) availabletransitions = self.multiCommunicate((delays, cmds)) self._setROParam('transitions', availabletransitions) print(self.transitions) # pylint: disable=C:invalid-name def doShutdown(self): pass def read(self, maxage=0): s = super().status(maxage) sv = self.status_value(s[1]) return sv[1] # pylint: disable=invalid-name def status_value(self, s1): val = '' sta = '' i = s1.rfind('[') if i >= 0: val = s1[i:] sta = s1[:i - 1] self._cache.put(self, 'value', val, currenttime(), self.maxage) return (sta, val) def doRead(self, maxage=0): return self.read(maxage) @usermethod def apply(self, *argv): """ applies a transition from the 'transitions' parameter. Function argument is index to item in the list. """ if len(argv) == 0: index = -1 else: index = argv[0] if index == -1: msg = '' for i in range(len(self.transitions)): if i: msg += " ," msg += self.transitions[i] + " (" + str(i) + ")" print(msg) print('please use 0 based index as parameter. No action taken.') else: if ((len(self.transitions) > index) and (index >= 0)): cmd = 'APPLY:' + self.transitions[index] print(cmd) self.write(cmd) print(self.transitions[index]) else: print('index out of range. No action taken.')
class MultiCounter(BaseChannel, PassiveChannel): """Channel for QMesyDAQ that allows to access selected channels in a multi-channel setup. """ parameters = { 'channels': Param('Tuple of active channels (1 based)', settable=True, type=listof(int)), } taco_class = Detector def doRead(self, maxage=0): if self._mode == SIMULATION: res = [0] * (max(self.channels) + 3) else: # read data via taco and transform it res = self._taco_guard(self._dev.read) expected = 3 + max(self.channels or [0]) # first 3 values are sizes of dimensions if len(res) >= expected: data = res[3:] # ch is 1 based, data is 0 based total = sum([data[ch - 1] for ch in self.channels]) else: self.log.warning( 'not enough data returned, check config! ' '(got %d elements, expected >=%d)', len(res), expected) data = None total = 0 resultlist = [total] if data is not None: for ch in self.channels: # ch is 1 based, _data is 0 based resultlist.append(data[ch - 1]) return resultlist def valueInfo(self): resultlist = [ Value('ch.sum', unit='cts', errors='sqrt', type='counter', fmtstr='%d') ] for ch in self.channels: resultlist.append( Value('ch%d' % ch, unit='cts', errors='sqrt', type='counter', fmtstr='%d')) return tuple(resultlist) def doReadIsmaster(self): return False def doReadFmtstr(self): resultlist = ['sum %d'] for ch in self.channels: resultlist.append('ch%d %%d' % ch) return ', '.join(resultlist)
class Sans1HV(BaseSequencer): valuetype = float attached_devices = { 'supply': Attach('NICOS Device for the highvoltage supply', Moveable), 'discharger': Attach('Switch to activate the discharge resistors', Moveable), 'interlock': Attach('Assume IDLE if set and Target is set to 0', Readable), } parameters = { 'ramp': Param('Current ramp speed (volt per minute)', type=int, unit='main/min', settable=True, volatile=True), 'lasthv': Param('When was hv applied last (timestamp)', type=float, internal=True, default=0.0, mandatory=False, settable=False), 'maxofftime': Param('Maximum allowed Off-time for fast ramp-up', type=int, unit='s', default=4 * 3600), 'slowramp': Param('Slow ramp-up speed (volt per minute)', type=int, unit='main/min', default=120), 'fastramp': Param('Fast ramp-up speed (volt per minute)', type=int, unit='main/min', default=1200), 'rampsteps': Param('Cold-ramp-up sequence (voltage, stabilize_minutes)', type=listof(tupleof(int, int)), unit='', default=[(70, 3), (300, 3), (500, 3), (800, 3), (1100, 3), (1400, 3), (1500, 10)]), } parameter_overrides = { 'abslimits': Override(default=(0, 1500), mandatory=False), 'unit': Override(default='V', mandatory=False, settable=False), } def _generateSequence(self, target): hvdev = self._attached_supply disdev = self._attached_discharger seq = [SeqMethod(hvdev, 'stop'), SeqMethod(hvdev, 'wait')] now = currenttime() # below first rampstep is treated as poweroff if target <= self.rampsteps[0][0]: # fast ramp seq.append(SeqParam(hvdev, 'ramp', self.fastramp)) seq.append(SeqDev(disdev, 1 if self.read() > target else 0)) if self.read() > self.rampsteps[0][0]: seq.append(SeqDev(hvdev, self.rampsteps[0][0])) seq.append(SeqMethod(hvdev, 'start', target)) return seq # check off time if self.lasthv and now - self.lasthv <= self.maxofftime: # short ramp up sequence seq.append(SeqParam(hvdev, 'ramp', self.fastramp)) seq.append(SeqDev(disdev, 1 if self.read() > target else 0)) seq.append(SeqDev(hvdev, target)) # retry if target not reached seq.append(SeqMethod(hvdev, 'start', target)) return seq # long sequence self.log.warning( 'Voltage was down for more than %.2g hours, ' 'ramping up slowly, be patient!', self.maxofftime / 3600) self.log.info( 'Voltage will be ready around %s', strftime('%X', localtime(now + self.doTime(self.doRead(0), target)))) seq.append(SeqParam(hvdev, 'ramp', self.slowramp)) seq.append(SeqDev(disdev, 0)) for voltage, minutes in self.rampsteps: # check for last point in sequence if target <= voltage: seq.append(SeqDev(hvdev, target)) seq.append( SeqSleep(minutes * 60, 'Stabilizing HV for %d minutes' % minutes)) break else: # append seq.append(SeqDev(hvdev, voltage)) seq.append( SeqSleep(minutes * 60, 'Stabilizing HV for %d minutes' % minutes)) seq.append(SeqDev(hvdev, target)) # be sure... seq.append(SeqMethod(hvdev, 'poll')) # force a read return seq def _waitFailed(self, step, action, exc_info): # signal single retry of the action return True def _runFailed(self, step, action, exc_info): # signal single retry of the action return 1 def doRead(self, maxage=0): voltage = self._attached_supply.read(maxage) # everything below the last rampstep is no HV yet... # bugfix: avoid floating point rounding errors if voltage >= (self.rampsteps[-1][0] - 0.001): # just assigning does not work inside poller, but we want that! self._setROParam('lasthv', currenttime()) return voltage def doIsAllowed(self, target): return self._attached_supply.isAllowed(target) def doTime(self, pos, target): # duration is in minutes... duration = abs(pos - target) / float(self.fastramp) if not self.lasthv or (currenttime() - self.lasthv > self.maxofftime): # cold start fromVolts = 0 for volts, waiting in self.rampsteps: duration += waiting duration += (min(volts, target) - fromVolts) / float( self.slowramp) fromVolts = volts if volts >= target: break return duration * 60. # convenience stuff def doReadRamp(self): return self._attached_supply.ramp def doWriteRamp(self, value): self._attached_supply.ramp = value return self.doReadRamp()
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 int(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 PumaMultiDetectorLayout(CanReference, HasTimeout, BaseSequencer): """PUMA multidetector arrangement device. There are 11 detector/blades(collimator) combinations moving on a circle. The detector tubes are mounted vertically and the blade can be moved around the detector, where the pivot point is the center of the detector tube. """ _num_axes = 11 attached_devices = { 'rotdetector': Attach('Detector tube position devices', Moveable, multiple=_num_axes), 'rotguide': Attach('Detector guide devices', Moveable, multiple=_num_axes), # 'man': Attach('multi analyzer', Moveable), 'att': Attach('Coupled axes detector', Moveable), } valuetype = tupleof(*(float for i in range(2 * _num_axes))) parameters = { 'general_reset': Param('', type=bool, settable=False, default=False), 'raildistance': Param('', type=float, settable=False, default=20., unit='mm'), 'detectorradius': Param('', type=float, settable=False, default=761.9, unit='mm'), 'refgap': Param('Gap between detectors during the reference of the ' 'guides', type=floatrange(2.75, 4.1), settable=False, userparam=True, default=3.), 'gapoffset': Param('Minimum gap for the det 1 from reference position', type=float, settable=False, userparam=False, default=4.), 'parkpos': Param( 'Detector and guide positions to park the ' 'multidetector in a safe position', type=listof(float), settable=False, userparam=False, default=[ -14.9, -17.5, -20., -22.5, -25., -27.5, -30., -32.5, -35., -37.5, -40., 3.5, 2.75, 2.5, 2.25, 2.0, 1.75, 1.5, 1.25, 1.0, 0.75, 0.5 ]), 'tubediameter': Param('diameter of detector tubes', type=floatrange(0, None), unit='mm', category='general', default=25.4), 'psdchannelwidth': Param('PSD channel width', type=floatrange(0, None), category='general', unit='mm', prefercache=False, default=0.7), 'opmode': Param( 'Operation mode, either "multi" for multi analysis or ' ' "pa" for polarization analysis', type=oneof('multi', 'pa'), settable=True, default='multi'), } parameter_overrides = { 'timeout': Override(default=600.), 'unit': Override(mandatory=False, default='', settable=False), 'fmtstr': Override(volatile=True), } hardware_access = False threadstate = None D2R = math.pi / 180 R2D = 1. / D2R # [-100, -80, -60, -40, -20, 0, 20, 40, 60, 80, 100] hortranslation = range(-100, 101, 20) anglis = [2.28, 2.45, 2.38, 2.35, 2.30, 2.43, 2.37, 2.43, 2.32, 2.36] @hiddenusercommand def park(self, blocking=True): """Move device to the ``park`` position. The park position is given by the ``parkposition`` parameter. It generate ands starts a sequence if none is running. The call blocks the daemon execution if ``blocking`` is set to True. """ if self._seq_is_running(): if self._mode == SIMULATION: self._seq_thread.join() self._seq_thread = None else: raise MoveError( self, 'Cannot park device, sequence is still ' 'running (at %s)!' % self._seq_status[1]) self._startSequence([ SeqMethod(self, '_move_guides_to_zero_pos'), SeqMethod(self, '_move_detectors', self.parkpos), ] + [ SeqDev(d, p) for d, p in zip(self._rotguide1, self.parkpos[11:][::-1]) ]) if blocking: # block the move to be sure that the device has reached target # before it can be dismounted or the position sensitive detectors # can be used self.wait() def doInit(self, mode): self._rotdetector0 = self._attached_rotdetector self._rotdetector1 = self._rotdetector0[::-1] self._rotguide0 = self._attached_rotguide self._rotguide1 = self._rotguide0[::-1] def valueInfo(self): ret = [] for dev in self._attached_rotdetector + self._attached_rotguide: ret.extend(dev.valueInfo()) return tuple(ret) def doReadFmtstr(self): return ', '.join('%s = %s' % (v.name, v.fmtstr) for v in self.valueInfo()) def doIsAllowed(self, target): # check if requested position is allowed in principle why = [] for dev, pos in zip(self._rotdetector0 + self._rotguide0, target): ok, _why = dev.isAllowed(pos) if ok: self.log.debug('%s: requested position %.3f deg allowed', dev, pos) else: why.append( '%s: requested position %.3f deg out of limits; %s' % (dev, pos, _why)) if why: return False, '; '.join(why) # now check detector and guide rotation allowed within single limits # and sequence limits return self._sequentialAngleLimit(target) def _move_guides_to_zero_pos(self): """Move all guides to a position '0.'. Starting at the most left guide looking for the first guide where the position is >= 0 so the devices left from it may be moved without any problem to position 0. Repeat as long all none of the positions is < 0. The move all with positions > 0 to 0. """ self.log.debug('move all guides to zero position') while min(d.read(0) for d in self._rotguide0) < 0: for d in self._rotguide0: if d.read(0) >= 0: for d1 in self._rotguide1[self._rotguide0.index(d):]: d1.maw(0) self.log.debug('%r', [d.read(0) for d in self._rotguide0]) for d in self._rotguide0: if not d.isAtTarget(0): d.maw(0) # remove all remaining move commands on cards due to touching any limit # switch for d in self._rotguide0: d.stop() def _move_detectors(self, target): """Move detectors to their positions. The order of the movement is calculated to avoid clashes during the movement. """ self.log.debug('Collision sorting') nstart = np.zeros((self._num_axes, self._num_axes), dtype=np.int8) rotpos = [d.read(0) for d in self._rotdetector0] for i in range(self._num_axes): for j in range(i + 1, self._num_axes): if target[i] < rotpos[j] + 2.5: nstart[i][j] = 1 self.log.debug('Collision sorting done: %r', nstart) istart = [0] * self._num_axes for _k in range(self._num_axes): if 0 not in istart: break # ready = 1 # for i in range(self._num_axes): # if istart[i] == 0: # ready = 0 # if ready != 0: # break for i in range(self._num_axes): if istart[i] == 0: if sum(nstart[i][i + 1:self._num_axes]) == 0: istart[i] = 1 self.log.debug('Move detector #%d', i + 1) self._rotdetector0[i].move(target[i]) session.delay(2) for j in range(self._num_axes): nstart[j][i] = 0 self._hw_wait(self._rotdetector0) # remove all remaining move commands on cards due to touching any limit # switch for d in self._rotdetector0: d.stop() def _move_guides(self, target): """Move all guides to their final position. Starting with the most outer guides towards the inner ones all guides may started one after the other, since should have target positions increasing with their position numbers. """ for n in [10, 0, 9, 1, 8, 2, 7, 3, 6, 4, 5]: self._rotguide0[n].move(target[n + self._num_axes]) self.log.debug('Move guide #%d', n + 1) self._hw_wait(self._rotguide0) # remove all remaining move commands on cards due to touching any limit # switch for d in self._rotguide0: d.stop() def _generateSequence(self, target): """Move multidetector to correct scattering angle of multi analyzer. It takes account into the different origins of the analyzer blades. """ # check if requested positions already reached within precision if self.isAtTarget(target): self.log.debug('device already at position, nothing to do!') return [] return [ SeqMethod(self, '_move_guides_to_zero_pos'), # The detectors can be moved without any restriction. SeqMethod(self, '_move_detectors', target), SeqMethod(self, '_move_guides', target), ] def doRead(self, maxage=0): return [d.read(maxage) for d in self._rotdetector0 + self._rotguide0] def doReset(self): for dev in self._rotguide0 + self._rotdetector0: # one reset per card is sufficient, since the card will be reset # completely if dev.motor.addr in [71, 77, 83, 89]: dev.reset() def doReference(self, *args): # self.doReset() # remove all remaining move commands on cards due to touching # any limit switch self.stop() self.log.info('Compacting all elements') for d, g in zip(self._rotdetector0, self._rotguide0): self.log.debug('reference: %s, %s', d, g) self._reference_det_guide(d, g) session.delay(1.5) self.log.info('Spread elements to reference guides.') for i, (d, g) in enumerate(zip(self._rotdetector1, self._rotguide1)): d.userlimits = d.abslimits pos = d.read(0) d.move(self.gapoffset - (10 - i) * self.refgap) while abs(pos - d.read(0)) < (self.refgap - 0.2): session.delay(1) g.reference() self._hw_wait([d, g]) g.maw(0) self.log.info('referencing of guides is finished') def _reference_det_guide(self, det, guide): """Drive 'det' and 'guide' devices to references. The 'det' device will be moved to its reference position, whereas the 'guide' device will only moved to a position hitting the upper limit switch. So the 'det' and 'guide' devices are in a position all other pairs could be referenced too. If the 'guide' hits the upper limit switch and the 'det' is not at it's reference position it will be moved away in steps of 1 deg. """ not_ref_switch = 'low' if guide.motor.refswitch == 'high' else 'high' try: det.reference() self._hw_wait([det]) while guide.motor.isAtReference(): if guide.motor.isAtReference(not_ref_switch): self._clear_guide_reference(guide) while guide.motor.isAtReference(): self._step(guide) det.reference() self._hw_wait([det]) while guide.motor.isAtReference(): self._step(guide) while not guide.motor.isAtReference(): guide.reference() self._hw_wait([guide]) finally: pass def _step(self, guide, size=1): """Move device 'guide' a step 'size' away. The sign of the size gives the direction the value the distance. """ p = guide.motor.read(0) if not guide.motor.isAllowed(p + size)[0]: guide.motor.setPosition(p - size) session.delay(1) guide.motor.maw(p + size) def _clear_guide_reference(self, guide): """Move all guides right from the 'guide' to free the limit switch. If there is a guide in between free, then free only left from this one """ self.log.debug('clearing guide: %s', guide) while True: # find the first free guide from left side freeguides = self._rotguide1 free = None for g in self._rotguide0[self._rotguide0.index(guide) + 1:]: not_ref_sw = 'low' if g.motor.refswitch == 'high' else 'high' if not g.motor.isAtReference(not_ref_sw): free = g freeguides = self._rotguide1[self._rotguide1.index(g):] self.log.debug('found free guide: %s, %r', free, freeguides) break # Try to free all left from found free guide if there was one for g in freeguides: for d in freeguides: if d == g: break while d.motor.isAtReference(): self._step(d) self._step(d) self._step(g) # if the found free is not free any more try the next round if free and free.motor.isAtReference(): break if g == guide: return if free is None: self.log.warning( "Can't free the guide: %s. Please check " 'manually why!', guide) def _hw_wait(self, devices): loops = 0 final_exc = None devlist = devices[:] # make a 'real' copy of the list while devlist: loops += 1 for dev in devlist[:]: try: done = dev.doStatus(0)[0] except Exception: dev.log.exception('while waiting') final_exc = filterExceptions(sys.exc_info(), final_exc) # remove this device from the waiters - we might still # have its subdevices in the list so that _hw_wait() # should not return until everything is either OK or # ERROR devlist.remove(dev) if done == status.BUSY: # we found one busy dev, normally go to next iteration # until this one is done (saves going through the whole # list of devices and doing unnecessary HW communication) if loops % 10: break # every 10 loops, go through everything to get an accurate # display in the action line continue devlist.remove(dev) if devlist: session.delay(self._base_loop_delay) if final_exc: reraise(*final_exc) def _read_corr(self): """Read the physical unit of axis.""" readraw0 = self._read_raw() temp1 = temp0 = self._correctAnglesRead(readraw0[:self._num_axes]) self.log.debug('detector rotation corrected: %r', temp0) self.log.debug('detector guide rotation corrected: %r', temp1) return [temp0, temp1] def _read_raw(self): """Read the physical unit of axis.""" readraw0 = [d.read() for d in self._rotdetector0] readraw1 = [d.read() for d in self._rotguide0] self.log.debug('detector rotation raw: %r', readraw0) self.log.debug('detector guide rotation raw: %r', readraw1) return readraw0 + readraw1 def _printPos(self): out = [] for i, dev in enumerate(self._attached_rotdetector): out.append('detector rotation %2d: %7.2f %s' % (i, dev.read(), dev.unit)) for i, dev in enumerate(self._attached_rotguide): out.append('guide rotation %2d: %7.2f %s' % (i, dev.read(), dev.unit)) self.log.debug('%s', '\n'.join(out)) def doIsAtTarget(self, target): self._printPos() return self._checkPositionReached(target, 'raw') def _checkPositionReached(self, target, mode): """Check whether requested position is reached within some limit.""" self.log.debug('length of list: %d', 0 if target is None else len(target)) if not target: return False if mode == 'raw': pos = self._read_raw() elif mode == 'cor': pos = self._read_cor() else: self.log.warning('not a valid mode given; corrected values or raw ' 'values?') return False check = 0 reached = [] nonreached = [] precs = [d.precision for d in self._rotdetector0 + self._rotguide0] for i, (t, p, prec) in enumerate(zip(target, pos, precs)): self.log.debug('%s %s', t, p) if abs(t - p) <= prec: reached.append(i) check += 1 else: nonreached.append(i) self.log.debug('not reached: %s', nonreached) self.log.debug('reached : %s', reached) self.log.debug('check : %s', check) return check == len(target) def _readAnaTranslation(self): """Read the translation value of the individual analyzer blades. Needed for the calculation of real angles of detectors due to different origins """ # [-125, -105, -85, -65, -45, -25, 0, 25, 45, 65, 85, 105, 125] anatranslist1 = list(range(-125, -5, 20)) + [0] + \ list(range(25, 126, 20)) return anatranslist1 def _readZeroAna(self): """Read whether the whole analyser table is rotated. Needed for the calculation of real angles of detectors due to different origins """ self.anarot = self._attached_att.read() return self.anarot def _correctZeroAna(self, position): return position - self._readZeroAna() def _correctAnglesRead(self, pos): trans = self._readAnaTranslation() read1 = [] for i in range(len(self._rotdetector0)): angle = pos[i] * self.D2R read0 = (self.detectorradius + trans[i]) * math.tan(angle) # b read0 += self.hortranslation[i] # b corrected # b/a: a corrected detector radius temp = read0 / (self.detectorradius + trans[i]) temp = math.atan(temp) # calc angle radian temp = temp * self.R2D # convert to degrees read1.append(temp) # list append self.log.debug('corrected detector angles: %s', read1) return read1 def _correctAnglesMove(self, pos): trans = self._readAnaTranslation() read1 = [] for i in range(len(self._rotdetector0)): angle = pos[i] * self.D2R # b: without taking into account the horizontal shift read0 = math.tan(angle) * (self.detectorradius + trans[i]) read0 -= self.hortranslation[i] # b check if minus/plus? # b/a: a corrected detector radius temp = read0 / self.detectorradius temp = math.atan(temp) # calc angle radian temp = temp * self.R2D # convert to degrees read1.append(temp) # list append self.log.debug('corrected detector angles: %s', read1) return read1 def _checkLimitSwitches(self, what): lis = [] if what in ['det', 'all']: lis += self._rotdetector0 if what == ['guide', 'all']: lis += self._rotguide0 ref = sum(dev.motor.isAtReference() for dev in lis) return ref == len(lis) def _sequentialAngleLimit(self, pos): """Check individual movement ranges allowed for detector or guide.""" dtarget = pos[:self._num_axes] def is_reverse_sorted(l): l1, l2 = tee(l) next(l2, None) return all(a >= b for a, b in zip(l1, l2)) def is_sorted(l): l1, l2 = tee(l) next(l2, None) return all(a <= b for a, b in zip(l1, l2)) if not is_reverse_sorted(dtarget): return False, 'detector targets not a list of consecutive values' check = set(range(1, self._num_axes + 1)) allowed = check.copy() self.log.debug('position: %s', pos) self.log.debug('anglis: %s', self.anglis) why = [] for i in range(self._num_axes): self.log.debug('check position %s %s', i, pos[i]) if i == 0: if abs(dtarget[i] - dtarget[i + 1]) < self.anglis[0]: why.append('case 0: %s %s %s' % (dtarget[i], dtarget[i + 1], self.anglis[0])) allowed.discard(i + 1) elif i == 10: if abs(dtarget[i] - dtarget[i - 1]) < self.anglis[9]: why.append('case 10: %s %s %s' % (dtarget[i], dtarget[i - 1], self.anglis[9])) allowed.discard(i + 1) else: if abs(dtarget[i] - dtarget[i + 1]) < self.anglis[i] or \ abs(dtarget[i] - dtarget[i - 1]) < self.anglis[i]: why.append('%s %s %s %s' % (dtarget[i - 1], dtarget[i], dtarget[i + 1], self.anglis[i])) allowed.discard(i) self.log.debug('movement allowed for the following detectors: %s', ', '.join([str(i) for i in allowed])) notallowed = check - allowed if notallowed: self.log.warning( 'movement not allowed for the following ' 'detectors: %s', ', '.join([str(i) for i in notallowed])) return False, '; '.join(why) allowed = check.copy() gtarget = pos[self._num_axes:] if self.opmode == 'multi' and not is_sorted(gtarget): return False, 'detector guide targets not a list of consecutive ' \ 'values %s' % (gtarget,) first = 0 last = self._num_axes - 1 rg1_min = -7.5 if dtarget[first] < 4.2: rg1_min += 1.4 * (dtarget[first] - 4.2) rg11_max = 21.9 if dtarget[last] < -23.5: rg11_max += 1.4 * (23.5 + dtarget[last - 1]) if not (rg1_min <= gtarget[first] <= gtarget[first + 1]): why.append('rg1: %s %s %s' % (rg1_min, gtarget[first], gtarget[first + 1])) allowed.discard(first + 1) if not (gtarget[last - 1] <= gtarget[last] <= rg11_max): why.append('rg11: %s %s %s' % (gtarget[last - 1], gtarget[last], rg11_max)) allowed.discard(last + 1) if self.opmode == 'multi': for i in range(1, last): if not (gtarget[i - 1] <= gtarget[i] <= gtarget[i + 1]): why.append( 'rg%i: %s %s %s' % (i + 1, gtarget[i - 1], gtarget[i], gtarget[i + 1])) allowed.discard(i + 1) notallowed = check - allowed if notallowed: self.log.warning( 'movement not allowed for the following guides: ' '%s', ', '.join([str(i) for i in notallowed])) return False, why return True, ''
class MultiSwitcher(MappedMoveable): """The multi-switcher generalizes the `Switcher` so that for a state change multiple underlying moveable devices can be controlled. This is useful if you have for example two motors that only ever move to certain discrete positions for selected 'configurations', e.g. a monochromator changer. Then you can control both using :: move(changer_switch, 'up') move(changer_switch, 'down') instead of moving the axis to positions hard to understand or remember:: move(changer1, 14.55, changer2, 8.15) move(changer1, 51.39, changer2, 3.14) and still have the underlying continuously moveable devices available for debugging purposes. """ attached_devices = { 'moveables': Attach('The N (continuous) devices which are' ' controlled', Moveable, multiple=True), 'readables': Attach('0 to N (continuous) devices which are' ' used for read back only', Readable, optional=True, multiple=True), } parameters = { 'precision': Param('List of allowed deviations (1 or N) from target ' 'position, or None to disable', mandatory=True, type=none_or(listof(none_or(floatrange(0))))), 'blockingmove': Param('Should we wait for the move to finish?', mandatory=False, default=True, settable=True, type=bool), } parameter_overrides = { 'mapping': Override(description='Mapping of state names to N values ' 'to move the moveables to', type=dictof(anytype, listof(anytype))), 'fallback': Override(userparam=False, type=none_or(anytype), mandatory=False), } hardware_access = False @lazy_property def devices(self): return self._attached_moveables + self._attached_readables def doInit(self, mode): MappedMoveable.doInit(self, mode) for k, t in self.mapping.items(): if len(t) != len(self.devices): raise ConfigurationError(self, 'Switcher state entry for key ' '%r has different length than ' 'moveables list' % k) if self.precision: if len(self.precision) not in [1, len(self.devices)]: raise ConfigurationError(self, 'The precision list must either' ' contain only one element or have ' 'the same amount of elements as the ' 'moveables list') def _startRaw(self, target): """target is the raw value, i.e. a list of positions""" moveables = self._attached_moveables if not isinstance(target, (tuple, list)) or \ len(target) < len(moveables): raise InvalidValueError(self, 'doStart needs a tuple of %d ' 'positions for this device!' % len(moveables)) # only check and move the moveables, which are first in self.devices for d, t in zip(moveables, target): if not d.isAllowed(t): raise InvalidValueError(self, 'target value %r not accepted ' 'by device %s' % (t, d.name)) for d, t in zip(moveables, target): self.log.debug('moving %r to %r', d, t) d.start(t) if self.blockingmove: multiWait(moveables) def _readRaw(self, maxage=0): if self._mode == SIMULATION and self.target is not None: # In simulation mode the values of the readables are assumed to be # given in the mapping table for the current target return tuple(d.read(maxage) for d in self._attached_moveables) + \ tuple(self.mapping[self.target][len(self._attached_moveables):]) return tuple(d.read(maxage) for d in self.devices) def _mapReadValue(self, pos): """maps a tuple to one of the configured values""" hasprec = bool(self.precision) if hasprec: precisions = self.precision if len(precisions) == 1: precisions = [precisions[0]] * len(self.devices) for name, values in self.mapping.items(): if hasprec: for p, v, prec in zip(pos, values, precisions): if prec: if abs(p - v) > prec: break elif p != v: break else: # if there was no break we end here... return name else: if tuple(pos) == tuple(values): return name if self.fallback is not None: return self.fallback raise PositionError(self, 'unknown position of %s: %s' % ( ', '.join(str(d) for d in self.devices), ', '.join(d.format(p) for (p, d) in zip(pos, self.devices)))) def doStatus(self, maxage=0): # if the underlying device is moving or in error state, # reflect its status move_status = multiStatus(self.devices, maxage) if move_status[0] not in (status.OK, status.WARN): return move_status return MappedReadable.doStatus(self, maxage) def doReset(self): multiReset(self._adevs) def doStop(self): multiStop(self._adevs)
class PassiveChannel(Measurable): """Abstract base class for one channel of a aggregate detector. See the `Detector` documentation for an overview of channel types, and the difference between passive and active channels. Derived classes have to implement `doRead` and `valueInfo` methods. They can return an empty list and tuple, respectively, if the channel contributes no scalar values but one or more arrays, e.g. classes derived from `ImageChannelMixin`. """ parameters = { 'ismaster': Param('If this channel is an active master', type=bool, settable=True), 'presetaliases': Param( 'Aliases for setting a preset for the first ' 'scalar on this channel', type=listof(str)), } # preset handling is slightly different in the Detector def doSetPreset(self, **preset): raise UsageError(self, 'This channel cannot be used as a detector') # methods that can be overridden _presetmap = None def presetInfo(self): """Return the preset keys that this channel supports. The default implementation returns all time/monitor/counter/other value names from `self.valueInfo()`, with '.' replaced by '_' to form valid identifiers. It also adds entries from `self.presetaliases`. """ if self._presetmap is not None: return set(self._presetmap) self._presetmap = {} for (i, value) in enumerate(self.valueInfo()): if value.type in ('counter', 'monitor', 'time', 'other'): self._presetmap[value.name.replace('.', '_')] = i if self._presetmap: for alias in self.presetaliases: self._presetmap[alias] = 0 return set(self._presetmap) def setChannelPreset(self, name, value): """Set a preset for this channel. This can be ignored by passive channels since soft presets are checked explicitly. """ self.ismaster = True def presetReached(self, name, value, maxage): """Return true if the soft preset for *name* has reached the given *value*. """ if name in self._presetmap: return self.read(maxage)[self._presetmap[name]] >= value return False # methods to be implemented in concrete subclasses def doStart(self): pass def doFinish(self): pass def doStop(self): pass def doRead(self, maxage=0): raise NotImplementedError('implement doRead') def valueInfo(self): raise NotImplementedError('implement valueInfo') def doStatus(self, maxage=0): return status.OK, 'idle'
class JustBinItDetector(Detector): """ A "detector" that reads image data from just-bin-it. Note: it only uses image channels. """ parameters = { 'brokers': Param('List of kafka hosts to be connected', type=listof(host(defaultport=9092)), mandatory=True, preinit=True, userparam=False), 'command_topic': Param( 'The topic to send just-bin-it commands to', type=str, userparam=False, settable=False, mandatory=True, ), 'response_topic': Param( 'The topic where just-bin-it responses appear', type=str, userparam=False, settable=False, mandatory=True, ), 'ack_timeout': Param( 'How long to wait for timeout on acknowledgement', type=int, default=5, unit='s', userparam=False, settable=False, ), } parameter_overrides = { 'unit': Override(default='events', settable=False, mandatory=False), 'fmtstr': Override(default='%d'), 'liveinterval': Override(type=floatrange(0.5), default=1), } _last_live = 0 _presets = {} _presetkeys = {'t'} _ack_thread = None _exit_thread = False def doPreinit(self, mode): self._command_sender = kafka.KafkaProducer( bootstrap_servers=self.brokers) # Set up the response message consumer self._response_consumer = kafka.KafkaConsumer( bootstrap_servers=self.brokers) self._response_topic = kafka.TopicPartition(self.response_topic, 0) self._response_consumer.assign([self._response_topic]) self._response_consumer.seek_to_end() self.log.debug('Response topic consumer initial position = %s', self._response_consumer.position(self._response_topic)) def doInit(self, _mode): pass def doPrepare(self): for image_channel in self._attached_images: image_channel.doPrepare() def doStart(self): self._last_live = -(self.liveinterval or 0) # Generate a unique-ish id unique_id = 'nicos-{}-{}'.format(self.name, int(time.time())) self.log.debug('set unique id = %s', unique_id) count_interval = self._presets.get('t', None) config = self._create_config(count_interval, unique_id) if count_interval: self.log.info( 'Requesting just-bin-it to start counting for %s seconds', count_interval) else: self.log.info('Requesting just-bin-it to start counting') self._send_command(self.command_topic, json.dumps(config).encode()) # Tell the channels to start for image_channel in self._attached_images: image_channel.doStart() # Check for acknowledgement of the command being received self._exit_thread = False self._ack_thread = createThread("jbi-ack", self._check_for_ack, (unique_id, self.ack_timeout)) def _check_for_ack(self, identifier, timeout_duration): timeout = int(time.time()) + timeout_duration acknowledged = False while not (acknowledged or self._exit_thread): messages = self._response_consumer.poll(timeout_ms=50) responses = messages.get(self._response_topic, []) for records in responses: msg = json.loads(records.value) if 'msg_id' in msg and msg['msg_id'] == identifier: acknowledged = self._handle_message(msg) break # Check for timeout if not acknowledged and int(time.time()) > timeout: err_msg = 'Count aborted as no acknowledgement received from ' \ 'just-bin-it within timeout duration '\ f'({timeout_duration} seconds)' self.log.error(err_msg) break if not acknowledged: # Couldn't start histogramming, so stop the channels etc. self._stop_histogramming() for image_channel in self._attached_images: image_channel.doStop() def _handle_message(self, msg): if 'response' in msg and msg['response'] == 'ACK': self.log.info('Counting request acknowledged by just-bin-it') return True elif 'response' in msg and msg['response'] == 'ERR': self.log.error('just-bin-it could not start counting: %s', msg['message']) else: self.log.error( 'Unknown response message received from just-bin-it') return False def _send_command(self, topic, message): self._command_sender.send(topic, message) self._command_sender.flush() def _create_config(self, interval, identifier): histograms = [] for image_channel in self._attached_images: histograms.append(image_channel.get_configuration()) config_base = { 'cmd': 'config', 'msg_id': identifier, 'histograms': histograms } if interval: config_base['interval'] = interval else: # If no interval then start open-ended count config_base['start'] = int(time.time()) * 1000 return config_base def valueInfo(self): return tuple(info for channel in self._attached_images for info in channel.valueInfo()) def doRead(self, maxage=0): return [ data for channel in self._attached_images for data in channel.read(maxage) ] def doReadArrays(self, quality): return [image.doReadArray(quality) for image in self._attached_images] def doFinish(self): self._stop_ack_thread() self._stop_histogramming() def _stop_histogramming(self): self._send_command(self.command_topic, b'{"cmd": "stop"}') def doShutdown(self): self._response_consumer.close() def doSetPreset(self, **presets): self._presets = presets def doStop(self): self._stop_ack_thread() self._stop_histogramming() def _stop_ack_thread(self): if self._ack_thread and self._ack_thread.is_alive(): self._exit_thread = True self._ack_thread.join() self._ack_thread = None def doStatus(self, maxage=0): return multiStatus(self._attached_images, maxage) def doReset(self): pass def doInfo(self): return [ data for channel in self._attached_images for data in channel.doInfo() ] def duringMeasureHook(self, elapsed): if elapsed > self._last_live + self.liveinterval: self._last_live = elapsed return LIVE return None def arrayInfo(self): return tuple(image.arrayInfo() for image in self._attached_images)
class MagLock(Moveable): """Puma specific magnetic lock device.""" attached_devices = { 'magazine': Attach('The monochromator magazine', Moveable), 'io_open': Attach('readout for the status', Readable), 'io_closed': Attach('readout for the status', Readable), 'io_set': Attach('output to set', Moveable), } valuetype = oneof('open', 'closed') parameters = { 'states': Param('List of state names', type=listof(str), mandatory=True), } # parameters = { # 'values': Param('List of values to move to', type=listof(anytype), # mandatory=True), # 'io_values': Param('List of values to move to', type=listof(anytype), # mandatory=True), # 'precision': Param('Precision for comparison', mandatory=True), # } def _bitmask(self, num): return 1 << num def doStart(self, position): magpos = self._magpos if magpos not in [0, 1, 2, 3]: raise NicosError(self, 'depot at unknown position') if position == self.read(0): return if position == 'closed': self._attached_io_set.move(0) elif position == 'open': self._attached_io_set.move(self._bitmask(magpos)) else: self.log.info('Maglock: illegal input') return session.delay(2) # XXX! if self.read(0) != position: raise NicosError(self, 'maglock returned wrong position!') else: self.log.info('Maglock: %s', self.read(0)) def _read(self): """Return internal string repr. of the right sensing switches.""" magpos = self._magpos # if magpos == 4: # raise NicosError(self, 'depot at unknown position') # return bitmask = self._bitmask(magpos) val = list( map( str, [(self._attached_io_open.read(0) & bitmask) // bitmask, (self._attached_io_closed.read(0) & bitmask) // bitmask], )) self.log.debug('Sensing switches are in State %s', val) return ''.join(val) def doRead(self, maxage=0): s = self._read() if s == '01': return 'closed' elif s == '10': return 'open' elif s == '00': return 'UNKNOWN' else: raise NicosError( self, 'Depot magnet switches in undefined status ' '%s check switches' % s) def doStatus(self, maxage=0): s = self._read() if s == '01': return status.OK, 'idle' elif s == '10': return status.OK, 'idle' elif s == '00': return status.BUSY, 'Moving' # '? or Error!' else: return status.ERROR, 'maglock is in error state' @property def _magpos(self): s = self._attached_magazine.read(0) for i, k in enumerate(self.states): if s == k: return i return 4
class EchoTime(Moveable): """Reseda echo time device. Provides storage and access to tunewave tables which are used to determine the device setup for the particular echo time, considering the measurement mode and the current wavelength.""" attached_devices = { 'wavelength': Attach('Wavelength device', Readable), 'dependencies': Attach('Echo time dependent devices', Readable, multiple=True), } parameters = { 'zerofirst': Param( 'mapping of Devices to preconfigured value to be ' 'set before applying the echo time', type=dictof(str, anytype), settable=True, userparam=False, default={}), 'stopfirst': Param('list of Devices to stop before setting a new ' 'echotime', type=listof(str), settable=True, userparam=False, default=[]), 'tables': Param('Tune wave tables', type=dictof(oneof('nrse', 'mieze'), dictof(float, dictof(float, dictof(str, anytype)))), settable=True, internal=True), 'currenttable': Param('Currently used tune wave tables', type=dictof(float, dictof(str, anytype)), settable=False, userparam=True, internal=True, volatile=True), 'tunedevs': Param('Devices used for tuning', type=listof(str), settable=False, internal=True, volatile=True), 'availtables': Param('Available tunewave tables', type=dictof(str, listof(float)), settable=False, internal=True, volatile=True), 'wavelengthtolerance': Param('Wavelength tolerance for table' 'determination', type=float, settable=True, default=0.1), } parameter_overrides = { 'target': Override(category='instrument'), } valuetype = float def doPreinit(self, mode): # create an internal lookup dictionary for tunedevs ({name: dev}) self._tunedevs = { entry.name: entry for entry in self._attached_dependencies } def doRead(self, maxage=0): # read all tuning devices devs = { key: value.read(maxage) for key, value in self._tunedevs.items() } # find correct echotime for current device setup in currently active # tunewave table for echotime, tunedevs in self.currenttable.items(): self.log.debug('checking if we are at echotime %s', echotime) success = True for tunedev, value in tunedevs.items(): # XXX: horrible hack if tunedev.endswith('reg_amp'): continue # fuzzy matching necessary due to maybe oscillating devices prec = getattr(self._tunedevs[tunedev], 'precision', 0) if not self._fuzzy_match(value, devs.get(tunedev, None), prec): self.log.debug( '-> no, because %s is at %s not %s ' '(prec = %s)', tunedev, devs.get(tunedev, None), value, prec) success = False break if success: self.log.debug('-> YES!') return echotime # return 0 as echotime and show additional info in status string # if the echo time could not be determined return 0.0 def doStart(self, value): # filter unsupported echotimes # find best match from table for entry in self.currenttable: if abs(entry - value) < 1e-12: value = entry break else: raise InvalidValueError( 'Given echo time not supported by current ' 'tunewave table (%s/%s) within %s' % (getattr(session.experiment, 'measurementmode', 'mieze'), self._attached_wavelength.read(), self.wavelengthtolerance)) # stop stopfirst devices for devname in self.stopfirst: dev = session.getDevice(devname) self.log.debug('stopping %s', str(dev)) dev.stop() # move zerofirst devices to configured value self.log.debug('zeroing devices') wait_on = set() for devname, val in self.zerofirst.items(): dev = session.getDevice(devname) self.log.debug('moving %s to zero', str(dev)) dev.start(val) wait_on.add(dev) self.log.debug('waiting for devices to reach zero') multiWait(wait_on) self.log.debug('devices now at zero') # move all tuning devices at once without blocking wait_on = set() for tunedev, val in sorted(self.currenttable[value].items()): if tunedev in self.stopfirst: self.log.debug('skipping %s (will be set later)', tunedev) continue if tunedev in self._tunedevs: self.log.debug('setting %s to %s', tunedev, val) dev = self._tunedevs[tunedev] dev.start(val) wait_on.add(dev) else: self.log.warning('tune device %r from table not in tunedevs! ' 'movement to %r ignored !' % (tunedev, val)) self.log.debug('waiting for devices...') multiWait(wait_on) self.log.debug('devices now at configured values') # activate stopfirst devices wait_on = set() for devname in self.stopfirst: dev = session.getDevice(devname) val = self.currenttable[value][devname] self.log.debug('moving %s to %s', devname, val) dev.move(val) wait_on.add(dev) self.log.debug('devices now at configured values') multiWait(wait_on) self.log.debug('all done. good luck!') def doReadTunedevs(self): return sorted(self._tunedevs) def doReadAvailtables(self): return {key: sorted(value) for key, value in self.tables.items()} def doReadCurrenttable(self): cur_wavelength = self._attached_wavelength.read() precision = self.wavelengthtolerance table = self.tables.get( getattr(session.experiment, 'measurementmode', 'mieze'), {}) # determine current tunewave table by measurement mode and fuzzy # matched wavelength for wavelength, tunewavetable in table.items(): if self._fuzzy_match(cur_wavelength, wavelength, precision): return self._validate_table(tunewavetable) return {} def getTable(self, measurement_mode, wavelength): """Get a specific tunewave table. Avoid transfering all tables for each access. """ return self._validate_table( self.tables.get(measurement_mode, {}).get(wavelength, {})) def setTable(self, measurement_mode, wavelength, table): """Set a specific tunewave table. Avoid transfering all tables for each access.""" # validate table structure and device values table = self._validate_table(table) # use ordinary dicts instead of readonlydicts to be able to change them tables = dict(self.tables) mode_table = dict(tables.setdefault(measurement_mode, {})) # ensure float for wavelength value due to later fuzzy matching mode_table[float(wavelength)] = table tables[measurement_mode] = mode_table self.tables = tables def deleteTable(self, measurement_mode, wavelength): """Delete a specific tunewave table. Avoid transfering all tables for each access.""" # use ordinary dicts instead of readonlydicts to be able to change them tables = dict(self.tables) tables[measurement_mode] = dict(tables.get(measurement_mode, {})) del tables[measurement_mode][float(wavelength)] self.tables = tables def _fuzzy_match(self, value, setpoint, precision): """General fuzzy matching of values (used for float comparisons).""" return (setpoint - precision) <= value <= (setpoint + precision) def _validate_table(self, table): """Validates the structure of a single tunewave table and and all the included device values (using the valuetype of the particular device). """ # Structure of a single tunewave table: {echotime: {tunedev : value}} result = {} for echotime, tunedevs in table.items(): echotime = float(echotime) result[echotime] = {} for tunedev, value in tunedevs.items(): result[echotime][tunedev] = self._tunedevs[tunedev].valuetype( value) return result