class VectorCoil(PowerSupply): """VectorCoil is a device to control a coil which creates a field at the sample position. Basically it is a powersupply device, working in Amps and having two additional parameters for calibration the vectorfield, for which these coil devices are used. """ parameters = { 'orientation': Param( 'Field vector which is created by this coil in ' 'mT (measured value!)', settable=True, default=(1., 1., 1.), type=tupleof(float, float, float), unit='mT', category='general'), 'calibrationcurrent': Param( 'Current in A which created the field ' 'given as Parameter orientation', settable=True, default=1., type=float, unit='A', category='general'), }
class DetectorMixin(DeviceMixinBase): """Provide the size parameter.""" parameters = { 'size': Param('size of the active detector area', type=tupleof(floatrange(0), floatrange(0)), settable=False, mandatory=False, unit='mm', default=(1000., 1000.)), }
class NOKMonitoredVoltage(Sensor): """Return a scaled and monitored Analogue value. Also checks the value to be within certain limits, if not, complain. """ parameters = { 'reflimits': Param('None or Limits to check the reference: low, warn, ' 'high', type=none_or(tupleof(float, float, float)), settable=False, default=None), 'scale': Param('Scaling factor', type=float, settable=False, default=1.), } parameter_overrides = { 'unit': Override(default='V', mandatory=False), } def doInit(self, mode): if self.reflimits is not None: if not (0 <= self.reflimits[0] <= self.reflimits[1] <= self.reflimits[2]): raise ConfigurationError( self, 'reflimits must be in ascending' ' order!') def doRead(self, maxage=0): value = self.scale * Sensor.doRead(self, maxage) if self.reflimits is not None: if abs(value) > self.reflimits[2]: raise HardwareError( self, 'Reference voltage (%.2f) above ' 'maximum (%.2f)' % (value, self.reflimits[2])) if abs(value) < self.reflimits[0]: raise HardwareError( self, 'Reference voltage (%.2f) below ' 'minimum (%.2f)' % (value, self.reflimits[0])) if abs(value) < self.reflimits[1]: self.log.warning( 'Reference voltage (%.2f) seems rather low, ' 'should be above %.2f', value, self.reflimits[1]) return value def doStatus(self, maxage=0): try: self.doRead(maxage) return Sensor.doStatus(self, maxage) except HardwareError as err: return status.ERROR, repr(err)
def test_tupleof(): assert tupleof(int, str, float)((1.0, 1.0, 1.0)) == (1, '1.0', 1.0) assert tupleof(int, str, float)() == (0, '', 0.0) assert tupleof(float, float)() == (0.0, 0.0) assert tupleof(int, int, int, int)(array((1, 2, 3, 4))) == (1, 2, 3, 4) assert raises(ValueError, tupleof(int, str), ('a', 'b')) assert raises(ValueError, tupleof(int, str), ('a', )) assert raises(ValueError, tupleof(int, str), 'x') assert raises( ProgrammingError, tupleof, )
class HexapodSpecial(PyTangoDevice, Device): """Ohe Hexapod Device for Workspace Configuration.""" parameters = { "workspaces": Param("Hexapod workspaces list containing tuples of " "(id, [xn, xp, yn, yp, zn, zp, rzn, rzp, ryn, ryp, " "rxn, rxp, tx, ty, tz, rz, ry, rx])", type=listof(tupleof(int, listof(float))), mandatory=True, settable=False) } def doInit(self, mode): if any(idx < 10 or idx > 19 for idx, _ in self.workspaces): raise ConfigurationError("Workspace ids muste be in 10..19 " "(Jülich workspace range)") if mode != SIMULATION: workspaces = self._dev.workspaces # Tango get workspaces for wsid, ws in self.workspaces: workspaces[wsid] = ws self._dev.workspaces = workspaces # Tango set workspaces
class CalibratedMagnet(HasLimits, Moveable): """Base clase for magnet supplies having an bipolar current source. Use this for magnets which can be calibrated, i.e. where:: B(I) = Ic0 + c1*erf(c2*I) + c3*atan(c4*I) works reasonably well. Coefficients c0..c4 are given as 'calibration' parameter. """ attached_devices = { 'currentsource': Attach('bipolar Powersupply', Moveable), } parameters = { 'ramp': Param('Target rate of field change per minute', unit='main/min', mandatory=False, settable=True, volatile=True), 'calibration': Param( 'Coefficients for calibration ' 'function: [c0, c1, c2, c3, c4] calculates ' 'B(I) = c0*I + c1*erf(c2*I) + c3*atan(c4*I)' ' in T', type=tupleof(float, float, float, float, float), default=(1.0, 0.0, 0.0, 0.0, 0.0), settable=True, chatty=True), } parameter_overrides = { 'unit': Override(mandatory=False, default='T'), 'abslimits': Override(mandatory=False, volatile=True), } def _current2field(self, current, *coefficients): """Return field in T for given current in A. Should be monotonic and asymetric or _field2current will fail! Note: This may be overridden in derived classes. """ v = coefficients or self.calibration if len(v) != 5: self.log.warning('Wrong number of coefficients in calibration ' 'data! Need exactly 5 coefficients!') return v[0]*current + v[1]*math.erf(v[2]*current) + \ v[3]*math.atan(v[4]*current) def _field2current(self, field): """Return required current in A for requested field in T. Note: This may be overridden in derived classes. """ # binary search/bisection maxcurr = self._attached_currentsource.abslimits[1] mincurr = -maxcurr maxfield = self._current2field(maxcurr) minfield = self._current2field(mincurr) if not minfield <= field <= maxfield: raise LimitError( self, 'requested field %g %s out of range %g..%g %s' % (field, self.unit, minfield, maxfield, self.unit)) res = fsolve(lambda cur: self._current2field(cur) - field, 0)[0] self.log.debug('current for %g %s is %g', field, self.unit, res) return res def doRead(self, maxage=0): return self._current2field(self._attached_currentsource.read(maxage)) def doStart(self, target): self._attached_currentsource.start(self._field2current(target)) def doStop(self): self._attached_currentsource.stop() def doReset(self): return self._attached_currentsource.reset() def doReadRamp(self): # This is an approximation! return self.calibration[0] * abs(self._attached_currentsource.ramp) def doWriteRamp(self, newramp): # This is an approximation! self._attached_currentsource.ramp = newramp / self.calibration[0] def doReadAbslimits(self): minfield, maxfield = [ self._current2field(I) for I in self._attached_currentsource.abslimits ] # include 0 in allowed range if minfield > 0: minfield = 0 if maxfield < 0: maxfield = 0 # get configured limits if any, or take max from source limits = self._config.get('abslimits', (minfield, maxfield)) # in any way, clamp limits to what the source can handle limits = [clamp(i, minfield, maxfield) for i in limits] return min(limits), max(limits) def doWriteUserlimits(self, limits): HasLimits.doWriteUserlimits(self, limits) # all Ok, set source to max of pos/neg field current maxcurr = max(self._field2current(i) for i in limits) mincurr = min(self._field2current(i) for i in limits) self._attached_currentsource.userlimits = (mincurr, maxcurr) def doTime(self, startval, target): # get difference in current delta = abs( self._field2current(target) - self._field2current(startval)) # ramp is per minute, doTime should return seconds return 60 * delta / self._attached_currentsource.ramp @usermethod def calibrate(self, fieldcolumn, *scannumbers): """Calibrate the B to I conversion, argument is one or more scan numbers. The first argument specifies the name of the device which should be used as a measuring device for the field. Example: >>> B_mira.calibrate(Bf, 351) """ scans = session.experiment.data.getLastScans() self.log.info('determining calibration from scans, please wait...') Is = [] Bs = [] currentcolumn = self._attached_currentsource.name # XXX(dataapi): adapt to new Dataset class for scan in scans: if scan.counter not in scannumbers: continue if fieldcolumn not in scan.ynames or \ currentcolumn not in scan.xnames: self.log.info('%s is not a calibration scan', scan.counter) continue xindex = scan.xnames.index(currentcolumn) yindex = scan.ynames.index(fieldcolumn) yunit = scan.yunits[yindex] if yunit == 'T': factor = 1.0 elif yunit == 'mT': factor = 1e-3 elif yunit == 'uT': factor = 1e-6 elif yunit == 'G': factor = 1e-4 elif yunit == 'kG': factor = 1e-1 else: raise NicosError( self, 'unknown unit for B field ' 'readout: %r' % yunit) for xr, yr in zip(scan.xresults, scan.yresults): Is.append(xr[xindex]) Bs.append(yr[yindex] * factor) if not Is: self.log.error('no calibration data found') return fit = Fit('calibration', self._current2field, ['c%d' % i for i in range(len(self.calibration))], [1] * len(self.calibration)) res = fit.run(Is, Bs, [1] * len(Bs)) if res._failed: self.log.warning('fit failed') return self.calibration = res._pars[1]
class AndorHFRCamera(PyTangoDevice, UsesFastshutter, ImageChannelMixin, ActiveChannel): """Camera communicating with Andor Basic script.""" TRIGGER_MODES = { 'internal': 0, 'external': 1, 'external start': 6, 'external exposure': 7, 'external exposure FVB': 9, } SHUTTER_MODES = { 'rolling': 0, 'global': 1, } parameters = { 'bin': Param('Binning (x,y)', type=tupleof(intrange(1, 64), intrange(1, 64)), settable=True, default=(1, 1), category='general'), 'triggermode': Param('Triggering signal definition', type=oneof(*TRIGGER_MODES), settable=True, default='internal', category='general'), 'shuttermode': Param('Shutter mode setting', type=oneof(*SHUTTER_MODES), settable=True, default='rolling', category='general'), 'spooldirectory': Param('Path to spool the images on the remote ' 'machine', type=absolute_win_path, category='general'), 'extratime': Param('Extra sleeping time to sync with Andors Basic', type=floatrange(0), default=3, settable=True), '_started': Param('Cached counting start flag', type=none_or(float), default=None, settable=True, internal=True), } def doPreinit(self, mode): PyTangoDevice.doPreinit(self, mode) self.log.info('Checking if camera script is ready!') try: msg = self._dev.communicate('ready?') if msg.strip() != 'ready!': raise CommunicationError( self, 'Camera script gave wrong ' 'answer - please check!') except NicosError: self.log.warning('Camera is not responding - please start ' 'tomography script on camera first!') raise def doReset(self): self.log.info('Checking if camera script is ready!') try: msg = self._dev.communicate('ready?') if msg.strip() != 'ready!': raise CommunicationError( self, 'Camera script gave wrong ' 'answer - please check!') except NicosError: self.log.warning('Camera is not responding - please start ' 'tomography script on camera first!') raise def doInit(self, mode): self._started = None def valueInfo(self): # no readresult by default return () def doStart(self): self.bin = self.bin self.shuttermode = self.shuttermode self.triggermode = self.triggermode self.doWriteSpooldirectory(self.spooldirectory) kct = float(self._query_value('GetKineticCycleTime')) self.log.info('kct: %s', kct) self._counttime = self._knumber * kct + 3 self.log.info('measure time = %s s', self._counttime) self.openFastshutter() self._write_command('count') self._started = currenttime() self.log.debug('started: %s', self._started) def doSetPreset(self, **presets): for preset, val in presets.items(): self._write_presets(preset, val) self._write_presets( 'spoolfile', presets.get('spoolfile', session.experiment.sample.samplename)) def presetInfo(self): return ['exptime', 'number', 'cycletime', 'spoolfile'] def doStatus(self, maxage=0): if self._started: if (self._started + self._counttime) > currenttime(): return status.BUSY, 'counting' return status.OK, 'idle' def doStop(self): self._started = None self.status(0) self._attached_fastshutter.maw('closed') def doFinish(self): self._started = None # self._attached_fastshutter.maw('closed') def doWriteTriggermode(self, value): self._write_command('SetTriggerMode %d' % self.TRIGGER_MODES[value]) def doWriteShuttermode(self, value): self._write_command('SetShutterMode %d' % self.SHUTTER_MODES[value]) def doWriteSpooldirectory(self, value): self._write_command('SetSpoolDirectory %s' % value) def doWriteBin(self, value): self._write_command('SetHBin %d' % value[0]) self._write_command('SetVBin %d' % value[1]) def _write_command(self, command): ret = self._dev.communicate(command) if not ret.startswith('ACK'): if ret.startswith('ERR'): raise InvalidValueError(self, 'Command: %s is invalid ' % command) raise InvalidValueError( self, 'Command: %s has invalid ' 'parameters' % command) def _query_value(self, request): return self._dev.communicate(request) def _write_presets(self, preset, value): if preset == 'exptime': self._write_command('SetExposureTime %f' % value) elif preset == 'number': self._write_command('SetKineticNumber %d' % value) self._knumber = int(value) elif preset == 'cycletime': self._write_command('SetKineticCycleTime %f' % value) elif preset == 'spoolfile': self._write_command('SetSpoolFileName %s' % value)
class GuideField(MappedMoveable): """Guidefield object. Needs to be switched to (re-)calculate the required currents for the coils. Calibration is done with a matrix giving the field components created with a given current. Needs the alpha virtual motor for calculations. """ attached_devices = { 'alpha': Attach('Device which provides the current \\alpha', AlphaStorage), 'coils': Attach('List of 3 devices used for the vector field', VectorCoil, multiple=3), } parameter_overrides = { 'mapping': Override(mandatory=False, type=dict, default={ 'off': None, 'perp': (1., 0., 0.), '-perp': (-1., 0., 0.), 'par': (0., 1., 0.), '-par': (0., -1., 0.), 'z': (0., 0., 1.), '-z': (0., 0., -1.), 'up': (0., 0., 1.), 'down': (0., 0., -1.), '0': (0., 0., 0.), }), 'precision': Override(mandatory=False), } parameters = { 'background': Param( 'Static magnetic field which is always present and' ' should be corrected', type=tupleof(float, float, float), unit='mT', settable='True', default=(0., 0., 0.), category='general'), 'field': Param('Absolute value of the desired field at the ' 'sample position', type=floatrange(0.1, 100), unit='mT', settable='True', default=25., category='general'), } _currentmatrix = None _currentmatrix_inv = None @lazy_property def coils(self): return self._attached_coils @lazy_property def alpha(self): return self._attached_alpha def doInit(self, mode): MappedMoveable.doInit(self, mode) M = np.zeros([3, 3]) for i in range(3): for j in range(3): M[i, j] = (self.coils[j].orientation[i] / self.coils[j].calibrationcurrent) self._currentmatrix = M self._currentmatrix_inv = np.linalg.inv(M) self.alpha._callback = self._alphaCallBack def doShutdown(self): self.alpha._callback = None def _alphaCallBack(self): if self.target in self.mapping: self.doStart(self.target) def _startRaw(self, orient): if orient: orient = np.array(orient) # set requested field (may try to compensate background) self._setfield(self.field * orient) else: # switch off completely self.coils[0].doStart(0.0) self.coils[1].doStart(0.0) self.coils[2].doStart(0.0) # no _readRaw, as self.target is the unmapped (Higher level) value def doRead(self, maxage=0): return self.target def _B2I(self, B=np.array([0.0, 0.0, 0.0])): r"""Rotate the requested field around z-axis by beta. First we get alpha from the spectrometer: alpha is the angle between X-axis and \\vec{Q} and is in degrees. """ # read alpha, calculate beta alpha = self.alpha.read(0) beta = np.radians(90 - alpha) R = np.array([[np.cos(beta), -np.sin(beta), 0.0], [np.sin(beta), np.cos(beta), 0.0], [0.0, 0.0, 1.0]]) temp = np.dot(self._currentmatrix_inv, np.dot(R, B)) return temp def _I2B(self, I=np.array([0.0, 0.0, 0.0])): """Calculate field from currents and rotate field around z-axis by -beta. """ # read alpha, calculate beta alpha = self.alpha.read(0) beta = np.radians(90 - alpha) RR = np.array([[np.cos(beta), -np.sin(beta), 0.0], [np.sin(beta), np.cos(beta), 0.0], [0.0, 0.0, 1.0]]) return np.dot(RR, np.dot(self._currentmatrix, I)) def _setfield(self, B=np.array([0, 0, 0])): r"""Set the given field. Field components are: * Bqperp: component perpendicular to q, but within the scattering plane * Bqpar: component parallel to q (within scattering plane) * Bz: component perpendicular to the scattering plane (If TwoTheta==0 & \\hbar\\omega=0 then this coordinate-system is the same as the XYZ of the coils.) """ # subtract offset (The field, which is already there, doesn't need to # be generated....) B = B - np.array(self.background) F = self._B2I(B) # compute currents for requested field # now check limits valueOk = True for i, d in enumerate(self.coils): check = d.isAllowed(F[i]) if not check[0]: self.log.error('Can\'t set %s to %s: %s', d, d.format(F[i], unit=True), check[1]) valueOk = False if not valueOk: raise LimitError(check[1]) # go there for i, d in enumerate(self.coils): d.start(F[i])
class CascadeDetector(VirtualImage): _perfoil = 16 parameters = { 'mode': Param('Data acquisition mode (tof or image)', type=oneof('tof', 'image'), settable=True, default='image', category='presets'), 'roi': Param('Region of interest, given as (x1, y1, x2, y2)', type=tupleof(int, int, int, int), default=(-1, -1, -1, -1), settable=True), 'tofchannels': Param('Total number of TOF channels to use', type=intrange(1, 1024), default=128, settable=True, category='presets'), 'foilsorder': Param('Usable foils, ordered by number.', type=listof(intrange(0, 31)), settable=False, default=[0, 1, 2, 3, 4, 5, 6, 7], category='instrument'), 'fitfoil': Param('Foil for contrast fitting (number BEFORE ' 'resorting)', type=int, default=0, settable=True), } def doInit(self, mode): self._buf = self._generate(0).astype('<u4') def doUpdateMode(self, value): self._dataprefix = (value == 'image') and 'IMAG' or 'DATA' self._datashape = (value == 'image') and self.sizes or \ (self._perfoil * len(self.foilsorder), ) + self.sizes # (self.tofchannels,) self._tres = (value == 'image') and 1 or self.tofchannels def doStart(self): self.readresult = [0, 0] VirtualImage.doStart(self) def valueInfo(self): if self.mode == 'tof': return (Value(self.name + '.roi', unit='cts', type='counter', errors='sqrt', fmtstr='%d'), Value(self.name + '.total', unit='cts', type='counter', errors='sqrt', fmtstr='%d'), Value('fit.contrast', unit='', type='other', errors='next', fmtstr='%.3f'), Value('fit.contrastErr', unit='', type='error', errors='none', fmtstr='%.3f'), Value('fit.avg', unit='', type='other', errors='next', fmtstr='%.1f'), Value('fit.avgErr', unit='', type='error', errors='none', fmtstr='%.1f'), Value('roi.contrast', unit='', type='other', errors='next', fmtstr='%.3f'), Value('roi.contrastErr', unit='', type='error', errors='none', fmtstr='%.3f'), Value('roi.avg', unit='', type='other', errors='next', fmtstr='%.1f'), Value('roi.avgErr', unit='', type='error', errors='none', fmtstr='%.1f')) return (Value(self.name + '.roi', unit='cts', type='counter', errors='sqrt', fmtstr='%d'), Value(self.name + '.total', unit='cts', type='counter', errors='sqrt', fmtstr='%d')) @property def arraydesc(self): if self.mode == 'image': return ArrayDesc('data', self._datashape, '<u4', ['X', 'Y']) return ArrayDesc('data', self._datashape, '<u4', ['T', 'X', 'Y']) def doReadArray(self, quality): # get current data array from detector, reshape properly data = VirtualImage.doReadArray(self, quality) # determine total and roi counts total = data.sum() if self.roi != (-1, -1, -1, -1): x1, y1, x2, y2 = self.roi roi = data[..., y1:y2, x1:x2].sum() else: x1, y1, x2, y2 = 0, 0, data.shape[-1], data.shape[-2] roi = total if self.mode == 'image': self.readresult = [roi, total] return data data = np.array([data] * self._datashape[0]) # demux timing into foil + timing nperfoil = self._datashape[0] // len(self.foilsorder) shaped = data.reshape((len(self.foilsorder), nperfoil) + self._datashape[1:]) # nperfoil = self.tofchannels // self.foils # shaped = data.reshape((self.foils, nperfoil) + self._datashape[1:]) x = np.arange(nperfoil) ty = shaped[self.fitfoil].sum((1, 2)) ry = shaped[self.fitfoil, :, y1:y2, x1:x2].sum((1, 2)) self.log.debug('fitting %r and %r' % (ty, ry)) self.readresult = [ roi, total, ] # also fit per foil data and pack everything together to be send # via cache for display payload = [] for foil in self.foilsorder: foil_tot = shaped[foil].sum((1, 2)) foil_roi = shaped[foil, :, y1:y2, x1:x2].sum((1, 2)) tpopt, tperr, _ = fit_a_sin_fixed_freq(x, foil_tot) rpopt, rperr, _ = fit_a_sin_fixed_freq(x, foil_roi) payload.append([ tpopt, tperr, foil_tot.tolist(), rpopt, rperr, foil_roi.tolist() ]) self._cache.put(self.name, '_foildata', payload, flag=FLAG_NO_STORE) return data
class DoubleMotorBeckhoffNOK(DoubleMotorBeckhoff): """NOK using two axes. """ parameters = { 'mode': Param('Beam mode', type=oneof(*MODES), settable=True, userparam=True, default='ng', category='experiment'), 'nok_motor': Param('Position of the motor for this NOK', type=tupleof(float, float), settable=False, unit='mm', category='general'), 'inclinationlimits': Param('Allowed range for the positional ' 'difference', type=limits, mandatory=True), 'backlash': Param('Backlash correction in phys. units', type=float, default=0., unit='main'), 'offsets': Param('Offsets of NOK-Motors (reactor side, sample side)', type=tupleof(float, float), default=(0., 0.), settable=False, unit='main', category='offsets'), } parameter_overrides = { 'precision': Override(type=floatrange(0, 100)), 'masks': Override(type=dictwith(**{name: float for name in MODES}), unit='', mandatory=True), 'address': Override(mandatory=False), } valuetype = tupleof(float, float) _honor_stop = True def doInit(self, mode): for name, idx in [('reactor', 0), ('sample', 1)]: self.__dict__[name] = SingleMotorOfADoubleMotorNOK( '%s.%s' % (self.name, name), unit=self.unit, both=self, lowlevel=True, index=idx) def doWriteMode(self, mode): self.log.debug('DoubleMotorBeckhoffNOK arg:%s self:%s', mode, self.mode) target = self.doRead(0) self.log.debug('DoubleMotorBeckhoffNOK target %s', target) target = [pos + self.masks[mode] for pos in target] self.log.debug('DoubleMotorBeckhoffNOK target %s', target) DoubleMotorBeckhoff.doStart(self, target) def doRead(self, maxage=0): self.log.debug('DoubleMotorBeckhoffNOK read') return [ pos - self.masks[self.mode] for pos in DoubleMotorBeckhoff.doRead(self, maxage) ] def doIsAllowed(self, target): self.log.debug('DoubleMotorBeckhoffNOK doIsAllowed') target_r, target_s = (target[0] + self.offsets[0], target[1] + self.offsets[1]) incmin, incmax = self.inclinationlimits inclination = target_s - target_r if not incmin < inclination < incmax: return False, 'Inclination %.2f out of limit (%.2f, %.2f)!' % ( inclination, incmin, incmax) # no problems detected, so it should be safe to go there.... return True, '' def doIsAtTarget(self, pos, target): self.log.debug('DoubleMotorBeckhoffNOK doIsAtTarget') stat = self.status(0) if stat[0] == status.BUSY: return False return True def doStart(self, target): self.log.debug('DoubleMotorBeckhoffNOK doStart') self.log.debug('target %s %s', target, type(target)) target = [pos + self.masks[self.mode] for pos in target] self.log.debug('target %s %s', target, type(target)) DoubleMotorBeckhoff.doStart(self, target)
class DetAngle(HasLimits, Moveable): """Angle of the outgoing (centered) beam to detector.""" attached_devices = { 'tubeangle': Attach('Angle of the tube to the ground', Moveable), 'tubepos': Attach('Position of detector inside tube', Readable), 'pivot': Attach('Position of the pivot point', Readable), 'theta': Attach('Tilt angle of the sample', Readable, optional=True), } parameters = { 'pivot1pos': Param('Distance of the pivot point 1 from wall', type=floatrange(0), mandatory=False, default=290, unit='mm'), 'b2pos': Param('Distance of B2 aperture from wall', type=floatrange(0), mandatory=False, default=165, unit='mm'), 'b3pos': Param('Distance of B3 aperture from wall', type=floatrange(0), mandatory=False, default=285, unit='mm'), 'samplepos': Param('Distance of sample from B3', type=floatrange(0), mandatory=False, default=50, unit='mm'), 'detheight': Param('Height of the detector', type=floatrange(0), mandatory=False, default=533.715, unit='mm'), # calculated from the beamheight - pivot.height - (256 - 160) * 2.093 mm # Beam height at 0 deg is in pixel 160 counted from top pixel (256) # times pixel size (2.093 mm) 'detoffset': Param('Offset from virtual tube base to lower edge of ' 'detector', type=floatrange(0), mandatory=False, default=619.772, unit='mm'), 'beamheight': Param('Height of beam above ground level', type=floatrange(0), mandatory=False, default=1193.7, unit='mm'), 'range': Param('range of angles between upper and lower detector edge', type=tupleof(float, float), volatile=True, unit='deg, deg', internal=True, preinit=False), } parameter_overrides = { 'unit': Override(mandatory=False, default='deg'), 'abslimits': Override(mandatory=False, volatile=True), } def doPreinit(self, mode): # center of the detector in respect to the tube base line self._detcenter = self.detheight / 2 + self.detoffset self._yoffset = self.beamheight - self._attached_pivot.height def doInit(self, mode): self._func = np.vectorize(self._alpha) def doRead(self, maxage=0): self._update(maxage) beta = self._attached_tubeangle.read(maxage) self.log.debug('Tube angle: %.3f', beta) alphaf = self._alpha(beta) if self._attached_theta: alphaf -= self._attached_theta.read(maxage) return alphaf def _update(self, maxage=0): pivot = self._attached_pivot # Calculate the distance between sample and pivot point self._xoffset = self.pivot1pos + pivot.grid * ( pivot.read(maxage) - 1) - (self.samplepos + self.b3pos) self.log.debug('Sample pivot distance : %.1f', self._xoffset) self._tpos = self._attached_tubepos.read(maxage) def _alpha(self, beta): beta = radians(beta) # calculate the detector center position in respect to sample x = self._xoffset + self._tpos * cos(beta) - self._detcenter * sin( beta) # calculate the height of the detector center in respect to the ground y = self._tpos * sin(beta) + self._detcenter * cos( beta) - self._yoffset return degrees(atan2(y, x)) def doStart(self, target): self._update(0) x = np.arange(self.absmin, self.absmax + 0.01, 0.01) y = self._func(x) if self._attached_theta: target += self._attached_theta.read(0) val = np.interp(target, y, x) self.log.debug('new beta: %f', val) self._attached_tubeangle.start(val) def doReadAbslimits(self): return self._attached_tubeangle.abslimits def doReadRange(self): alpha = self.doRead(0) opening = degrees(atan2(self.detheight, self._tpos)) / 2. return (alpha - opening, alpha + opening)
class HasTimeout(DeviceMixinBase): """ Mixin class for devices whose wait() should have a simple timeout. Classes using this mixin may provide a `timeoutAction` which gets called once as soon as the device times out (and a status is obtained). Any Exceptions occurring therein will not stop a script! The time at which the device will get a timeout (in case it is still in `status.BUSY` or has not reached its target value which corresponds to `isAtTarget` being False if implemented) is determined upon calling `start` and is saved to the non-userparameter `_timesout`. If you need a more dynamic timeout calculation, you may provide a `doReadTimeout` method (with volatile=True) to calculate the extra amount on the fly. The relevant timestamps are internally stored in the (non-userparam) `_timesout`, which is either set to None, if there is no nicos-initiated movement, or to an iterable of tuples describing what action is supposedly performed and at which time it should be finished. If there is a doTime method, it is used to calculate the length of an intermediary 'ramping' phase and timeout. This may be followed by other phases. If `timeout` is not None, a last phase is added which takes `timeout` seconds to time out. You may set the `timeout` parameter to None in which case the device will never time out. """ parameters = { 'timeout': Param('Time limit for the device to reach its target' ', or None', unit='s', fmtstr='%.1f', type=none_or(float), settable=True, mandatory=False, chatty=True), '_timesout': Param('Device movement should finish between these ' 'timestamps', type=none_or(nonemptylistof(tupleof(string, float))), unit='s', userparam=False, settable=True), } # derived classes may redefine this if they need different behaviour timeout_status = status.NOTREACHED # internal flag to determine if the timeout action has been executed _timeoutActionCalled = False @property def _startTime(self): # only read the parameter once from the cache db to avoid race # condition between the checks below timesout = self._timesout return timesout[0][1] if timesout else None @property def _timeoutTime(self): # see above timesout = self._timesout return timesout[-1][1] if timesout else None def _getTimeoutTimes(self, current_pos, target_pos, current_time): """Calculates timestamps for timeouts returns an iterable of tuples (status_string, timestamp) with ascending timestamps. First timestamp has to be `current_time` which is the only argument to this. The last timestamp will be used as the final timestamp to determine if the device's movement timed out or not. Additional timestamps (in between) may be set if need for _combinedStatus returning individual status text's (e.g. to differentiate between 'ramping' and 'stabilization'). """ res = [('start', current_time)] if hasattr(self, 'doTime'): res.append(('', res[-1][1] + self.doTime(current_pos, target_pos))) if self.timeout: res.append(('', res[-1][1] + self.timeout)) return res def isTimedOut(self): """Method to (only) check whether a device's movement timed out or not. Returns False unless there was a timeout in which case it returns True. """ if self.timeout is None: return False timeoutTime = self._timeoutTime if timeoutTime is not None: remaining = timeoutTime - currenttime() if remaining > 0: self.log.debug("%.2f s left before timeout", remaining) else: self.log.debug("timeout since %.2f s", -remaining) return remaining < 0 return False def resetTimeout(self, target): """Method called to reset the timeout when the device is started to a new target. """ self._timeoutActionCalled = False timesout = self._getTimeoutTimes(self.read(), target, currenttime()) self._setROParam('_timesout', timesout) def _clearTimeout(self): self._setROParam('_timesout', None) def _targetReached(self): """Clears timeout in order to suppress further timeouts after the device has reached its target. This is determined by `_combinedStatus`. Thus this will just be checked when the status is polled periodically before the device timed out. This behaviour may be changed in derived classes, e.g. `HasWindowTimeout`. """ self._clearTimeout() def _combinedStatus(self, maxage=0): """Create a combined status from doStatus, isAtTarget and timedOut If a timeout happens, use the status set by self.timeout_status and call `timeoutAction` once if defined. Pollers and other `SLAVE`s do *not* call `timeoutAction`. """ from nicos.core.device import Readable # isort:skip code, msg = Readable._combinedStatus(self, maxage) if code in (status.OK, status.WARN) and self._timeoutTime: if not self.isAtTarget(self.read(maxage)): code = status.BUSY msg = statusString('target not yet reached', msg) else: self._targetReached() if code == status.BUSY: if self.isTimedOut(): code = self.timeout_status msg = statusString('movement timed out', msg) # only call once per timeout, flag is reset in Device.start() if not self._timeoutActionCalled and \ session.mode in (MASTER, MAINTENANCE): try: if hasattr(self, 'timeoutAction'): self.timeoutAction() self._timeoutActionCalled = True return self._combinedStatus(maxage) except Exception: self.log.exception('error calling timeout action', exc=True) finally: self._timeoutActionCalled = True elif self._timesout: # give indication about the phase of the movement for m, t in self._timesout or []: if t > currenttime(): msg = statusString(m, msg) break elif code == status.ERROR: self._timeoutActionCalled = True return (code, msg)
class DoubleMotorNOK(SequencerMixin, CanReference, PseudoNOK, HasPrecision, Moveable): """NOK using two axes. If backlash is negative, approach form the negative side (default), else approach from the positive side. If backlash is zero, don't mind and just go to the target. """ attached_devices = { 'motor_r': Attach('NOK moving motor, reactor side', Moveable), 'motor_s': Attach('NOK moving motor, sample side', Moveable), } parameters = { 'mode': Param('Beam mode', type=oneof(*MODES), settable=True, userparam=True, default='ng', category='experiment'), 'nok_motor': Param('Position of the motor for this NOK', type=tupleof(float, float), settable=False, unit='mm', category='general'), 'inclinationlimits': Param('Allowed range for the positional ' 'difference', type=limits, mandatory=True), 'backlash': Param('Backlash correction in phys. units', type=float, default=0., unit='main'), 'offsets': Param('Offsets of NOK-Motors (reactor side, sample side)', type=tupleof(float, float), default=(0., 0.), settable=False, unit='main', category='offsets'), } parameter_overrides = { 'precision': Override(type=floatrange(0, 100)), 'masks': Override(type=dictwith(**{name: float for name in MODES}), unit='', mandatory=True), } valuetype = tupleof(float, float) _honor_stop = True @lazy_property def _devices(self): return self._attached_motor_r, self._attached_motor_s def doInit(self, mode): for dev in self._devices: if hasattr(dev, 'backlash') and dev.backlash != 0: self.log.warning( 'Attached Device %s should not have a ' 'non-zero backlash!', dev) def doRead(self, maxage=0): return [ dev.read(maxage) - ofs - self.masks[self.mode] for dev, ofs in zip(self._devices, self.offsets) ] def doIsAllowed(self, targets): target_r, target_s = targets target_r += self.offsets[0] target_s += self.offsets[1] incmin, incmax = self.inclinationlimits inclination = target_s - target_r if not incmin <= inclination <= incmax: return False, 'Inclination %.2f out of limit (%.2f, %.2f)!' % ( inclination, incmin, incmax) for dev in self._devices: res = dev.isAllowed(target_r) if not res[0]: return res # no problems detected, so it should be safe to go there.... return True, '' def doIsAtTarget(self, targets): traveldists = [ target - (akt + ofs) for target, akt, ofs in zip(targets, self.read(0), self.offsets) ] self.log.debug('doIsAtTarget', targets, 'traveldists', traveldists) return max(abs(v) for v in traveldists) <= self.precision def doStop(self): SequencerMixin.doStop(self) for dev in self._devices: dev.stop() try: self.wait() finally: self.reset() def doStart(self, targets): """Generate and start a sequence if none is running. The sequence is optimised for negative backlash. It will first move both motors to the lowest value of (target + backlash, current_position) and then to the final target. So, inbetween, the NOK should be parallel to the beam. """ if self._seq_is_running(): raise MoveError(self, 'Cannot start device, it is still moving!') # check precision, only move if needed! if self.isAtTarget(targets): return devices = self._devices # XXX: backlash correction and repositioning later # build a moving sequence sequence = [] # now go to target sequence.append([ SeqDev(d, t + ofs + self.masks[self.mode], stoppable=True) for d, t, ofs in zip(devices, targets, self.offsets) ]) # now go to target again sequence.append([ SeqDev(d, t + ofs + self.masks[self.mode], stoppable=True) for d, t, ofs in zip(devices, targets, self.offsets) ]) self.log.debug('Seq: %r', sequence) self._startSequence(sequence) def doReset(self): multiReset(self._motors)
class DoubleMotorBeckhoff(PseudoNOK, BeckhoffMotorBase): """a double motor Beckhoff knows both axis at once! It comunicats direktly by modbuss """ hardware_access = True parameters = { 'addresses': Param('addresses of each motor', type=tupleof(motoraddress, motoraddress), mandatory=True, settable=False, userparam=False), 'mode': Param('Beam mode', type=oneof(*MODES), settable=True, userparam=True, default='ng', category='experiment'), 'motortemp': Param('Motor temperature', type=tupleof(float, float), settable=False, userparam=True, volatile=True, unit='degC', fmtstr='%.1f, %.1f'), '_rawvalues': Param('Raw positions', type=tupleof(float, float), internal=True, category='experiment'), } parameter_overrides = { 'ruler': Override(type=tupleof(float, float), mandatory=True, settable=False, userparam=False), } valuetype = tupleof(float, float) def doReference(self): """Reference the NOK. Just set the do_reference bit and check for completion """ self.log.error('nope') def doReadMaxvalue(self): return 1111 def doReadMinvalue(self): return -1111 def doReadMotortemp(self): # in degC self.log.debug('DoubleMotorBeckhoff doReadMotortemp') return self._HW_readParameter('motorTemp') def doReadFirmware(self): # TODO self._HW_readParameter('firmwareVersion')) return 'V%.1f' % (0.1 * 0) def doReadVmax(self): # TODO self._speed2phys(self._HW_readParameter('vMax')) return 0 def doReadRefpos(self): # TODO self._steps2phys(self._HW_readParameter('refPos')) return 0 def doReadPotentiometer(self): return [self._fit(a) for a in self._HW_readParameter('potentiometer')][0] def _writeUpperControlWord(self, value): self.log.debug('_writeUpperControlWord 0x%04x', value) value = int(value) & 0xffff self._dev.WriteOutputWord((self.addresses[0] + 1, value)) self._dev.WriteOutputWord((self.addresses[1] + 1, value)) def _readReturn(self): value = [] for i in range(2): valuel = self._dev.ReadOutputWords((self.addresses[i] + 8, 2)) self.log.debug('_readReturn %d: -> %d (0x%08x)', i, valuel, valuel) value.append(struct.unpack('=i', struct.pack('<2H', *valuel))[0]) return value def _HW_readParameter(self, index): if index not in self.HW_readable_Params: raise UsageError('Reading not possible for parameter index %s' % index) index = self.HW_readable_Params.get(index, index) self.log.debug('readParameter %d', index) for i in range(10): self._writeUpperControlWord((index << 8) | 4) session.delay(0.15) stat = self._areadStatusWord(self.addresses[0]) self.log.debug('readStatusWord 0 %d', stat) stat = self._areadStatusWord(self.addresses[0]) self.log.debug('readStatusWord 1 %d', stat) if (stat & 0x8000) != 0: if i > 0: self.log.debug('readParameter %d', i + 1) return self._readReturn() if (stat & 0x4000) != 0: raise UsageError( self, 'NACK ReadPar command not recognized ' 'by HW, please retry later...') raise UsageError( self, 'ReadPar command not recognized by HW, please ' 'retry later ...') def _areadPosition(self, address): value = self._dev.ReadOutputWords((address + 6, 2)) value = struct.unpack('=i', struct.pack('<2H', *value))[0] self.log.debug('_readPosition: -> %d steps', value) return value def _awriteDestination(self, value, address): self.log.debug('_writeDestination %r', value) value = struct.unpack('<2H', struct.pack('=i', value)) self._dev.WriteOutputWords(tuple([address + 2]) + value) def _awriteControlBit(self, bit, value, numbits, address): self.log.debug('_writeControlBit %r, %r', bit, value) self._dev.WriteOutputWord( (address, (value & ((1 << int(numbits)) - 1)) << int(bit))) session.delay(0.1) # work around race conditions.... def _asteps2phys(self, steps, ruler): value = steps / float(self.slope) - ruler self.log.debug('_steps2phys ruler: %r steps -> %s', steps, self.format(value, unit=True)) return value def _aphys2steps(self, value, ruler): steps = int((value + ruler) * float(self.slope)) self.log.debug('_phys2steps ruler: %s -> %r steps', self.format(value, unit=True), steps) return steps def _indexReadPosition(self, i): return self._asteps2phys(self._areadPosition(self.addresses[i]), self.ruler[i]) def _areadStatusWord(self, address): value = self._dev.ReadOutputWords((address + 4, 1))[0] self.log.debug('_areadStatusWord 0x%04x', value) return value def _areadErrorWord(self, address): value = self._dev.ReadOutputWords((address + 5, 1))[0] self.log.debug('_readErrorWord 0x%04x', value) return value def _indexHW_status(self, i): """Used status bits.""" # read HW values errval = self._areadErrorWord(self.addresses[i]) statval = (self._areadStatusWord(self.addresses[i]) ^ self.HW_Status_Inv) & ~self.HW_Status_Ign msg = bitDescription(statval, *self.HW_Statusbits) self.log.debug('HW_status: (%x) %s', statval, msg) if errval: errnum = errval return status.ERROR, 'ERROR %d: %s, %s' % ( errnum, self.HW_Errors.get(errnum, 'Unknown Error {0:d}'.format(errnum)), msg) for mask, stat in self.HW_Status_map: if statval & mask: return stat, msg return status.OK, msg if msg else 'Ready' def _HW_wait_while_HOT(self): sd = 6.5 anz = int(round(self.waittime * 60 / sd)) # Pech bei 2.session for a in range(anz): temp = max(self.motortemp) if temp < self.maxtemp: # wait if temp>33 until temp<26 if a: self.log.info('%d Celsius continue', temp) else: self.log.debug('%d Celsius continue', temp) return True self.log.info('%d Celsius Timeout in: %.1f min', temp, (anz - a) * sd / 60) session.delay(sd) raise NicosTimeoutError('HW still HOT after {0:d} min'.format( self.waittime)) def doRead(self, maxage=0): self.log.debug('DoubleMotorBeckhoff read') res = [self._indexReadPosition(0), self._indexReadPosition(1)] self.log.debug('%s', res) self._setROParam('_rawvalues', res) return res def doStatus(self, maxage=0): self.log.debug('DoubleMotorBeckhoff status') lowlevel = BaseSequencer.doStatus(self, maxage) if lowlevel[0] == status.BUSY: return lowlevel akt = [self._indexHW_status(0), self._indexHW_status(1)] self.log.debug('Status: %s', akt) msg = [st[1] for st in akt] self.log.debug('Status: %s', msg) return (max([st[0] for st in akt]), ', '.join(msg) if msg.count(msg[0]) == 1 else msg[0]) def _generateSequence(self, target, indexes, code): self.log.debug('DoubleMotorBeckhoff Seq generated %s %s 0x%X', target, indexes, code) seq = [SeqMethod(self, '_HW_wait_while_HOT')] for i in indexes: seq.append( SeqMethod(self, '_awriteDestination', self._aphys2steps(target[i], self.ruler[i]), self.addresses[i])) seq.append( SeqMethod(self, '_awriteControlBit', 0, code, 10, self.addresses[i])) return seq def doStart(self, target): self.log.debug('DoubleMotorBeckhoff move to %s', target) if self._seq_is_running(): raise MoveError(self, 'Cannot start device, it is still moving!') self._startSequence(self._generateSequence(target, [0, 1], 0x304))
def _tuple(members, **kwds): return tupleof(*(get_validator(**m) for m in members))
class ImagePlateImage(ImageChannelMixin, PassiveChannel): """Represents the client to a MAATEL Image Plate Detector.""" MAP_SHAPE = { 125: (900, 10000), 250: (900, 5000), 500: (900, 2500), } attached_devices = { "imgdrum": Attach("Image Plate Detector Drum " "control device.", ImagePlateDrum), } parameters = { "erase": Param("Erase image plate on next start?", type=bool, settable=True, mandatory=False, default=True), "roi": Param("Region of interest", type=tupleof(int, int, int, int), default=(0, 0, 0, 0), settable=True, volatile=True, category="general"), "pixelsize": Param("Pixel size in microns", type=oneof(125, 250, 500), default=500, settable=True, volatile=True, category="general"), "file": Param("Image file location on maatel computer", type=str, settable=True, volatile=True, category="general"), "readout_millis": Param("Timeout in ms for the readout", type=int, settable=True, default=60000), } def doInit(self, mode): self.arraydesc = ArrayDesc('data', self.MAP_SHAPE[self.pixelsize], numpy.uint16) def doPrepare(self): # erase and expo position if self.erase: self._attached_imgdrum.maw(ImagePlateDrum.POS_ERASE) self._attached_imgdrum.maw(ImagePlateDrum.POS_EXPO) def valueInfo(self): # no readresult -> no values return () def doReadArray(self, quality): if quality == FINAL: # start readout self._attached_imgdrum.maw(ImagePlateDrum.POS_READ) narray = None timeout = self._attached_imgdrum._dev.get_timeout_millis() self._attached_imgdrum._dev.set_timeout_millis(self.readout_millis) try: narray = self._attached_imgdrum._dev.Bitmap16Bit finally: self._attached_imgdrum._dev.set_timeout_millis(timeout) return narray return None def doReadRoi(self): return (0, self._attached_imgdrum._dev.InterestZoneY, 1250, self._attached_imgdrum._dev.InterestZoneH) def doReadPixelsize(self): return self._attached_imgdrum._dev.PixelSize def doReadFile(self): return self._attached_imgdrum._dev.ImageFile def doWriteRoi(self, value): self.log.warning("setting x offset and width are not supported " "- ignored.") self._attached_imgdrum._dev.InterestZoneY = value[1] self._attached_imgdrum._dev.InterestZoneH = value[3] def doWritePixelsize(self, value): self._attached_imgdrum._dev.PixelSize = value self.arraydesc = ArrayDesc('data', self.MAP_SHAPE[value], numpy.uint16) def doWriteFile(self, value): self._attached_imgdrum._dev.ImageFile = value
class TofChannel(PyTangoDevice, ImageChannelMixin, PassiveChannel): """Basic Tango Device for TofDetector.""" STRSHAPE = ['x', 'y', 'z', 't'] parameters = { 'detshape': Param('Shape of tof detector', type=dictof(str, int)), 'timechannels': Param('Number of time channels - if set to 1 TOF mode ' 'is disabled', type=intrange(1, 1024), settable=True), 'divisor': Param('Width of a time channel', type=int, unit='0.1us', settable=True), 'delay': Param('Offset delay in measure begin', type=int, unit='0.1us', settable=True), 'readchannels': Param('Tuple of (start, end) channel numbers will be ' 'returned by a read', type=tupleof(int, int), default=(0, 0), settable=True, mandatory=True), 'readtimechan': Param('Tuple of (start, end) integrated time channels ' 'will be returned by a read', type=tupleof(int, int), default=(0, 0), settable=True, mandatory=True), } def doInit(self, mode): self.arraydesc = ArrayDesc('data', (self.detshape.get('x', 1), self.detshape.get('t', 1)), numpy.uint32) if mode != SIMULATION: self._dev.set_timeout_millis(10000) def doPrepare(self): self._dev.Clear() PyTangoDevice._hw_wait(self) self.log.debug("Detector cleared") self._dev.Prepare() def doStart(self): start, end = self.readchannels self.readresult = [0] * (end - start + 1) self._dev.Start() self.log.debug("Detector started") def doFinish(self): self._dev.Stop() session.delay(0.2) PyTangoDevice._hw_wait(self) def doStop(self): self._dev.Stop() def doPause(self): self.doFinish() return True def doResume(self): self.doStart() def doReadTimechannels(self): return self._dev.timeChannels def doWriteTimechannels(self, value): self._dev.timeChannels = value self._pollParam('detshape') def doReadDivisor(self): return self._dev.timeInterval def doWriteDivisor(self, value): self._dev.timeInterval = value def doReadDelay(self): return self._dev.delay def doWriteDelay(self, value): self._dev.delay = value def doReadDetshape(self): shvalue = self._dev.detectorSize return {'x': shvalue[0], 't': shvalue[3]} def valueInfo(self): start, end = self.readchannels return tuple(Value("chan-%d" % i, unit="cts", errors="sqrt", type="counter", fmtstr="%d") for i in range(start, end + 1)) def doReadArray(self, quality): self.log.debug("Tof Detector read image") start, end = self.readchannels # get current data array from detector array = numpy.asarray(self._dev.value, numpy.uint32) array = array.reshape(self.detshape['t'], self.detshape['x']) if self.timechannels > 1: startT, endT = self.readtimechan res = array[startT:endT+1].sum(axis=0)[start:end+1] else: res = array[0, start:end+1] self.readresult = res.tolist() return array