class ThermoStatedCellHolder(Device): """ The device holds fundamental parameters for the sample environment `Thermo Stated Cell Holder`. This holder has two rows. Each row can have four cartridges (thus in total of eight cartridges). Each cartridge can be loaded with three different kind of cell holder, namely, `Narrow Cell` with 10 positions, `Wide Cell` with four positions and `Rotating Cell` with three positions. """ parameters = { 'cell_type_indices': Param( 'Cell type indices', type=listof(int), default=[0] * 8, # Number of cartridges settable=False), 'cell_type_names': Param('Cell types', type=listof(str), default=['Narrow Cell'] * 8, settable=False), 'first_positions': Param('Scanned first positions', type=listof(tupleof(float, float)), default=[(0.0, 0.0)] * 8, settable=False), 'positions': Param('Calculated positions', type=listof(listof(tupleof(float, float))), settable=False) }
class SelectorLambda(Moveable): """ Control selector wavelength directly, converting between speed and wavelength. This uses not the default conversion from the Astrium selector classes, since the selector is tilted against the beam, and it is easier to use the constant determined by wavelength calibration. This class allows two calibration settings, determined by the current value of a (manually moved) "tilt" device. """ parameters = { 'constants': Param( 'Conversion constants: ' 'lam[Ang] = constant/speed[Hz] + offset', type=tupleof(float, float), mandatory=True, unit='Ang Hz'), 'offsets': Param('Conversion offsets: ' 'lam[Ang] = constant/speed[Hz] + offset', type=tupleof(float, float), mandatory=True, unit='Ang'), } attached_devices = { 'seldev': Attach('The selector speed device', Moveable), 'tiltdev': Attach('The tilt device', Readable), } hardware_access = False def doRead(self, maxage=0): tilted = bool(self._attached_tiltdev.read(maxage)) speed = self._attached_seldev.read(maxage) return (60 * self.constants[tilted] / speed) + self.offsets[tilted] \ if speed else -1 def doIsAllowed(self, value): if value == 0: return False, 'zero wavelength not allowed' tilted = bool(self._attached_tiltdev.read(0)) speed = int( round(60 * self.constants[tilted] / (value - self.offsets[tilted]))) return self._attached_seldev.isAllowed(speed) def doStart(self, value): tilted = bool(self._attached_tiltdev.read(0)) speed = int( round(60 * self.constants[tilted] / (value - self.offsets[tilted]))) self.log.debug('moving selector to %f rpm', speed) self._attached_seldev.start(speed)
class Detector(Moveable): """Combination device for the detector axes.""" valuetype = tupleof(float, float, float) attached_devices = { 'x': Attach('X motor', Moveable), 'y': Attach('Y motor', Moveable), 'z': Attach('Z motor', Moveable), } parameter_overrides = { 'unit': Override(mandatory=False, default='mm'), 'fmtstr': Override(default='%.1f, %.2f, %.0f'), } def doRead(self, maxage=0): return (self._attached_x.read(maxage), self._attached_y.read(maxage), self._attached_z.read(maxage)) def doIsAllowed(self, pos): for (i, name, dev) in [(0, 'x', self._attached_x), (1, 'y', self._attached_y), (2, 'z', self._attached_z)]: ok, why = dev.isAllowed(pos[i]) if not ok: return False, name + ': ' + why return True, '' def doStart(self, pos): self._attached_x.start(pos[0]) self._attached_y.start(pos[1]) self._attached_z.start(pos[2])
class LinearROIChannel(PostprocessPassiveChannel): """Calculates counts for a region of interest in a 1D spectrum.""" parameters = { 'roi': Param('Region of interest (start, end) including end', tupleof(int, int), settable=True, category='general'), } parameter_overrides = { 'unit': Override(default='cts'), 'fmtstr': Override(default='%d'), } def getReadResult(self, arrays, _results, _quality): if any(self.roi): return [arr[self.roi[0]:self.roi[1] + 1].sum() for arr in arrays] return [arr.sum() for arr in arrays] def valueInfo(self): if len(self.readresult) > 1: return tuple( Value(name=self.name + '[%d]' % i, type='counter', fmtstr='%d') for i in range(1, len(self.readresult) + 1)) return Value(name=self.name, type='counter', fmtstr='%d'),
class Spectrum(VirtualImage): parameters = { 'preselection': Param('Preset value for this channel', type=float, settable=True), } parameter_overrides = { 'sizes': Override(type=tupleof(intrange(1, 1), intrange(1, 16384)), default=(1, 16384)), 'ismaster': Override(settable=True), } # set to True to get a simplified doEstimateTime is_timer = False def doEstimateTime(self, elapsed): if not self.ismaster or self.doStatus()[0] != status.BUSY: return None if self.is_timer: return self.preselection - elapsed else: counted = float(self.doRead()[0]) # only estimated if we have more than 3% or at least 100 counts if counted > 100 or counted > 0.03 * self.preselection: if 0 <= counted <= self.preselection: return (self.preselection - counted) * elapsed / counted def doReadArray(self, _quality): return self._buf[0]
class RectROIChannel(PostprocessPassiveChannel): """Calculates counts for a rectangular region of interest.""" parameters = { 'roi': Param('Rectangular region of interest (x, y, width, height)', tupleof(int, int, int, int), settable=True, category='general'), } parameter_overrides = { 'unit': Override(default='cts'), 'fmtstr': Override(default='%d'), } def getReadResult(self, arrays, _results, _quality): if any(self.roi): x, y, w, h = self.roi return [arr[y:y + h, x:x + w].sum() for arr in arrays] return [arr.sum() for arr in arrays] def valueInfo(self): if len(self.readresult) > 1: return tuple( Value(name=self.name + '[%d]' % i, type='counter', fmtstr='%d') for i in range(1, len(self.readresult) + 1)) return Value(name=self.name, type='counter', fmtstr='%d'),
class LaserDetector(Measurable): parameters = { 'pv_name': Param('Store the current identifier', internal=False, type=str, default="SES-SCAN:LSR-001:AnalogInput", settable=True), 'curstatus': Param('Store the current device status', internal=True, type=tupleof(int, str), default=(status.OK, ""), settable=True), 'answer': Param('Store the current device status', internal=True, type=float, default=0, settable=True), } def doPrepare(self): self.curstatus = status.BUSY, "Preparing" self.curstatus = status.OK, "" def doStart(self): max_pow = 0 results = [] for _ in range(5): session.delay(0.1) val = pvget(self.pv_name) max_pow = max(val, max_pow) results.append(val) self.answer = sum(results) / len(results) def doRead(self, maxage=0): return [self.answer] def doFinish(self): self._stop_processing() def _stop_processing(self): self.curstatus = status.OK, "" def doSetPreset(self, t, **preset): self.curstatus = status.BUSY, "Preparing" def doStop(self): # Treat like a finish self._stop_processing() def doStatus(self, maxage=0): return self.curstatus def duringMeasureHook(self, elapsed): return LIVE def valueInfo(self): return Value(self.name, unit=self.unit),
class ImageKafkaDataSink(ProducesKafkaMessages, DataSink): """ Data sink which writes images to Kafka after serializing them. The parameter *channeltostream* provides a dict of all the image channels from which the data is to be be forwarded mapped to a tuple of (kafka topic, message source name) """ parameters = { 'maximagesize': Param('Expected max array size of the image', type=int, default=5e7), 'channeltostream': Param( 'Dict of image channel name(to be forwarded) -> (topic, source)', type=dictof(str, tupleof(str, str)), mandatory=True), } parameter_overrides = { 'settypes': Override(default=[POINT]), } handlerclass = ImageKafkaDataSinkHandler serializer = HistogramFlatbuffersSerializer() def doInit(self, mode): # Increase the maximum message size that the producer can send self._setProducerConfig(max_request_size=self.maximagesize)
class DLSCard(BaseImageChannel): attached_devices = { 'wheels': Attach('The filter wheel positions', Readable, multiple=True, optional=True), } parameters = { 'angles': Param('Scattering angles of the detector', type=tupleof(float, float), mandatory=True, settable=True), 'mode': Param('Measure mode', type=oneof(*MODES), default='cross_cross', settable=True), } def _get_filters(self): return ' '.join('%d' % wh.read() for wh in self._attached_wheels) def setMode(self): self._dev.readoutMode = self.mode def readAbscissa(self): return self._dev.abscissa def readIntensity(self): data = self._dev.intensity return data.reshape((len(data) // 3, 3)) abscissa_arraydesc = ArrayDesc('data', shape=(264,), dtype='<f8') intensity_arraydesc = ArrayDesc('data', shape=(100, 3), dtype='<f8')
class HoveringAxis(Axis): """An axis that also controls air for airpads.""" attached_devices = { 'switch': Attach('The device used for switching air on and off', Moveable), } parameters = { 'startdelay': Param('Delay after switching on air', type=float, mandatory=True, unit='s'), 'stopdelay': Param('Delay before switching off air', type=float, mandatory=True, unit='s'), 'switchvalues': Param('(off, on) values to write to switch device', type=tupleof(anytype, anytype), default=(0, 1)), } def doTime(self, start, end): return Axis.doTime(self, start, end) + self.startdelay + self.stopdelay def _preMoveAction(self): self._adevs['switch'].maw(self.switchvalues[1]) session.delay(self.startdelay) def _postMoveAction(self): session.delay(self.stopdelay) self._adevs['switch'].maw(self.switchvalues[0])
class SwordAxis(Axis): attached_devices = { 'switch': Attach('The device used for switching the brake', Moveable), } parameters = { 'startdelay': Param('Delay after switching on brake', type=float, mandatory=True, unit='s'), 'stopdelay': Param('Delay before switching off brake', type=float, mandatory=True, unit='s'), 'switchvalues': Param('(on, off) values to write to brake switch', type=tupleof(anytype, anytype), default=(2, 1)), } def doStatus(self, maxage=0): stval, ststr = Axis.doStatus(self, maxage) # special case: the Phytron server correctly returns an error if the # enable bit is not set, but since this is always the case we want to # present it as just a WARN state if stval == status.ERROR and ststr == 'motor halted, ENABLE not set': return status.WARN, ststr return stval, ststr def doTime(self, start, end): return Axis.doTime(self, start, end) + self.startdelay + self.stopdelay def _preMoveAction(self): self._adevs['switch'].maw(self.switchvalues[1]) session.delay(self.startdelay) def _postMoveAction(self): session.delay(self.stopdelay) self._adevs['switch'].maw(self.switchvalues[0])
class PolSwitcher(SequencerMixin, MultiSwitcher): """The turntable that contains the polarizer or neutron guide. Changing the table positions has to be done in a certain order, so that the final positions of the polarizer or guide are always reproducible. """ parameters = { 'movepos': Param('Position (xv, yv, xh, yh) while rotating', type=tupleof(float, float, float, float), default=(5., 5., 5., 5.)), } parameter_overrides = { 'fmtstr': Override(default='%s'), 'unit': Override(mandatory=False, default=''), } def doInit(self, mode): MultiSwitcher.doInit(self, mode) if len(self._attached_moveables) != 5: raise ConfigurationError(self, 'must have exactly 5 moveables') self._mot_rot, self._mot_xv, self._mot_yv, self._mot_xh, \ self._mot_yh = self._attached_moveables self.valuetype = oneof(*sorted(self.mapping, key=num_sort)) def _generateSequence(self, target): seq = [] targets = self.mapping[target] rot_target, xv_target, xh_target, yv_target, yh_target = targets # move translation units to move pos (in parallel) seq.append(tuple(SeqDev(m, p) for (m, p) in zip(self._attached_moveables[1:], self.movepos))) # move rotation stage seq.append(SeqDev(self._mot_rot, rot_target)) # move Y axes to final position with backlash seq.append((SeqDev(self._mot_yv, yv_target + 0.1), SeqDev(self._mot_yh, yh_target + 0.1))) seq.append(SeqDev(self._mot_yv, yv_target)) seq.append(SeqDev(self._mot_yh, yh_target)) # move X axes to 0.1 and then to final position seq.append((SeqDev(self._mot_xv, 0.1), SeqDev(self._mot_xh, 0.1))) seq.append(SeqDev(self._mot_xv, xv_target)) seq.append(SeqDev(self._mot_xh, xh_target)) # move rotation stage again, if it changed seq.append(SeqDev(self._mot_rot, rot_target)) return seq def doStart(self, target): if self._seq_is_running(): if self._mode == SIMULATION: self._seq_thread.join() self._seq_thread = None else: raise MoveError(self, 'Cannot start device, sequence is still ' 'running (at %s)!' % self._seq_status[1]) self._startSequence(self._generateSequence(target))
class MagnetSampleTheta(Moveable): """Class for controlling the sample rotation inside a magnet that is built with significant dark angles that must be avoided for incoming and outgoing beam, by rotating the magnet itself on the sample table. """ attached_devices = { 'sample_theta': Attach('Sample-only theta motor', Moveable), 'magnet_theta': Attach('Magnet-plus-sample motor', Moveable), 'two_theta': Attach('Scattering angle', Moveable), } parameters = { 'blocked': Param('Blocked angle range in the magnet. 0 is the ' 'incoming beam direction', unit='deg', type=listof(tupleof(float, float))), 'windowstep': Param('Steps in which to move the magnet when looking ' 'for free windows', unit='deg', type=int, default=5), } def _find_window(self, gamma, magnet): # find a free window for incoming and outgoing beam, which is closest # to the current position of the magnet result = [] for pos in range(0, 360, self.windowstep): for (a1, a2) in self.blocked: # check for blocked incoming beam if in_range(pos, -a2, -a1): break # check for blocked outgoing beam if in_range(pos, -a2 + 180 + gamma, -a1 + 180 + gamma): break else: # no "break" result.append(pos) self.log.debug('gamma: %.3f, magnet: %.3f', gamma, magnet) self.log.debug('new possible positions: %s', result) if not result: raise ComputationError(self, 'no position found for magnet with ' 'incoming and outgoing beam free') return min(result, key=lambda pos: abs(pos - 0.1)) def doStart(self, pos): # get target for scattering angle gamma = self._attached_two_theta.target magnet = self._attached_magnet_theta.read(0) # determine nearest free window new_magnet = self._find_window(gamma, magnet) self._attached_magnet_theta.start(to_range(new_magnet)) self._attached_sample_theta.start(to_range(pos - new_magnet)) def _getWaiters(self): return [self._attached_sample_theta, self._attached_magnet_theta] def doRead(self, maxage=0): angle = self._attached_magnet_theta.read(maxage) + \ self._attached_sample_theta.read(maxage) return to_range(angle)
class HoveringAxis(SequencerMixin, Axis): """An axis that also controls air for airpads.""" attached_devices = { 'switch': Attach('The device used for switching air on and off', Moveable), } parameters = { 'startdelay': Param('Delay after switching on air', type=float, mandatory=True, unit='s'), 'stopdelay': Param('Delay before switching off air', type=float, mandatory=True, unit='s'), 'switchvalues': Param('(off, on) values to write to switch device', type=tupleof(anytype, anytype), default=(0, 1)), } hardware_access = True def _generateSequence(self, target): return [ SeqDev(self._attached_switch, self.switchvalues[1]), SeqSleep(self.startdelay), SeqCall(Axis.doStart, self, target), SeqCall(self._hw_wait), SeqSleep(self.stopdelay), SeqDev(self._attached_switch, self.switchvalues[0]), ] def _hw_wait(self): # overridden: query Axis status, not HoveringAxis status while Axis.doStatus(self, 0)[0] == status.BUSY: session.delay(self._base_loop_delay) def doStart(self, target): if self._seq_is_running(): self.stop() self.log.info('waiting for axis to stop...') self.wait() if abs(target - self.read()) < self.precision: return self._startSequence(self._generateSequence(target)) def doStop(self): # stop only the axis, but the sequence has to run through Axis.doStop(self) def doTime(self, start, end): return Axis.doTime(self, start, end) + self.startdelay + self.stopdelay
class Polarizer(Moveable): """Controls both the position of the polarizer and the spin flipper. """ valuetype = oneof(*POL_SETTINGS) hardware_access = False attached_devices = { 'switcher': Attach('polarizer in/out switch', Moveable), 'flipper': Attach('flipper', Moveable), } parameters = { 'values': Param('Possible values (for GUI)', internal=True, type=listof(str), default=POL_SETTINGS), 'switchervalues': Param('Possible values for the switcher (out, in)', type=tupleof(str, str), default=('ng', 'pol')), } parameter_overrides = { 'fmtstr': Override(default='%s'), 'unit': Override(mandatory=False, default=''), } def doRead(self, maxage=0): switcher_pos = self._attached_switcher.read(maxage) flipper_pos = self._attached_flipper.read(maxage) if switcher_pos == 'unknown' or flipper_pos == 'unknown': return 'unknown' if switcher_pos == self.switchervalues[0]: return 'out' # Polarizer is a transmission supermirror => without flipper we get # the "down" polarization. if flipper_pos == 'on': return 'up' return 'down' def doStart(self, target): switch_pos = self._attached_switcher.read(0) if target == 'out': if switch_pos != self.switchervalues[0]: self._attached_switcher.start(self.switchervalues[0]) self._attached_flipper.start('off') else: if switch_pos != self.switchervalues[1]: self._attached_switcher.start(self.switchervalues[1]) if target == 'up': self._attached_flipper.start('on') elif target == 'down': self._attached_flipper.start('off')
def doInit(self, mode): SXTalBase.doInit(self, mode) if self.inelastic: self.__dict__['en'] = SXTalIndex('en', unit='meV', fmtstr='%.3f', index=3, lowlevel=True, sxtal=self) self.valuetype = tupleof(float, float, float, float)
class NPGZFileSink(BaseNPGZFileSink): handlerclass = NPGZImageSinkHandler parameters = { 'linknametemplate': Param('Template for the data file name hardlinked' 'to the i-th file configured using' '(i, [nametemplates]).', type=tupleof(int, list), mandatory=True,settable = False, prefercache = False, ext_desc=TEMPLATE_DESC), }
class CollimationSlit(TwoAxisSlit): """Two-axis slit with an additional parameter for the "open" position.""" parameters = { 'openpos': Param('Position to move slit completely open', type=tupleof(float, float), default=(50.0, 50.0)), } parameter_overrides = { 'fmtstr': Override(default='%.1f x %.1f'), }
class VirtualChannel(ActiveChannel): """A virtual detector channel.""" parameters = { 'curvalue': Param('Current value', settable=True, unit='main'), 'curstatus': Param('Current status', type=tupleof(int, str), settable=True, default=(status.OK, 'idle'), no_sim_restore=True), } _delay = 0.1 _thread = None def doInit(self, mode): self._stopflag = False if mode == MASTER: self.curvalue = 0 def doStart(self): if self._thread and self._thread.is_alive(): return self.curvalue = 0 self.doResume() def doPause(self): self.doFinish() return True def doResume(self): self._stopflag = False self.curstatus = (status.BUSY, 'counting') self._thread = createThread('%s %s' % (self.__class__.__name__, self), self._counting) def doFinish(self): if self._thread and self._thread.is_alive(): self._stopflag = True self._thread.join() else: self.curstatus = (status.OK, 'idle') def doStop(self): self.doFinish() def doStatus(self, maxage=0): return self.curstatus def doRead(self, maxage=0): return self.curvalue def doShutdown(self): if self._thread: self.doStop()
def doInit(self, mode): # Even if the slit could not be become closer then 0 and not more # opened the maxheight the instrument scientist want to scan over # the limits to find out the 'open' and 'closed' point for the neutrons self.valuetype = tupleof(floatrange(-1, self.maxheight + 1), float) # generate auto devices for name, idx, opmode in [('height', 0, CENTERED), ('center', 1, CENTERED)]: self.__dict__[name] = SingleSlitAxis('%s.%s' % (self.name, name), slit=self, unit=self.unit, lowlevel=True, index=idx, opmode=opmode) self._motors = [self._attached_slit_r, self._attached_slit_s]
class HePolSink(DataSink): """For every scan, records the mon2/mon1 ratio in the transmission device. """ attached_devices = { 'transmission': Attach('Transmission device', Moveable), } parameters = { 'monitors': Param('Names of the two monitor devices to calculate ' 'the transmission ratio', type=tupleof(str, str), mandatory=True), } parameter_overrides = { 'settypes': Override(default={SCAN, SUBSCAN}) } handlerclass = HePolSinkHandler
class Mailer(Notifier): """Sends notifications via e-mail. If a Mailer is configured as a notifier (the Mailer device is in the list of `notifiers` in the `sysconfig` entry), the receiver addresses (not copies) can be set by `.SetMailReceivers`. """ parameters = { 'mailserver': Param('Mail server', type=str, default='localhost', settable=True), 'sender': Param('Mail sender address', type=mailaddress, mandatory=True), } parameter_overrides = { 'receivers': Override(description='Mail receiver addresses', type=listof(mailaddress)), 'copies': Override(type=listof(tupleof(mailaddress, oneof('all', 'important')))), } def reset(self): self.log.info('mail receivers cleared') self.receivers = [] def send(self, subject, body, what=None, short=None, important=True): def send(): receivers = self._getAllRecipients(important) if not receivers: return ret = sendMail(self.mailserver, receivers, self.sender, self.subject + ' -- ' + subject, body) if not ret: # on error, ret is a list of errors self.log.info('%smail sent to %s', what and what + ' ' or '', ', '.join(receivers)) else: self.log.warning('sending mail failed: %s', ', '.join(ret)) if not self._checkRateLimit(): return createThread('mail sender', send)
class ROIChannel(PostprocessPassiveChannel): """Calculates counts for a rectangular or ellipsoid region of interest.""" parameters = { 'roi': Param('Rectangular region of interest (x1, y1, x2, y2)', type=tupleof(int, int, int, int), settable=True, category='general'), 'shape': Param('Select the shape of the ROI', type=oneof('rectangle', 'ellipse'), settable=True, category='general'), } parameter_overrides = { 'unit': Override(default='cts'), 'fmtstr': Override(default='%d'), } def getReadResult(self, arrays, _results, _quality): arr = arrays[0] if arr is None: return [0, 0] if any(self.roi): x1, y1, x2, y2 = self.roi if self.shape == 'rectangle': inner = arr[y1:y2, x1:x2].sum() else: cx = (x1 + x2) / 2. cy = (y1 + y2) / 2. y, x = numpy.indices(arr.shape) ix = ((y - cy) / (y2 - cy))**2 - ((x - cx) / (x2 - cx))**2 <= 1 inner = arr[ix].sum() outer = arr.sum() - inner return [inner, outer] return [arr.sum(), 0] def valueInfo(self): return (Value(name=self.name + '.in', type='counter', fmtstr='%d'), Value(name=self.name + '.out', type='counter', fmtstr='%d'))
class SINQAsciiSink(FileSink): """ This is a filesink which writes scan data files in the SINQ ASCII format. The implementation strives to be as compatible as possible to the old format as written by SICS. SINQ ASCII files are constructed from a template file. This template file contains ASCII text which is copied verbatim to the output intermixed with place holder strings. These are replaced with data from NICOS. In addition, the actual data from the scan is collected and written too. The special placeholders recognized are: - !!VAR(dev,par)!! is replaced with the value of the parameter par of device dev. Par can be missing and is then value. - !!DRIV(dev)!! replaced by the value of dev - !!ZERO(dev)!! replaced by the offset of dev. Dev must be a motor - !!SCRIPT(txt)!! replaced by the output of running script txt - !!DATE!! replaced by the current date and time - !!FILE!! replaced by the original file path of the scan file - !!SCANZERO!! replaced by a list of zero points of the scanned devices There is some redundancy here but as the goal is to reuse the SICS template files as far as possible, this has to be accepted. One file per scan is written. This format is designed with single counters in mind, this is not for area detetcor data. Use the NexusFileSink for such data. """ parameters = { 'templatefile': Param('Path to SICS style template file', type=str, mandatory=True), 'scaninfo': Param('Header text and nicos device for each scan point', type=listof(tupleof(str, nicosdev)), mandatory=True), } parameter_overrides = { 'settypes': Override(default=[SCAN, SUBSCAN]), } handlerclass = SINQAsciiSinkHandler
class MezeiFlipper(BaseFlipper): """ Class for a Mezei type flipper consisting of flipper and correction current. For the state "on" the two power supplies are moved to the values given by the `currents` parameter, for "off" they are moved to zero. """ parameters = { 'currents': Param('Flipper and correction current', settable=True, type=tupleof(float, float)), } def doStart(self, value): if value == ON: self._attached_flip.start(self.currents[0]) self._attached_corr.start(self.currents[1]) else: self._attached_flip.start(0) self._attached_corr.start(0)
class VSForbiddenMoveable(WindowMoveable): """ Velocity selectors have forbidden regions in which they are not supposed to run for reason of excessive vibration. This class checks for this """ parameters = { 'forbidden_regions': Param('List of forbidden regions', type=listof(tupleof(float, float)), mandatory=True) } valuetype = float def doIsAllowed(self, value): for region in self.forbidden_regions: if region[0] < value < region[1]: return False, \ 'Desired value value is within ' \ 'forbidden region %f to %f' \ % (region[0], region[1]) return True, ''
class McStasImage(ImageChannelMixin, PassiveChannel): """Image channel based on McStas simulation.""" _mythread = None _process = None parameters = { 'size': Param( 'Detector size in pixels (x, y)', settable=False, type=tupleof(intrange(1, 8192), intrange(1, 8192)), default=(1, 1), ), 'mcstasprog': Param('Name of the McStas simulation executable', type=str, settable=False), 'mcstasdir': Param('Directory where McStas stores results', type=str, default='singlecount', settable=False), 'mcstasfile': Param('Name of the McStas data file', type=str, settable=False), 'mcsiminfo': Param('Name for the McStas Siminfo file', settable=False, type=str, default='mccode.sim'), 'ci': Param('Constant ci applied to simulated intensity I', settable=False, type=floatrange(0.), default=1e3) } def doInit(self, mode): self.arraydesc = ArrayDesc(self.name, self.size, '<u4') self._workdir = os.getcwd() def doReadArray(self, quality): self.log.debug('quality: %s', quality) if quality == LIVE: self._send_signal(SIGUSR2) elif quality == FINAL: if self._mythread and self._mythread.is_alive(): self._mythread.join(1.) if self._mythread.is_alive(): self.log.exception("Couldn't join readout thread.") else: self._mythread = None self._readpsd(quality == LIVE) return self._buf def _prepare_params(self): """Return a list of key=value strings. Each entry defines a parameter setting for the mcstas simulation call. examples: param=10 """ raise NotImplementedError('Please implement _prepare_params method') def doPrepare(self): self._mcstas_params = ' '.join(self._prepare_params()) self.log.debug('McStas parameters: %s', self._mcstas_params) self._buf = np.zeros(self.size[::-1]) self.readresult = [0] def valueInfo(self): return (Value(self.name + '.sum', unit='cts', type='counter', errors='sqrt', fmtstr='%d'), ) def doStart(self): self._mythread = createThread('detector %s' % self, self._run) def doStatus(self, maxage=0): if self._mythread and self._mythread.is_alive(): return status.BUSY, 'busy' return status.OK, 'idle' def doFinish(self): self.log.debug('finish') self._send_signal(SIGTERM) def _send_signal(self, sig): if self._process and self._process.is_running(): self._process.send_signal(sig) # wait for mcstas releasing fds datafile = path.join(self._workdir, self.mcstasdir, self.mcstasfile) siminfo = path.join(self._workdir, self.mcstasdir, self.mcsiminfo) try: while self._process and self._process.is_running(): fnames = [f.path for f in self._process.open_files()] if siminfo not in fnames and datafile not in fnames: break session.delay(.01) except (AccessDenied, NoSuchProcess): self.log.debug( 'McStas process already terminated in _send_signal(%r)', sig) self.log.debug('McStas process has written file on signal (%r)', sig) def _run(self): """Run McStas simulation executable. The current settings of the instrument parameters will be transferred to it. """ try: shutil.rmtree(self.mcstasdir) except (IOError, OSError): self.log.info('could not remove old data') command = '%s -n 1e8 -d %s %s' % (self.mcstasprog, self.mcstasdir, self._mcstas_params) self.log.debug('run %s', command) try: self._process = Popen(command.split(), stdout=PIPE, stderr=PIPE, cwd=self._workdir) out, err = self._process.communicate() if out: self.log.debug('McStas output:') for line in out.splitlines(): self.log.debug('[McStas] %s', line) if err: self.log.warning('McStas found some problems:') for line in err.splitlines(): self.log.warning('[McStas] %s', line) except OSError as e: self.log.error('Execution failed: %s', e) self._process.wait() self._process = None def _readpsd(self, ignore_error=False): try: with open( path.join(self._workdir, self.mcstasdir, self.mcstasfile), 'r') as f: lines = f.readlines()[-3 * (self.size[0] + 1):] if lines[0].startswith('# Data') and self.mcstasfile in lines[0]: self._buf = ( np.loadtxt(lines[1:self.size[0] + 1], dtype=np.float32) * self.ci).astype(np.uint32) self.readresult = [self._buf.sum()] elif not ignore_error: raise IOError('Did not find start line: %s' % lines[0]) except IOError: if not ignore_error: self.log.exception('Could not read result file')
class TwoAxisSlit(CanReference, Moveable): """A rectangular slit consisting of 2 orthogonal slits. All instances have attributes controlling single dimensions that can be used as devices, for example in scans. These attributes are: * `width`, `height` -- aliases for the horizontal and vertical slits Example usage:: >>> scan(slit.width, 0, 1, 6) # scan over slit width from 0 to 5 mm """ attached_devices = { 'horizontal': Attach('Horizontal slit', HasPrecision), 'vertical': Attach('Vertical slit', HasPrecision), } parameters = { 'parallel_ref': Param('Set to True if the blades\' reference drive ' 'can be done in parallel.', type=bool, default=False), } parameter_overrides = { 'fmtstr': Override(default='%.2f %.2f'), 'unit': Override(mandatory=False), } valuetype = tupleof(float, float) hardware_access = False def doInit(self, mode): self._slits = [self._attached_horizontal, self._attached_vertical] self._slitnames = ['horizontal', 'vertical'] for name in self._slitnames: self.__dict__[name] = self._adevs[name] self.__dict__['width'] = self.horizontal self.__dict__['height'] = self.vertical def doIsAllowed(self, target): if len(target) != 2: raise InvalidValueError(self, 'arguments required for centered ' 'mode: [width, height]') for slit, slitname, pos in zip(self._slits, self._slitnames, target): ok, why = slit.isAllowed(pos) if not ok: return ok, '[%s slit] %s' % (slitname, why) return True, '' def doStart(self, target): th, tv = target self._attached_horizontal.move(th) self._attached_vertical.move(tv) def doReset(self): for ax in self._slits: ax.reset() for ax in self._slits: ax.wait() def doReference(self): multiReference(self, self._slits, self.parallel_ref) def doRead(self, maxage=0): return [d.read(maxage) for d in self._slits] def valueInfo(self): return Value('%s.width' % self, unit=self.unit, fmtstr='%.2f'), \ Value('%s.height' % self, unit=self.unit, fmtstr='%.2f') def doStatus(self, maxage=0): return multiStatus(list(zip(self._slitnames, self._slits))) def doReadUnit(self): return self._attached_horizontal.unit
class HttpConnector(HasCommunication, Readable): """ Device to connect to the HTTP Server using HTTP Basic Authentication. *baseurl* provided in the parameters is prepended while connecting to the server using GET or POST. Parameter *base64auth* provides a way to authenticate the connection. """ parameters = { 'baseurl': Param('Base request URL to be used', type=str, mandatory=True), 'base64auth': Param('HTTP authentication encoded in base64', type=str, mandatory=True, userparam=False), 'byteorder': Param('Endianness of the raw data on server(big/little)', type=oneof('big', 'little'), default='little'), 'curstatus': Param('Current status of the connection (readonly)', type=tupleof(int, str), settable=True, internal=True) } parameter_overrides = { 'unit': Override(mandatory=False, userparam=False, settable=False) } status_code_msg = { 400: 'Bad request', 403: 'Authentication did not work..', 404: 'Somehow, address was not found!', 500: 'Internal server error', } def doInit(self, mode): # Check if the base url is available self._com_retry(None, requests.get, self.baseurl, headers=self._get_auth()) def _get_auth(self): return {"Authorization": "Basic %s" % self.base64auth} def _com_return(self, result, info): # Check if the communication was successful response = result.status_code if response in self.status_code_msg: raise CommunicationError(self.status_code_msg.get(response)) elif response != 200: raise CommunicationError('Error while connecting to server!') self._setROParam('curstatus', (status.OK, '')) return result def _com_raise(self, err, info): self._setROParam('curstatus', (status.ERROR, 'Communication Error!')) HasCommunication._com_raise(self, err, info) def _com_warn(self, retries, name, err, info): self._com_raise(err, info) def doRead(self, maxage=0): return '' def doStatus(self, maxage=0): return self.curstatus def get(self, name='', params=()): """Connect to *baseurl/name* using the GET protocol :param name: String to be appended to the *baseurl* :param params: GET parameters to be passed :return: (requests.Response) response """ return self._com_retry(None, requests.get, self.baseurl + '/' + name, headers=self._get_auth(), params=params) def post(self, name='', data=()): """Connect to *baseurl/name* using the POST protocol :param name: String to be appended to the *baseurl* :param data: POST parameters to be passed :return: (requests.Response) response """ return self._com_retry(None, requests.post, self.baseurl + '/' + name, headers=self._get_auth(), data=data)
class TAS(Instrument, Moveable): """An instrument class that can move in (q,w) space. When setting up a triple-axis configuration, use this as your instrument device (or derive an individual subclass). """ attached_devices = { 'cell': Attach('Unit cell object to calculate angles', Cell), 'mono': Attach('Monochromator device', Monochromator), 'ana': Attach('Analysator device', Monochromator), 'phi': Attach('Sample scattering angle', Moveable), 'psi': Attach('Sample rocking angle', Moveable), 'alpha': Attach('Device moved to "alpha" angle between ki and Q', Moveable, optional=True), } parameters = { 'scanmode': Param('Operation mode: one of ' + ', '.join(SCANMODES), type=oneof(*SCANMODES), default='CKI', settable=True, category='instrument'), 'scanconstant': Param('Constant of the operation mode', type=float, default=0, settable=True, category='instrument'), 'axiscoupling': Param('Whether the sample th/tt axes are coupled', type=bool, default=True, settable=True, category='instrument'), 'psi360': Param('Whether the range of psi is 0-360 deg ' '(otherwise -180-180 deg is assumed)', type=bool, default=True, settable=True, category='instrument'), 'scatteringsense': Param('Scattering sense', default=(1, -1, 1), type=tupleof(oneof(1, -1), oneof(1, -1), oneof(1, -1)), settable=True, chatty=True, category='instrument'), 'energytransferunit': Param('Energy transfer unit', type=str, default='THz', settable=True), 'collimation': Param('Collimation settings', type=str, settable=True, category='instrument'), 'spurioncheck': Param('Whether to check for spurions during simulation', settable=True, type=bool, default=True), } parameter_overrides = { 'fmtstr': Override(default='[%6.4f, %6.4f, %6.4f, %6.4f]'), 'unit': Override(default='rlu rlu rlu THz', mandatory=False, settable=True) } valuetype = tupleof(float, float, float, float) hardware_access = False def doInit(self, mode): self.__dict__['h'] = TASIndex('h', unit='rlu', fmtstr='%.3f', index=0, lowlevel=True, tas=self) self.__dict__['k'] = TASIndex('k', unit='rlu', fmtstr='%.3f', index=1, lowlevel=True, tas=self) self.__dict__['l'] = TASIndex('l', unit='rlu', fmtstr='%.3f', index=2, lowlevel=True, tas=self) self.__dict__['E'] = TASIndex('E', unit=self.energytransferunit, fmtstr='%.3f', index=3, lowlevel=True, tas=self) self._last_calpos = None if self.scatteringsense[0] != self._attached_mono.scatteringsense: self.log.warning('%s.scatteringsense is not the same as ' '%s.scatteringsense[0], please reset %s', self._attached_mono, self, self) if self.scatteringsense[2] != self._attached_ana.scatteringsense: self.log.warning('%s.scatteringsense is not the same as ' '%s.scatteringsense[2], please reset %s', self._attached_ana, self, self) def doShutdown(self): for name in ['h', 'k', 'l', 'E']: if name in self.__dict__: self.__dict__[name].shutdown() def _getWaiters(self): if self.scanmode == 'DIFF': res = [self._attached_mono, self._attached_phi, self._attached_psi] else: res = [self._attached_mono, self._attached_ana, self._attached_phi, self._attached_psi] if self._attached_alpha is not None: res.append(self._attached_alpha) return res def _thz(self, ny): if self.energytransferunit == 'meV': return ny / THZ2MEV return ny def doIsAllowed(self, pos): qh, qk, ql, ny = pos ny = self._thz(ny) try: angles = self._attached_cell.cal_angles( [qh, qk, ql], ny, self.scanmode, self.scanconstant, self.scatteringsense[1], self.axiscoupling, self.psi360) except ComputationError as err: return False, str(err) # check limits for the individual axes for devname, value in zip(['mono', 'ana', 'phi', 'psi', 'alpha'], angles): dev = self._adevs[devname] if dev is None: continue if isinstance(dev, Monochromator): ok, why = dev.isAllowed(from_k(value, dev.unit)) else: ok, why = dev.isAllowed(value) if not ok: return ok, 'target position %s outside limits for %s: %s' % \ (dev.format(value, unit=True), dev, why) return True, '' def _sim_getMinMax(self): ret = [] if self._sim_min is not None: for i, name in enumerate(['h', 'k', 'l', 'E']): ret.append((name, '%.4f' % self._sim_value[i], '%.4f' % self._sim_min[i], '%.4f' % self._sim_max[i])) return ret def _sim_setValue(self, pos): self._sim_old_value = self._sim_value self._sim_value = pos self._sim_min = tuple(map(min, pos, self._sim_min or pos)) self._sim_max = tuple(map(max, pos, self._sim_max or pos)) def doReset(self): self.doWriteScatteringsense(self.scatteringsense) def doStart(self, pos): self.doWriteScatteringsense(self.scatteringsense) qh, qk, ql, ny = pos ny = self._thz(ny) angles = self._attached_cell.cal_angles( [qh, qk, ql], ny, self.scanmode, self.scanconstant, self.scatteringsense[1], self.axiscoupling, self.psi360) mono, ana, phi, psi, alpha = self._attached_mono, self._attached_ana, \ self._attached_phi, self._attached_psi, self._attached_alpha self.log.debug('moving phi/stt to %s', angles[2]) phi.start(angles[2]) self.log.debug('moving psi/sth to %s', angles[3]) psi.start(angles[3]) if alpha is not None: self.log.debug('moving alpha to %s', angles[4]) alpha.start(angles[4]) self.log.debug('moving mono to %s', angles[0]) mono.start(from_k(angles[0], mono.unit)) if self.scanmode != 'DIFF': self.log.debug('moving ana to %s', angles[1]) ana.start(from_k(angles[1], ana.unit)) # spurion check if self.spurioncheck and self._mode == SIMULATION: self._spurionCheck(pos) # store the min and max values of h,k,l, and E for simulation self._sim_setValue(pos) def doFinish(self): # make sure index members read the latest value for index in (self.h, self.k, self.l, self.E): if index._cache: index._cache.invalidate(index, 'value') def doStatus(self, maxage=0): if self.scanmode == 'DIFF': return multiStatus(((name, self._adevs[name]) for name in ['mono', 'phi', 'psi', 'alpha']), maxage) else: return multiStatus(((name, self._adevs[name]) for name in ['mono', 'ana', 'phi', 'psi', 'alpha']), maxage) def doWriteScanmode(self, val): if val == 'DIFF': self.log.warning('Switching to two-axis mode; you are responsible ' 'for moving the analyzer axes to the desired ' 'position') def doWriteScatteringsense(self, val): self._attached_mono.scatteringsense = val[0] self._attached_ana.scatteringsense = val[2] def doReadUnit(self): return 'rlu rlu rlu %s' % self.energytransferunit def doWriteEnergytransferunit(self, val): if val not in ENERGYTRANSFERUNITS: raise InvalidValueError(self, 'invalid energy transfer unit: %r' % val) if self._cache: self._cache.invalidate(self, 'value') self.unit = 'rlu rlu rlu %s' % val self.E.unit = val def valueInfo(self): return Value('h', unit='rlu', fmtstr='%.4f'), \ Value('k', unit='rlu', fmtstr='%.4f'), \ Value('l', unit='rlu', fmtstr='%.4f'), \ Value('E', unit=self.energytransferunit, fmtstr='%.4f') def doRead(self, maxage=0): mono, ana, phi, psi = self._attached_mono, self._attached_ana, \ self._attached_phi, self._attached_psi # read out position monovalue = to_k(mono.read(maxage), mono.unit) if self.scanmode == 'DIFF': hkl = self._attached_cell.angle2hkl( [monovalue, monovalue, phi.read(maxage), psi.read(maxage)], self.axiscoupling) ny = 0 else: anavalue = to_k(ana.read(maxage), ana.unit) hkl = self._attached_cell.angle2hkl( [monovalue, anavalue, phi.read(maxage), psi.read(maxage)], self.axiscoupling) ny = self._attached_cell.cal_ny(monovalue, anavalue) if self.energytransferunit == 'meV': ny *= THZ2MEV pos = [hkl[0], hkl[1], hkl[2], ny] return pos def _calpos(self, pos, printout=True, checkonly=True): qh, qk, ql, ny, sc, sm = pos ny = self._thz(ny) if sm is None: sm = self.scanmode if sc is None: sc = self.scanconstant try: angles = self._attached_cell.cal_angles( [qh, qk, ql], ny, sm, sc, self.scatteringsense[1], self.axiscoupling, self.psi360) except ComputationError as err: if checkonly: self.log.error('cannot calculate position: %s', err) return else: raise if not printout: return angles ok, why = True, '' for devname, value in zip(['mono', 'ana', 'phi', 'psi', 'alpha'], angles): dev = self._adevs[devname] if dev is None: continue if isinstance(dev, Monochromator): devok, devwhy = dev.isAllowed(from_k(value, dev.unit)) else: devok, devwhy = dev.isAllowed(value) if not devok: ok = False why += 'target position %s outside limits for %s: %s -- ' % \ (dev.format(value, unit=True), dev, devwhy) self.log.info('ki: %8.3f A-1', angles[0]) if self.scanmode != 'DIFF': self.log.info('kf: %8.3f A-1', angles[1]) self.log.info('2theta sample: %8.3f deg', angles[2]) self.log.info('theta sample: %8.3f deg', angles[3]) if self._attached_alpha is not None: self.log.info('alpha: %8.3f deg', angles[4]) if ok: self._last_calpos = pos if checkonly: self.log.info('position allowed') else: if checkonly: self.log.warning('position not allowed: %s', why[:-4]) else: raise LimitError(self, 'position not allowed: ' + why[:-4]) def _reverse_calpos(self, phi, psi, **kwds): if 'E' in kwds: ny = self._thz(kwds['E']) if self.scanmode == 'CKI': ki = self.scanconstant kf = self._attached_cell.cal_kf(ny, ki) elif self.scanmode == 'CKF': kf = self.scanconstant ki = self._attached_cell.cal_ki1(ny, kf) else: self.log.error('cannot calculate position with scanmode %s', self.scanmode) elif 'ki' in kwds or 'kf' in kwds: ki = kwds.get('ki') kf = kwds.get('kf') if not ki or not kf: self.log.error('must give both ki and kf arguments') else: ki = self._attached_mono.read() kf = self._attached_ana.read() ny = self._attached_cell.cal_ny(ki, kf) if self.energytransferunit == 'meV': ny *= THZ2MEV hkl = self._calhkl([ki, kf, phi, psi]) self.log.info('ki: %8.3f A-1', ki) self.log.info('kf: %8.3f A-1', kf) self.log.info('pos: [%.4f, %.4f, %.4f, %.4f] rlu rlu rlu %s', *(tuple(hkl) + (ny, self.energytransferunit))) def _calhkl(self, angles): return self._attached_cell.angle2hkl(angles, self.axiscoupling) def _getCollimation(self): """Return current Soller collimator acceptance angles in minutes of arc. Order of the returned list must be alpha1-alpha4 then beta1-beta4. If not installed, use '6000'. Must be overridden for instruments with collimation support. """ def to_coll(v): if v == 'open': return 6000 return int(v) try: a1, a2, a3, a4, b1, b2, b3, b4 = map(to_coll, self.collimation.split()) except Exception: try: a1, a2, a3, a4 = map(to_coll, self.collimation.split()) except Exception: self.log.warning('collimation parameter should be set to ' '"a1 a2 a3 a4 b1 b2 b3 b4", assuming open') return [6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000] else: return [a1, a2, a3, a4, 6000, 6000, 6000, 6000] else: return [a1, a2, a3, a4, b1, b2, b3, b4] def _getResolutionParameters(self): """Return a list of 30 parameters used for resolution calculation.""" return [ 0, # circular (0) or rectangular (1) source 5, # width of source / diameter (cm) 5, # height of source / diameter (cm) 0, # no guide (0) or guide (1) 1, # horizontal guide divergence (min/AA) 1, # vertical guide divergence (min/AA) 1, # cylindrical (0) or cuboid (1) sample 1, # sample width / diameter perp. to Q (cm) 1, # sample width / diameter along Q (cm) 1, # sample height (cm) 1, # circular (0) or rectangular (1) detector 2.5, # width / diameter of the detector (cm) 10, # height / diameter of the detector (cm) 0.2, # thickness of monochromator (cm) 20, # width of monochromator (cm) 20, # height of monochromator (cm) 0.2, # thickness of analyzer (cm) 15, # width of analyzer (cm) 15, # height of analyzer (cm) 200, # distance source - monochromator (cm) 200, # distance monochromator - sample (cm) 100, # distance sample - analyzer (cm) 100, # distance analyzer - detector (cm) 0, # horizontal curvature of monochromator (1/cm) 0, # vertical curvature of monochromator (1/cm) 0, # horizontal curvature of analyzer (1/cm) 0, # vertical curvature of analyzer (1/cm) 100, # distance monochromator - monitor (cm) 4, # width of monitor (cm) 10, # height of monitor (cm) ] def _spurionCheck(self, pos): for line in spurions.check_acc_bragg(self, *pos): self.log.info(line) for line in spurions.check_ho_spurions( to_k(self._attached_ana.read(), self._attached_ana.unit), pos[3] - 0.25, pos[3] + 0.25): self.log.info(line) kival = to_k(self._attached_mono.read(), self._attached_mono.unit) phival = self._attached_phi.read() for line in spurions.check_powderrays(kival, spurions.alu_hkl, phival): self.log.info(line) for line in spurions.check_powderrays(kival, spurions.copper_hkl, phival): self.log.info(line)