class SimpleAcq(Base, SimpleDataInterface): """ Read human readable numbers from serial port. Example config for copy-paste: simple_data_acq: module.Class: 'simple_data_acq.SimpleAcq' interface: 'ASRL1::INSTR' baudrate: 115200 """ _modclass = 'simple' _modtype = 'hardware' resource = ConfigOption('interface', 'ASRL1::INSTR', missing='warn') baudrate = ConfigOption('baudrate', 115200, missing='warn') def on_activate(self): """ Activate module. """ self.rm = visa.ResourceManager() self.log.debug('Resources: {0}'.format(self.rm.list_resources())) self.my_instrument = self.rm.open_resource(self.resource, baud_rate=self.baudrate) def on_deactivate(self): """ Deactivate module. """ self.my_instrument.close() self.rm.close() def getData(self): """ Read one value from serial port. @return int: vaue form serial port """ try: return int(self.my_instrument.read_raw().decode( 'utf-8').rstrip().split()[1]) except: return 0 def getChannels(self): """ Number of channels. @return int: number of channels """ return 1
class InfluxDataClient(Base, ProcessInterface): """ Retrieve live data from InfluxDB as if the measurement device was connected directly. Example config for copy-paste: influx_data_client: module.Class: 'influx_data_client.InfluxDataClient' user: '******' password: '******' dbname: 'db_name' host: 'localhost' port: 8086 dataseries: 'data_series_name' field: 'field_name' criterion: 'criterion_name' """ _modclass = 'InfluxDataClient' _modtype = 'hardware' user = ConfigOption('user', missing='error') pw = ConfigOption('password', missing='error') dbname = ConfigOption('dbname', missing='error') host = ConfigOption('host', missing='error') port = ConfigOption('port', default=8086) series = ConfigOption('dataseries', missing='error') field = ConfigOption('field', missing='error') cr = ConfigOption('criterion', missing='error') def on_activate(self): """ Activate module. """ self.connect_db() def on_deactivate(self): """ Deactivate module. """ del self.conn def connect_db(self): """ Connect to Influx database """ self.conn = InfluxDBClient(self.host, self.port, self.user, self.pw, self.dbname) def get_process_value(self): """ Return a measured value """ q = 'SELECT last({0}) FROM {1} WHERE (time > now() - 10m AND {2})'.format( self.field, self.series, self.cr) res = self.conn.query(q) return list(res[('{0}'.format(self.series), None)])[0]['last'] def get_process_unit(self): """ Return the unit that the value is measured in @return (str, str): a tuple of ('abreviation', 'full unit name') """ return '°C', ' degrees Celsius'
class PM100D(Base, SimpleDataInterface): """ Hardware module for Thorlabs PM100D powermeter. Example config : powermeter: module.Class: 'powermeter.PM100D.PM100D' address: 'USB0::0x1313::0x8078::P0013645::INSTR' This module needs the ThorlabsPM100 package from PyPi, this package is not included in the environment To add install it, type : pip install ThorlabsPM100 in the Anaconda prompt after aving activated qudi environment """ _modclass = 'powermeter' _modtype = 'hardware' _address = ConfigOption('address', missing='error') _timeout = ConfigOption('timeout', 1) _power_meter = None def on_activate(self): """ Startup the module """ rm = visa.ResourceManager() try: self._inst = rm.open_resource(self._address, timeout=self._timeout) except: self.log.error( 'Could not connect to hardware. Please check the wires and the address.' ) self._power_meter = ThorlabsPM100(inst=self._inst) def on_deactivate(self): """ Stops the module """ self._inst.close() def getData(self): """ SimpleDataInterface function to get the power from the powermeter """ return np.array([self.get_power()]) def getChannels(self): """ SimpleDataInterface function to know how many data channel the device has, here 1. """ return 1 def get_power(self): """ Return the power read from the ThorlabsPM100 package """ return self._power_meter.read
class OceanOptics(Base, SpectrometerInterface): """ Hardware module for reading spectra from the Ocean Optics spectrometer software. Example config for copy-paste: myspectrometer: module.Class: 'spectrometer.oceanoptics_spectrometer.OceanOptics' spectrometer_serial: 'QEP01583' #insert here the right serial number. """ _serial = ConfigOption('spectrometer_serial', missing='warn') _integration_time = StatusVar('integration_time', default=10000) def on_activate(self): """ Activate module. """ self.spec = sb.Spectrometer.from_serial_number(self._serial) self.log.info(''.format(self.spec.model, self.spec.serial_number)) self.spec.integration_time_micros(self._integration_time) self.log.info('Exposure set to {} microseconds'.format( self._integration_time)) def on_deactivate(self): """ Deactivate module. """ self.spec.close() def recordSpectrum(self): """ Record spectrum from Ocean Optics spectrometer. @return []: spectrum data """ wavelengths = self.spec.wavelengths() specdata = np.empty((2, len(wavelengths)), dtype=np.double) specdata[0] = wavelengths / 1e9 specdata[1] = self.spec.intensities() return specdata def getExposure(self): """ Get exposure. @return float: exposure Not implemented. """ return self._integration_time def setExposure(self, exposureTime): """ Set exposure. @param float exposureTime: exposure time in microseconds """ self._integration_time = exposureTime self.spec.integration_time_micros(self._integration_time)
class IxonUltra(Base, CameraInterface): """ Hardware class for Andors Ixon Ultra 897 Example config for copy-paste: andor_ultra_camera: module.Class: 'camera.andor.iXon897_ultra.IxonUltra' dll_location: 'C:\\camera\\andor.dll' # path to library file default_exposure: 1.0 default_read_mode: 'IMAGE' default_temperature: -70 default_cooler_on: True default_acquisition_mode: 'SINGLE_SCAN' default_trigger_mode: 'INTERNAL' """ _modtype = 'camera' _modclass = 'hardware' _dll_location = ConfigOption('dll_location', missing='error') _default_exposure = ConfigOption('default_exposure', 1.0) _default_read_mode = ConfigOption('default_read_mode', 'IMAGE') _default_temperature = ConfigOption('default_temperature', -70) _default_cooler_on = ConfigOption('default_cooler_on', True) _default_acquisition_mode = ConfigOption('default_acquisition_mode', 'SINGLE_SCAN') _default_trigger_mode = ConfigOption('default_trigger_mode', 'INTERNAL') _exposure = _default_exposure _temperature = _default_temperature _cooler_on = _default_cooler_on _read_mode = _default_read_mode _acquisition_mode = _default_acquisition_mode _gain = 0 _width = 0 _height = 0 _last_acquisition_mode = None # useful if config changes during acq _supported_read_mode = ReadMode # TODO: read this from camera, all readmodes are available for iXon Ultra _max_cooling = -100 _live = False _camera_name = 'iXon Ultra 897' _shutter = "closed" _trigger_mode = _default_trigger_mode _scans = 1 #TODO get from camera _acquiring = False def on_activate(self): """ Initialisation performed during activation of the module. """ # self.cam.SetAcquisitionMode(1) # single # self.cam.SetTriggerMode(0) # internal # self.cam.SetCoolerMode(0) # Returns to ambient temperature on ShutDown # self.set_cooler_on_state(self._cooler_on) # self.set_exposure(self._exposure) # self.set_setpoint_temperature(self._temperature) self.dll = cdll.LoadLibrary(self._dll_location) self.dll.Initialize() nx_px, ny_px = c_int(), c_int() self._get_detector(nx_px, ny_px) self._width, self._height = nx_px.value, ny_px.value self._set_read_mode(self._read_mode) self._set_trigger_mode(self._trigger_mode) self._set_exposuretime(self._exposure) self._set_acquisition_mode(self._acquisition_mode) def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. """ self.stop_acquisition() self._set_shutter(0, 0, 0.1, 0.1) self._shut_down() def get_name(self): """ Retrieve an identifier of the camera that the GUI can print @return string: name for the camera """ return self._camera_name def get_size(self): """ Retrieve size of the image in pixel @return tuple: Size (width, height) """ return self._width, self._height def support_live_acquisition(self): """ Return whether or not the camera can take care of live acquisition @return bool: True if supported, False if not """ return False def start_live_acquisition(self): """ Start a continuous acquisition @return bool: Success ? """ if self._support_live: self._live = True self._acquiring = False return False def start_single_acquisition(self): """ Start a single acquisition @return bool: Success ? """ if self._shutter == 'closed': msg = self._set_shutter(0, 1, 0.1, 0.1) if msg == 'DRV_SUCCESS': self._shutter = 'open' else: self.log.error('shutter did not open.{0}'.format(msg)) if self._live: return -1 else: self._acquiring = True # do we need this here? msg = self._start_acquisition() if msg != "DRV_SUCCESS": return False self._acquiring = False return True def stop_acquisition(self): """ Stop/abort live or single acquisition @return bool: Success ? """ msg = self._abort_acquisition() if msg == "DRV_SUCCESS": self._live = False self._acquiring = False return True else: return False def get_acquired_data(self): """ Return an array of last acquired image. @return numpy array: image data in format [[row],[row]...] Each pixel might be a float, integer or sub pixels """ width = self._width height = self._height if self._read_mode == 'IMAGE': if self._acquisition_mode == 'SINGLE_SCAN': dim = width * height elif self._acquisition_mode == 'KINETICS': dim = width * height * self._scans elif self._acquisition_mode == 'RUN_TILL_ABORT': dim = width * height else: self.log.error('Your acquisition mode is not covered currently') elif self._read_mode == 'SINGLE_TRACK' or self._read_mode == 'FVB': if self._acquisition_mode == 'SINGLE_SCAN': dim = width elif self._acquisition_mode == 'KINETICS': dim = width * self._scans else: self.log.error('Your acquisition mode is not covered currently') dim = int(dim) image_array = np.zeros(dim) cimage_array = c_int * dim cimage = cimage_array() # this will be a bit hacky if self._acquisition_mode == 'RUN_TILL_ABORT': error_code = self.dll.GetOldestImage(pointer(cimage), dim) else: error_code = self.dll.GetAcquiredData(pointer(cimage), dim) if ERROR_DICT[error_code] != 'DRV_SUCCESS': self.log.warning('Couldn\'t retrieve an image. {0}'.format(ERROR_DICT[error_code])) else: self.log.debug('image length {0}'.format(len(cimage))) for i in range(len(cimage)): # could be problematic for 'FVB' or 'SINGLE_TRACK' readmode image_array[i] = cimage[i] image_array = np.reshape(image_array, (self._width, self._height)) self._cur_image = image_array return image_array def set_exposure(self, exposure): """ Set the exposure time in seconds @param float time: desired new exposure time @return bool: Success? """ msg = self._set_exposuretime(exposure) if msg == "DRV_SUCCESS": self._exposure = exposure return True else: return False def get_exposure(self): """ Get the exposure time in seconds @return float exposure time """ self._get_acquisition_timings() return self._exposure # not sure if the distinguishing between gain setting and gain value will be problematic for # this camera model. Just keeping it in mind for now. #TODO: Not really funcitonal right now. def set_gain(self, gain): """ Set the gain @param float gain: desired new gain @return float: new exposure gain """ n_pre_amps = self._get_number_preamp_gains() msg = '' if (gain >= 0) & (gain < n_pre_amps): msg = self._set_preamp_gain(gain) else: self.log.warning('Choose gain value between 0 and {0}'.format(n_pre_amps-1)) if msg == 'DRV_SUCCESS': self._gain = gain else: self.log.warning('The gain wasn\'t set. {0}'.format(msg)) return self._gain def get_gain(self): """ Get the gain @return float: exposure gain """ _, self._gain = self._get_preamp_gain() return self._gain def get_ready_state(self): """ Is the camera ready for an acquisition ? @return bool: ready ? """ status = c_int() self._get_status(status) if ERROR_DICT[status.value] == 'DRV_IDLE': return True else: return False # soon to be interface functions for using # a camera as a part of a (slow) photon counter def set_up_counter(self): check_val = 0 if self._shutter == 'closed': msg = self._set_shutter(0, 1, 0.1, 0.1) if msg == 'DRV_SUCCESS': self._shutter = 'open' else: self.log.error('Problems with the shutter.') check_val = -1 ret_val1 = self._set_trigger_mode('EXTERNAL') ret_val2 = self._set_acquisition_mode('RUN_TILL_ABORT') # let's test the FT mode # ret_val3 = self._set_frame_transfer(True) error_code = self.dll.PrepareAcquisition() error_msg = ERROR_DICT[error_code] if error_msg == 'DRV_SUCCESS': self.log.debug('prepared acquisition') else: self.log.debug('could not prepare acquisition: {0}'.format(error_msg)) self._get_acquisition_timings() if check_val == 0: check_val = ret_val1 | ret_val2 if msg != 'DRV_SUCCESS': ret_val3 = -1 else: ret_val3 = 0 check_val = ret_val3 | check_val return check_val def count_odmr(self, length): first, last = self._get_number_new_images() self.log.debug('number new images:{0}'.format((first, last))) if last - first + 1 < length: while last - first + 1 < length: first, last = self._get_number_new_images() else: self.log.debug('acquired too many images:{0}'.format(last - first + 1)) images = [] for i in range(first, last + 1): img = self._get_images(i, i, 1) images.append(img) self.log.debug('expected number of images:{0}'.format(length)) self.log.debug('number of images acquired:{0}'.format(len(images))) return False, np.array(images).transpose() def get_down_time(self): return self._exposure def get_counter_channels(self): width, height = self.get_size() num_px = width * height return [i for i in map(lambda x: 'px {0}'.format(x), range(num_px))] # non interface functions regarding camera interface def _abort_acquisition(self): error_code = self.dll.AbortAcquisition() return ERROR_DICT[error_code] def _shut_down(self): error_code = self.dll.ShutDown() return ERROR_DICT[error_code] def _start_acquisition(self): error_code = self.dll.StartAcquisition() self.dll.WaitForAcquisition() return ERROR_DICT[error_code] # setter functions def _set_shutter(self, typ, mode, closingtime, openingtime): """ @param int typ: 0 Output TTL low signal to open shutter 1 Output TTL high signal to open shutter @param int mode: 0 Fully Auto 1 Permanently Open 2 Permanently Closed 4 Open for FVB series 5 Open for any series """ typ, mode, closingtime, openingtime = c_int(typ), c_int(mode), c_float(closingtime), c_float(openingtime) error_code = self.dll.SetShutter(typ, mode, closingtime, openingtime) return ERROR_DICT[error_code] def _set_exposuretime(self, time): """ @param float time: exposure duration @return string answer from the camera """ error_code = self.dll.SetExposureTime(c_float(time)) return ERROR_DICT[error_code] def _set_read_mode(self, mode): """ @param string mode: string corresponding to certain ReadMode @return string answer from the camera """ check_val = 0 if hasattr(ReadMode, mode): n_mode = getattr(ReadMode, mode).value n_mode = c_int(n_mode) error_code = self.dll.SetReadMode(n_mode) if mode == 'IMAGE': self.log.debug("widt:{0}, height:{1}".format(self._width, self._height)) msg = self._set_image(1, 1, 1, self._width, 1, self._height) if msg != 'DRV_SUCCESS': self.log.warning('{0}'.format(ERROR_DICT[error_code])) if ERROR_DICT[error_code] != 'DRV_SUCCESS': self.log.warning('Readmode was not set: {0}'.format(ERROR_DICT[error_code])) check_val = -1 else: self._read_mode = mode return check_val def _set_trigger_mode(self, mode): """ @param string mode: string corresponding to certain TriggerMode @return string: answer from the camera """ check_val = 0 if hasattr(TriggerMode, mode): n_mode = c_int(getattr(TriggerMode, mode).value) self.log.debug('Input to function: {0}'.format(n_mode)) error_code = self.dll.SetTriggerMode(n_mode) else: self.log.warning('{0} mode is not supported'.format(mode)) check_val = -1 if ERROR_DICT[error_code] != 'DRV_SUCCESS': check_val = -1 else: self._trigger_mode = mode return check_val def _set_image(self, hbin, vbin, hstart, hend, vstart, vend): """ This function will set the horizontal and vertical binning to be used when taking a full resolution image. Parameters @param int hbin: number of pixels to bin horizontally @param int vbin: number of pixels to bin vertically. int hstart: Start column (inclusive) @param int hend: End column (inclusive) @param int vstart: Start row (inclusive) @param int vend: End row (inclusive). @return string containing the status message returned by the function call """ hbin, vbin, hstart, hend, vstart, vend = c_int(hbin), c_int(vbin),\ c_int(hstart), c_int(hend), c_int(vstart), c_int(vend) error_code = self.dll.SetImage(hbin, vbin, hstart, hend, vstart, vend) msg = ERROR_DICT[error_code] if msg == 'DRV_SUCCESS': self._hbin = hbin.value self._vbin = vbin.value self._hstart = hstart.value self._hend = hend.value self._vstart = vstart.value self._vend = vend.value self._width = int((self._hend - self._hstart + 1) / self._hbin) self._height = int((self._vend - self._vstart + 1) / self._vbin) else: self.log.error('Call to SetImage went wrong:{0}'.format(msg)) return ERROR_DICT[error_code] def _set_output_amplifier(self, typ): """ @param c_int typ: 0: EMCCD gain, 1: Conventional CCD register @return string: error code """ error_code = self.dll.SetOutputAmplifier(typ) return ERROR_DICT[error_code] def _set_preamp_gain(self, index): """ @param c_int index: 0 - (Number of Preamp gains - 1) """ error_code = self.dll.SetPreAmpGain(index) return ERROR_DICT[error_code] def _set_temperature(self, temp): temp = c_int(temp) error_code = self.dll.SetTemperature(temp) return ERROR_DICT[error_code] def _set_acquisition_mode(self, mode): """ Function to set the acquisition mode @param mode: @return: """ check_val = 0 if hasattr(AcquisitionMode, mode): n_mode = c_int(getattr(AcquisitionMode, mode).value) error_code = self.dll.SetAcquisitionMode(n_mode) else: self.log.warning('{0} mode is not supported'.format(mode)) check_val = -1 if ERROR_DICT[error_code] != 'DRV_SUCCESS': check_val = -1 else: self._acquisition_mode = mode return check_val def _set_cooler(self, state): if state: error_code = self.dll.CoolerON() else: error_code = self.dll.CoolerOFF() return ERROR_DICT[error_code] def _set_frame_transfer(self, transfer_mode): acq_mode = self._acquisition_mode if (acq_mode == 'SINGLE_SCAN') | (acq_mode == 'KINETIC'): self.log.debug('Setting of frame transfer mode has no effect in acquisition ' 'mode \'SINGLE_SCAN\' or \'KINETIC\'.') return -1 else: rtrn_val = self.dll.SetFrameTransferMode(transfer_mode) if ERROR_DICT[rtrn_val] == 'DRV_SUCCESS': return 0 else: self.log.warning('Could not set frame transfer mode:{0}'.format(ERROR_DICT[rtrn_val])) return -1 # getter functions def _get_status(self, status): error_code = self.dll.GetStatus(byref(status)) return ERROR_DICT[error_code] def _get_detector(self, nx_px, ny_px): error_code = self.dll.GetDetector(byref(nx_px), byref(ny_px)) return ERROR_DICT[error_code] def _get_camera_serialnumber(self, number): """ Gives serial number Parameters """ error_code = self.dll.GetCameraSerialNumber(byref(number)) return ERROR_DICT[error_code] def _get_acquisition_timings(self): exposure = c_float() accumulate = c_float() kinetic = c_float() error_code = self.dll.GetAcquisitionTimings(byref(exposure), byref(accumulate), byref(kinetic)) self._exposure = exposure.value self._accumulate = accumulate.value self._kinetic = kinetic.value return ERROR_DICT[error_code] def _get_oldest_image(self): """ Return an array of last acquired image. @return numpy array: image data in format [[row],[row]...] Each pixel might be a float, integer or sub pixels """ width = self._width height = self._height if self._read_mode == 'IMAGE': if self._acquisition_mode == 'SINGLE_SCAN': dim = width * height / self._hbin / self._vbin elif self._acquisition_mode == 'KINETICS': dim = width * height / self._hbin / self._vbin * self._scans elif self._read_mode == 'SINGLE_TRACK' or self._read_mode == 'FVB': if self._acquisition_mode == 'SINGLE_SCAN': dim = width elif self._acquisition_mode == 'KINETICS': dim = width * self._scans dim = int(dim) image_array = np.zeros(dim) cimage_array = c_int * dim cimage = cimage_array() error_code = self.dll.GetOldestImage(pointer(cimage), dim) if ERROR_DICT[error_code] != 'DRV_SUCCESS': self.log.warning('Couldn\'t retrieve an image') else: self.log.debug('image length {0}'.format(len(cimage))) for i in range(len(cimage)): # could be problematic for 'FVB' or 'SINGLE_TRACK' readmode image_array[i] = cimage[i] image_array = np.reshape(image_array, (int(self._width/self._hbin), int(self._height/self._vbin))) return image_array def _get_number_amp(self): """ @return int: Number of amplifiers available """ n_amps = c_int() self.dll.GetNumberAmp(byref(n_amps)) return n_amps.value def _get_number_preamp_gains(self): """ Number of gain settings available for the pre amplifier @return int: Number of gains available """ n_gains = c_int() self.dll.GetNumberPreAmpGains(byref(n_gains)) return n_gains.value def _get_preamp_gain(self): """ Function returning @return tuple (int1, int2): First int describing the gain setting, second value the actual gain """ index = c_int() gain = c_float() self.dll.GetPreAmpGain(index, byref(gain)) return index.value, gain.value def _get_temperature(self): temp = c_int() error_code = self.dll.GetTemperature(byref(temp)) if ERROR_DICT[error_code] != 'DRV_SUCCESS': self.log.error('Can not retrieve temperature'.format(ERROR_DICT[error_code])) return temp.value def _get_temperature_f(self): """ Status of the cooling process + current temperature @return: (float, str) containing current temperature and state of the cooling process """ temp = c_float() error_code = self.dll.GetTemperatureF(byref(temp)) return temp.value, ERROR_DICT[error_code] def _get_size_of_circular_ring_buffer(self): index = c_long() error_code = self.dll.GetSizeOfCircularBuffer(byref(index)) if ERROR_DICT[error_code] != 'DRV_SUCCESS': self.log.error('Can not retrieve size of circular ring ' 'buffer: {0}'.format(ERROR_DICT[error_code])) return index.value def _get_number_new_images(self): first = c_long() last = c_long() error_code = self.dll.GetNumberNewImages(byref(first), byref(last)) msg = ERROR_DICT[error_code] pass_returns = ['DRV_SUCCESS', 'DRV_NO_NEW_DATA'] if msg not in pass_returns: self.log.error('Can not retrieve number of new images {0}'.format(ERROR_DICT[error_code])) return first.value, last.value # not working properly (only for n_scans = 1) def _get_images(self, first_img, last_img, n_scans): """ Return an array of last acquired image. @return numpy array: image data in format [[row],[row]...] Each pixel might be a float, integer or sub pixels """ width = self._width height = self._height # first_img, last_img = self._get_number_new_images() # n_scans = last_img - first_img dim = width * height * n_scans dim = int(dim) image_array = np.zeros(dim) cimage_array = c_int * dim cimage = cimage_array() first_img = c_long(first_img) last_img = c_long(last_img) size = c_ulong(width * height) val_first = c_long() val_last = c_long() error_code = self.dll.GetImages(first_img, last_img, pointer(cimage), size, byref(val_first), byref(val_last)) if ERROR_DICT[error_code] != 'DRV_SUCCESS': self.log.warning('Couldn\'t retrieve an image. {0}'.format(ERROR_DICT[error_code])) else: for i in range(len(cimage)): # could be problematic for 'FVB' or 'SINGLE_TRACK' readmode image_array[i] = cimage[i] self._cur_image = image_array return image_array
class SoftPIDController(GenericLogic, PIDControllerInterface): """ Control a process via software PID. """ _modclass = 'pidlogic' _modtype = 'logic' ## declare connectors process = Connector(interface='ProcessInterface') control = Connector(interface='ProcessControlInterface') # config opt timestep = ConfigOption(default=100) # status vars kP = StatusVar(default=1) kI = StatusVar(default=1) kD = StatusVar(default=1) setpoint = StatusVar(default=273.15) manualvalue = StatusVar(default=0) sigNewValue = QtCore.Signal(float) def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) self.log.debug('The following configuration was found.') # checking for the right configuration for key in config.keys(): self.log.debug('{0}: {1}'.format(key, config[key])) #number of lines in the matrix plot self.NumberOfSecondsLog = 100 self.threadlock = Mutex() def on_activate(self): """ Initialisation performed during activation of the module. """ self._process = self.get_connector('process') self._control = self.get_connector('control') self.previousdelta = 0 self.cv = self._control.getControlValue() self.timer = QtCore.QTimer() self.timer.setSingleShot(True) self.timer.setInterval(self.timestep) self.timer.timeout.connect(self._calcNextStep, QtCore.Qt.QueuedConnection) self.sigNewValue.connect(self._control.setControlValue) self.history = np.zeros([3, 5]) self.savingState = False self.enable = False self.integrated = 0 self.countdown = 2 self.timer.start(self.timestep) def on_deactivate(self): """ Perform required deactivation. """ pass def _calcNextStep(self): """ This function implements the Takahashi Type C PID controller: the P and D term are no longer dependent on the set-point, only on PV (which is Thlt). The D term is NOT low-pass filtered. This function should be called once every TS seconds. """ self.pv = self._process.getProcessValue() if self.countdown > 0: self.countdown -= 1 self.previousdelta = self.setpoint - self.pv print('Countdown: ', self.countdown) elif self.countdown == 0: self.countdown = -1 self.integrated = 0 self.enable = True if (self.enable): delta = self.setpoint - self.pv self.integrated += delta ## Calculate PID controller: self.P = self.kP * delta self.I = self.kI * self.timestep * self.integrated self.D = self.kD / self.timestep * (delta - self.previousdelta) self.cv += self.P + self.I + self.D self.previousdelta = delta ## limit contol output to maximum permissible limits limits = self._control.getControlLimits() if (self.cv > limits[1]): self.cv = limits[1] if (self.cv < limits[0]): self.cv = limits[0] self.history = np.roll(self.history, -1, axis=1) self.history[0, -1] = self.pv self.history[1, -1] = self.cv self.history[2, -1] = self.setpoint self.sigNewValue.emit(self.cv) else: self.cv = self.manualvalue limits = self._control.getControlLimits() if (self.cv > limits[1]): self.cv = limits[1] if (self.cv < limits[0]): self.cv = limits[0] self.sigNewValue.emit(self.cv) self.timer.start(self.timestep) def startLoop(self): """ Start the control loop. """ self.countdown = 2 def stopLoop(self): """ Stop the control loop. """ self.countdown = -1 self.enable = False def getSavingState(self): """ Find out if we are keeping data for saving later. @return bool: whether module is saving process and control data """ return self.savingState def startSaving(self): """ Start saving process and control data. Does not do anything right now. """ pass def saveData(self): """ Write process and control data to file. Does not do anything right now. """ pass def get_kp(self): """ Return the proportional constant. @return float: proportional constant of PID controller """ return self.kP def set_kp(self, kp): """ Set the proportional constant of the PID controller. @prarm float kp: proportional constant of PID controller """ self.kP = kp def get_ki(self): """ Get the integration constant of the PID controller @return float: integration constant of the PID controller """ return self.kI def set_ki(self, ki): """ Set the integration constant of the PID controller. @param float ki: integration constant of the PID controller """ self.kI = ki def get_kd(self): """ Get the derivative constant of the PID controller @return float: the derivative constant of the PID controller """ return self.kD def set_kd(self, kd): """ Set the derivative constant of the PID controller @param float kd: the derivative constant of the PID controller """ self.kD = kd def get_setpoint(self): """ Get the current setpoint of the PID controller. @return float: current set point of the PID controller """ return self.setpoint def set_setpoint(self, setpoint): """ Set the current setpoint of the PID controller. @param float setpoint: new set point of the PID controller """ self.setpoint = setpoint def get_manual_value(self): """ Return the control value for manual mode. @return float: control value for manual mode """ return self.manualvalue def set_manual_value(self, manualvalue): """ Set the control value for manual mode. @param float manualvalue: control value for manual mode of controller """ self.manualvalue = manualvalue limits = self._control.getControlLimits() if (self.manualvalue > limits[1]): self.manualvalue = limits[1] if (self.manualvalue < limits[0]): self.manualvalue = limits[0] def get_enabled(self): """ See if the PID controller is controlling a process. @return bool: whether the PID controller is preparing to or conreolling a process """ return self.enable or self.countdown >= 0 def set_enabled(self, enabled): """ Set the state of the PID controller. @param bool enabled: desired state of PID controller """ if enabled and not self.enable and self.countdown == -1: self.startLoop() if not enabled and self.enable: self.stopLoop() def get_control_limits(self): """ Get the minimum and maximum value of the control actuator. @return list(float): (minimum, maximum) values of the control actuator """ return self._control.getControlLimits() def set_control_limits(self, limits): """ Set the minimum and maximum value of the control actuator. @param list(float) limits: (minimum, maximum) values of the control actuator This function does nothing, control limits are handled by the control module """ pass def get_control_value(self): """ Get current control output value. @return float: control output value """ return self.cv def get_process_value(self): """ Get current process input value. @return float: current process input value """ return self.pv def get_extra(self): """ Extra information about the controller state. @return dict: extra informatin about internal controller state Do not depend on the output of this function, not every field exists for every PID controller. """ return {'P': self.P, 'I': self.I, 'D': self.D}
class OBISLaser(Base, SimpleLaserInterface): """ Implements the Coherent OBIS laser. Example configuration: ``` # obis: # module.Class: 'SimpleLaserInterface.OBISLaser' # com_port: 'COM3' ``` """ _modclass = 'laser' _modtype = 'hardware' eol = '\r' _model_name = 'UNKNOWN' _com_port = ConfigOption('com_port', missing='error') def on_activate(self): """ Activate module. """ self.obis = serial.Serial(self._com_port, timeout=1) connected = self.connect_laser() if not connected: self.log.error('Laser does not seem to be connected.') return -1 else: self._model_name = self._communicate('SYST:INF:MOD?') return 0 def on_deactivate(self): """ Deactivate module. """ self.disconnect_laser() def connect_laser(self): """ Connect to Instrument. @return bool: connection success """ response = self._communicate('*IDN?')[0] if response.startswith('ERR-100'): return False else: return True def disconnect_laser(self): """ Close the connection to the instrument. """ self.off() self.obis.close() def allowed_control_modes(self): """ Control modes for this laser """ self.log.warning(self._model_name + ' does not have control modes') def get_control_mode(self): """ Get current laser control mode. @return ControlMode: current laser control mode """ self.log.warning(self._model_name + ' does not have control modes, cannot get current mode.') def set_control_mode(self, mode): """ Set laser control mode. @param ControlMode mode: desired control mode @return ControlMode: actual control mode """ self.log.warning(self._model_name + ' does not have control modes, ' 'cannot set to mode {}'.format(mode) ) def get_power(self): """ Get laser power. @return float: laser power in watts """ # The present laser output power in watts response = self._communicate('SOUR:POW:LEV?') return float(response) def get_power_setpoint(self): """ Get the laser power setpoint. @return float: laser power setpoint in watts """ # The present laser power level setting in watts (set level) response = self._communicate('SOUR:POW:LEV:IMM:AMPL?') return float(response) def get_power_range(self): """ Get laser power range. @return tuple(float, float): laser power range """ minpower = float(self._communicate('SOUR:POW:LIM:LOW?')) maxpower = float(self._communicate('SOUR:POW:LIM:HIGH?')) return (minpower, maxpower) def set_power(self, power): """ Set laser power @param float power: desired laser power in watts """ self._communicate('SOUR:POW:LEV:IMM:AMPL {}'.format(power)) def get_current_unit(self): """ Get unit for laser current. @return str: unit for laser curret """ return 'A' # amps def get_current_range(self): """ Get range for laser current. @return tuple(flaot, float): range for laser current """ low = self._communicate('SOUR:CURR:LIM:LOW?') high = self._communicate('SOUR:CURR:LIM:HIGH?') return (float(low), float(high)) def get_current(self): """ Cet current laser current @return float: current laser current in amps """ return float(self._communicate('SOUR:POW:CURR?')) def get_current_setpoint(self): """ Current laser current setpoint. @return float: laser current setpoint """ self.log.warning('Getting the current setpoint is not supported by the ' + self._model_name) return -1 def set_current(self, current_percent): """ Set laser current setpoint. @param float current_percent: laser current setpoint """ self._communicate('SOUR:POW:CURR {}'.format(current_percent)) return self.get_current() def get_shutter_state(self): """ Get laser shutter state. @return ShutterState: laser shutter state """ return ShutterState.NOSHUTTER def set_shutter_state(self, state): """ Set the desired laser shutter state. @param ShutterState state: desired laser shutter state @return ShutterState: actual laser shutter state """ self.log.warning(self._model_name + ' does not have a shutter') return self.get_shutter_state() def get_temperatures(self): """ Get all available temperatures. @return dict: dict of temperature names and value """ return { 'Diode': self._get_diode_temperature(), 'Internal': self._get_internal_temperature(), 'Base Plate': self._get_baseplate_temperature() } def set_temperatures(self, temps): """ Set temperature for lasers with adjustable temperature for tuning @return dict: dict with new temperature setpoints """ self.log.warning(self._model_name + ' cannot set temperatures.') return {} def get_temperature_setpoints(self): """ Get temperature setpints. @return dict: dict of temperature name and setpoint value """ return {'Diode':float(self._communicate('SOUR:TEMP:DIOD:DSET?').split('C')[0])} def get_laser_state(self): """ Get laser operation state @return LaserState: laser state """ state = self._communicate('SOUR:AM:STAT?') if 'ON' in state: return LaserState.ON elif 'OFF' in state: return LaserState.OFF else: return LaserState.UNKNOWN def set_laser_state(self, status): """ Set desited laser state. @param LaserState status: desired laser state @return LaserState: actual laser state """ # TODO: this is big. cannot be called without having LaserState, # which is only defined in the simple laser interface. # I think this shoudl be a private method. actstat = self.get_laser_state() if actstat != status: if status == LaserState.ON: self._communicate('SOUR:AM:STAT ON') #return self.get_laser_state() elif status == LaserState.OFF: self._communicate('SOUR:AM:STAT OFF') #return self.get_laser_state() return self.get_laser_state() def on(self): """ Turn laser on. @return LaserState: actual laser state """ status = self.get_laser_state() if status == LaserState.OFF: self._communicate('SOUR:AM:STAT ON') return self.get_laser_state() else: return self.get_laser_state() def off(self): """ Turn laser off. @return LaserState: actual laser state """ self.set_laser_state(LaserState.OFF) return self.get_laser_state() def get_extra_info(self): """ Extra information from laser. @return str: multiple lines of text with information about laser """ extra = ('System Model Name: ' + self._communicate('SYST:INF:MOD?') + '\n' 'System Manufacture Date: ' + self._communicate('SYST:INF:MDAT?') + '\n' 'System Calibration Date: ' + self._communicate('SYST:INF:CDAT?') + '\n' 'System Serial Number: ' + self._communicate('SYST:INF:SNUM?') + '\n' 'System Part Number: ' + self._communicate('SYST:INF:PNUM?') + '\n' 'Firmware version: ' + self._communicate('SYST:INF:FVER?') + '\n' 'System Protocol Version: ' + self._communicate('SYST:INF:PVER?') + '\n' 'System Wavelength: ' + self._communicate('SYST:INF:WAV?') + '\n' 'System Power Rating: ' + self._communicate('SYST:INF:POW?') + '\n' 'Device Type: ' + self._communicate('SYST:INF:TYP?') + '\n' 'System Power Cycles: ' + self._communicate('SYST:CYCL?') + '\n' 'System Power Hours: ' + self._communicate('SYST:HOUR?') + '\n' 'Diode Hours: ' + self._communicate('SYST:DIOD:HOUR?') ) return extra ########################## communication methods ############################### def _send(self, message): """ Send a message to to laser @param string message: message to be delivered to the laser """ new_message = message + self.eol self.obis.write(new_message.encode()) def _communicate(self, message): """ Send a receive messages with the laser @param string message: message to be delivered to the laser @returns string response: message received from the laser """ self._send(message) time.sleep(0.1) response_len = self.obis.inWaiting() response = [] while response_len > 0: this_response_line = self.obis.readline().decode().strip() if (response_len == 4) and (this_response_line == 'OK'): response.append('') else: response.append(this_response_line) response_len = self.obis.inWaiting() # Potentially multi-line responses - need to be joined into string full_response = ''.join(response) if full_response == 'ERR-100': self.log.warning(self._model_name + ' does not support the command ' + message) return '-1' return full_response ########################## internal methods #################################### def _get_diode_temperature(self): """ Get laser diode temperature @return float: laser diode temperature """ response = float(self._communicate('SOUR:TEMP:DIOD?').split('C')[0]) return response def _get_internal_temperature(self): """ Get internal laser temperature @return float: internal laser temperature """ return float(self._communicate('SOUR:TEMP:INT?').split('C')[0]) def _get_baseplate_temperature(self): """ Get laser base plate temperature @return float: laser base plate temperature """ return float(self._communicate('SOUR:TEMP:BAS?').split('C')[0]) def _get_interlock_status(self): """ Get the status of the system interlock @returns bool interlock: status of the interlock """ response = self._communicate('SYST:LOCK?') if response.lower() == 'ok': return True elif response.lower() == 'off': return False else: return False def _set_laser_to_11(self): """ Set the laser power to 11 """ self.set_power(0.165)
class MicrowaveSmiq(Base, MicrowaveInterface): """ This is the Interface class to define the controls for the simple microwave hardware. """ _modclass = 'MicrowaveSmiq' _modtype = 'hardware' _gpib_address = ConfigOption('gpib_address', missing='error') _gpib_timeout = ConfigOption('gpib_timeout', 10, missing='warn') def on_activate(self): """ Initialisation performed during activation of the module. """ self._gpib_timeout = self._gpib_timeout * 1000 # trying to load the visa connection to the module self.rm = visa.ResourceManager() try: self._gpib_connection = self.rm.open_resource(self._gpib_address, timeout=self._gpib_timeout) except: self.log.error('This is MWSMIQ: could not connect to GPIB address >>{}<<.' ''.format(self._gpib_address)) raise self.log.info('MWSMIQ initialised and connected to hardware.') self.model = self._gpib_connection.query('*IDN?').split(',')[1] self._command_wait('*CLS') self._command_wait('*RST') return def on_deactivate(self): """ Cleanup performed during deactivation of the module. """ #self._gpib_connection.close() #self.rm.close() return def _command_wait(self, command_str): """ Writes the command in command_str via GPIB and waits until the device has finished processing it. @param command_str: The command to be written """ self._gpib_connection.write(command_str) self._gpib_connection.write('*WAI') while int(float(self._gpib_connection.query('*OPC?'))) != 1: time.sleep(0.2) return def get_limits(self): """ Create an object containing parameter limits for this microwave source. @return MicrowaveLimits: device-specific parameter limits """ limits = MicrowaveLimits() limits.supported_modes = (MicrowaveMode.CW, MicrowaveMode.LIST, MicrowaveMode.SWEEP) limits.min_frequency = 300e3 limits.max_frequency = 6.4e9 limits.min_power = -144 limits.max_power = 10 limits.list_minstep = 0.1 limits.list_maxstep = 6.4e9 limits.list_maxentries = 4000 limits.sweep_minstep = 0.1 limits.sweep_maxstep = 6.4e9 limits.sweep_maxentries = 10001 if self.model == 'SMIQ02B': limits.max_frequency = 2.2e9 limits.max_power = 13 elif self.model == 'SMIQ03B': limits.max_frequency = 3.3e9 limits.max_power = 13 elif self.model == 'SMIQ03HD': limits.max_frequency = 3.3e9 limits.max_power = 13 elif self.model == 'SMIQ04B': limits.max_frequency = 4.4e9 elif self.model == 'SMIQ06B': pass elif self.model == 'SMIQ06ATE': pass else: self.log.warning('Model string unknown, hardware limits may be wrong.') limits.list_maxstep = limits.max_frequency limits.sweep_maxstep = limits.max_frequency return limits def off(self): """ Switches off any microwave output. Must return AFTER the device is actually stopped. @return int: error code (0:OK, -1:error) """ mode, is_running = self.get_status() if not is_running: return 0 if mode == 'list': self._command_wait(':FREQ:MODE CW') self._gpib_connection.write('OUTP:STAT OFF') self._gpib_connection.write('*WAI') while int(float(self._gpib_connection.query('OUTP:STAT?'))) != 0: time.sleep(0.2) if mode == 'list': self._command_wait(':LIST:LEARN') self._command_wait(':FREQ:MODE LIST') return 0 def get_status(self): """ Gets the current status of the MW source, i.e. the mode (cw, list or sweep) and the output state (stopped, running) @return str, bool: mode ['cw', 'list', 'sweep'], is_running [True, False] """ is_running = bool(int(float(self._gpib_connection.query('OUTP:STAT?')))) mode = self._gpib_connection.query(':FREQ:MODE?').strip('\n').lower() if mode == 'swe': mode = 'sweep' return mode, is_running def get_power(self): """ Gets the microwave output power. @return float: the power set at the device in dBm """ mode, dummy = self.get_status() if mode == 'list': return float(self._gpib_connection.query(':LIST:POW?')) else: # This case works for cw AND sweep mode return float(self._gpib_connection.query(':POW?')) def get_frequency(self): """ Gets the frequency of the microwave output. Returns single float value if the device is in cw mode. Returns list like [start, stop, step] if the device is in sweep mode. Returns list of frequencies if the device is in list mode. @return [float, list]: frequency(s) currently set for this device in Hz """ mode, is_running = self.get_status() if 'cw' in mode: return_val = float(self._gpib_connection.query(':FREQ?')) elif 'sweep' in mode: start = float(self._gpib_connection.query(':FREQ:STAR?')) stop = float(self._gpib_connection.query(':FREQ:STOP?')) step = float(self._gpib_connection.query(':SWE:STEP?')) return_val = [start+step, stop, step] elif 'list' in mode: # Exclude first frequency entry (duplicate due to trigger issues) frequency_str = self._gpib_connection.query(':LIST:FREQ?').split(',', 1)[1] return_val = np.array([float(freq) for freq in frequency_str.split(',')]) return return_val def cw_on(self): """ Switches on cw microwave output. Must return AFTER the device is actually running. @return int: error code (0:OK, -1:error) """ current_mode, is_running = self.get_status() if is_running: if current_mode == 'cw': return 0 else: self.off() if current_mode != 'cw': self._command_wait(':FREQ:MODE CW') self._gpib_connection.write(':OUTP:STAT ON') self._gpib_connection.write('*WAI') dummy, is_running = self.get_status() while not is_running: time.sleep(0.2) dummy, is_running = self.get_status() return 0 def set_cw(self, frequency=None, power=None): """ Configures the device for cw-mode and optionally sets frequency and/or power @param float frequency: frequency to set in Hz @param float power: power to set in dBm @return tuple(float, float, str): with the relation current frequency in Hz, current power in dBm, current mode """ mode, is_running = self.get_status() if is_running: self.off() # Activate CW mode if mode != 'cw': self._command_wait(':FREQ:MODE CW') # Set CW frequency if frequency is not None: self._command_wait(':FREQ {0:f}'.format(frequency)) # Set CW power if power is not None: self._command_wait(':POW {0:f}'.format(power)) # Return actually set values mode, dummy = self.get_status() actual_freq = self.get_frequency() actual_power = self.get_power() return actual_freq, actual_power, mode def list_on(self): """ Switches on the list mode microwave output. Must return AFTER the device is actually running. @return int: error code (0:OK, -1:error) """ current_mode, is_running = self.get_status() if is_running: if current_mode == 'list': return 0 else: self.off() # This needs to be done due to stupid design of the list mode (sweep is better) self.cw_on() self._command_wait(':LIST:LEARN') self._command_wait(':FREQ:MODE LIST') dummy, is_running = self.get_status() while not is_running: time.sleep(0.2) dummy, is_running = self.get_status() return 0 def set_list(self, frequency=None, power=None): """ Configures the device for list-mode and optionally sets frequencies and/or power @param list frequency: list of frequencies in Hz @param float power: MW power of the frequency list in dBm @return tuple(list, float, str): current frequencies in Hz, current power in dBm, current mode """ mode, is_running = self.get_status() if is_running: self.off() # Cant change list parameters if in list mode if mode != 'cw': self.set_cw() self._gpib_connection.write(":LIST:SEL 'QUDI'") self._gpib_connection.write('*WAI') # Set list frequencies if frequency is not None: s = ' {0:f},'.format(frequency[0]) for f in frequency[:-1]: s += ' {0:f},'.format(f) s += ' {0:f}'.format(frequency[-1]) self._gpib_connection.write(':LIST:FREQ' + s) self._gpib_connection.write('*WAI') self._gpib_connection.write(':LIST:MODE STEP') self._gpib_connection.write('*WAI') # Set list power if power is not None: self._gpib_connection.write(':LIST:POW {0:f}'.format(power)) self._gpib_connection.write('*WAI') self._command_wait(':TRIG1:LIST:SOUR EXT') # Apply settings in hardware self._command_wait(':LIST:LEARN') # If there are timeout problems after this command, update the smiq firmware to > 5.90 # as there was a problem with excessive wait times after issuing :LIST:LEARN over a # GPIB connection in firmware 5.88 self._command_wait(':FREQ:MODE LIST') actual_freq = self.get_frequency() actual_power = self.get_power() mode, dummy = self.get_status() return actual_freq, actual_power, mode def reset_listpos(self): """ Reset of MW list mode position to start (first frequency step) @return int: error code (0:OK, -1:error) """ self._command_wait(':ABOR:LIST') return 0 def sweep_on(self): """ Switches on the sweep mode. @return int: error code (0:OK, -1:error) """ current_mode, is_running = self.get_status() if is_running: if current_mode == 'sweep': return 0 else: self.off() if current_mode != 'sweep': self._command_wait(':FREQ:MODE SWEEP') self._gpib_connection.write(':OUTP:STAT ON') dummy, is_running = self.get_status() while not is_running: time.sleep(0.2) dummy, is_running = self.get_status() return 0 def set_sweep(self, start=None, stop=None, step=None, power=None): """ Configures the device for sweep-mode and optionally sets frequency start/stop/step and/or power @return float, float, float, float, str: current start frequency in Hz, current stop frequency in Hz, current frequency step in Hz, current power in dBm, current mode """ mode, is_running = self.get_status() if is_running: self.off() if mode != 'sweep': self._command_wait(':FREQ:MODE SWEEP') if (start is not None) and (stop is not None) and (step is not None): self._gpib_connection.write(':SWE:MODE STEP') self._gpib_connection.write(':SWE:SPAC LIN') self._gpib_connection.write('*WAI') self._gpib_connection.write(':FREQ:START {0:f}'.format(start - step)) self._gpib_connection.write(':FREQ:STOP {0:f}'.format(stop)) self._gpib_connection.write(':SWE:STEP:LIN {0:f}'.format(step)) self._gpib_connection.write('*WAI') if power is not None: self._gpib_connection.write(':POW {0:f}'.format(power)) self._gpib_connection.write('*WAI') self._command_wait(':TRIG1:SWE:SOUR EXT') actual_power = self.get_power() freq_list = self.get_frequency() mode, dummy = self.get_status() return freq_list[0], freq_list[1], freq_list[2], actual_power, mode def reset_sweeppos(self): """ Reset of MW sweep mode position to start (start frequency) @return int: error code (0:OK, -1:error) """ self._command_wait(':ABOR:SWE') return 0 def set_ext_trigger(self, pol=TriggerEdge.RISING): """ Set the external trigger for this device with proper polarization. @param TriggerEdge pol: polarisation of the trigger (basically rising edge or falling edge) @return object: current trigger polarity [TriggerEdge.RISING, TriggerEdge.FALLING] """ mode, is_running = self.get_status() if is_running: self.off() if pol == TriggerEdge.RISING: edge = 'POS' elif pol == TriggerEdge.FALLING: edge = 'NEG' else: self.log.warning('No valid trigger polarity passed to microwave hardware module.') edge = None if edge is not None: self._command_wait(':TRIG1:SLOP {0}'.format(edge)) polarity = self._gpib_connection.query(':TRIG1:SLOP?') if 'NEG' in polarity: return TriggerEdge.FALLING else: return TriggerEdge.RISING
class RNG(Base, RNGInterface): """Random Number Generator quasi-instrument. Every time get_random_value() method is called, it takes self.mean and self.noise and returns the following random number (a list of samples_number random numbers): mean + noise*( random.random()-0.5 ) """ _modclass = 'RNG' _modtype = 'hardware' # config _mean = ConfigOption(name='mean', default=0.0, missing='warn') _noise = ConfigOption(name='noise', default=0.0, missing='warn') def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) def on_activate(self): """ Initialisation performed during activation of the module. """ pass # self.mean = self._mean # self.noise = self._noise def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. """ pass # self.log.warning('rng>deactivation') def set_params(self, mean=None, noise=None): """ Set mean value and noise amplitude of the RNG @param float mean: optional, mean value of the RNG @param float noise: optional, noise amplitude of the RNG, max deviation of random number from mean @return int: error code (0:OK, -1:error) """ if mean is not None: self._mean = mean if noise is not None: self._noise = noise def get_params(self): """ Get mean value and noise amplitude of the random number generator @return dict: {'mean': mean_value, 'noise': noise_amplitude} """ return {'mean': self._mean, 'noise': self._noise} def get_random_value(self, samples_number=1): """ Get the output value of the random number generator :param int samples_number: optional, number of random numbers to return :return list random_numbers: list of n_samples output random numbers """ output = [] for i in range(samples_number): random_value = self._mean + self._noise * (2 * random.random() - 1) output.append(random_value) return output # Methods to test data transfer over the network def return_float(self): return 1.23456789e-3 def return_string(self): return 'abcdefghABCDEFGH12345&()@$$@)$_@buidw*' def return_boolean(self): return True def return_1d_array(self, length=100): float_list = [1e-3 * i for i in range(length)] return float_list def return_1d_tuple(self, length=100): float_list = [1e-3 * i for i in range(length)] return tuple(float_list) def return_2d_array(self, length=10): array = np.zeros((length, length)) for i in range(length): for j in range(length): array[i][j] = (i - j) * 1e-3 return array
class MotorStagePI(Base, MotorInterface): """unstable: Christoph Müller, Simon Schmitt This is the Interface class to define the controls for the simple microwave hardware. Example config for copy-paste: motorstage_pi: module.Class: 'motor.motor_stage_pi.MotorStagePI' com_port_pi_xyz: 'ASRL1::INSTR' pi_xyz_baud_rate: 9600 pi_xyz_timeout: 1000 pi_xyz_term_char: '\n' pi_first_axis_label: 'x' pi_second_axis_label: 'y' pi_third_axis_label: 'z' pi_first_axis_ID: '1' pi_second_axis_ID: '2' pi_third_axis_ID: '3' pi_first_min: -0.1 # in m pi_first_max: 0.1 # in m pi_second_min: -0.1 # in m pi_second_max: 0.1 # in m pi_third_min: -0.1 # in m pi_third_max: 0.1 # in m pi_first_axis_step: 1e-7 # in m pi_second_axis_step: 1e-7 # in m pi_third_axis_step: 1e-7 # in m vel_first_min: 1e-5 # in m/s vel_first_max: 5e-2 # in m/s vel_second_min: 1e-5 # in m/s vel_second_max: 5e-2 # in m/s vel_third_min: 1e-5 # in m/s vel_third_max: 5e-2 # in m/s vel_first_axis_step: 1e-5 # in m/s vel_second_axis_step: 1e-5 # in m/s vel_third_axis_step: 1e-5 # in m/s """ _modclass = 'MotorStagePI' _modtype = 'hardware' _com_port_pi_xyz = ConfigOption('com_port_pi_xyz', 'ASRL1::INSTR', missing='warn') _pi_xyz_baud_rate = ConfigOption('pi_xyz_baud_rate', 9600, missing='warn') _pi_xyz_timeout = ConfigOption('pi_xyz_timeout', 1000, missing='warn') _pi_xyz_term_char = ConfigOption('pi_xyz_term_char', '\n', missing='warn') _first_axis_label = ConfigOption('pi_first_axis_label', 'x', missing='warn') _second_axis_label = ConfigOption('pi_second_axis_label', 'y', missing='warn') _third_axis_label = ConfigOption('pi_third_axis_label', 'z', missing='warn') _first_axis_ID = ConfigOption('pi_first_axis_ID', '1', missing='warn') _second_axis_ID = ConfigOption('pi_second_axis_ID', '2', missing='warn') _third_axis_ID = ConfigOption('pi_third_axis_ID', '3', missing='warn') _min_first = ConfigOption('pi_first_min', -0.1, missing='warn') _max_first = ConfigOption('pi_first_max', 0.1, missing='warn') _min_second = ConfigOption('pi_second_min', -0.1, missing='warn') _max_second = ConfigOption('pi_second_max', 0.1, missing='warn') _min_third = ConfigOption('pi_third_min', -0.1, missing='warn') _max_third = ConfigOption('pi_third_max', 0.1, missing='warn') step_first_axis = ConfigOption('pi_first_axis_step', 1e-7, missing='warn') step_second_axis = ConfigOption('pi_second_axis_step', 1e-7, missing='warn') step_third_axis = ConfigOption('pi_third_axis_step', 1e-7, missing='warn') _vel_min_first = ConfigOption('vel_first_min', 1e-5, missing='warn') _vel_max_first = ConfigOption('vel_first_max', 5e-2, missing='warn') _vel_min_second = ConfigOption('vel_second_min', 1e-5, missing='warn') _vel_max_second = ConfigOption('vel_second_max', 5e-2, missing='warn') _vel_min_third = ConfigOption('vel_third_min', 1e-5, missing='warn') _vel_max_third = ConfigOption('vel_third_max', 5e-2, missing='warn') _vel_step_first = ConfigOption('vel_first_axis_step', 1e-5, missing='warn') _vel_step_second = ConfigOption('vel_second_axis_step', 1e-5, missing='warn') _vel_step_third = ConfigOption('vel_third_axis_step', 1e-5, missing='warn') def __init__(self, **kwargs): super().__init__(**kwargs) def on_activate(self): """ Initialisation performed during activation of the module. @return: error code """ self.rm = visa.ResourceManager() self._serial_connection_xyz = self.rm.open_resource( resource_name=self._com_port_pi_xyz, baud_rate=self._pi_xyz_baud_rate, timeout=self._pi_xyz_timeout) return 0 def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. @return: error code """ self._serial_connection_xyz.close() self.rm.close() return 0 def get_constraints(self): """ Retrieve the hardware constrains from the motor device. @return dict: dict with constraints for the sequence generation and GUI Provides all the constraints for the xyz stage and rot stage (like total movement, velocity, ...) Each constraint is a tuple of the form (min_value, max_value, stepsize) """ constraints = OrderedDict() axis0 = {} axis0['label'] = self._first_axis_label axis0['ID'] = self._first_axis_ID axis0['unit'] = 'm' # the SI units axis0['ramp'] = None # a possible list of ramps axis0['pos_min'] = self._min_first axis0['pos_max'] = self._max_first axis0['pos_step'] = self.step_first_axis axis0['vel_min'] = self._vel_min_first axis0['vel_max'] = self._vel_max_first axis0['vel_step'] = self._vel_step_first axis0['acc_min'] = None axis0['acc_max'] = None axis0['acc_step'] = None axis1 = {} axis1['label'] = self._second_axis_label axis1['ID'] = self._second_axis_ID axis1['unit'] = 'm' # the SI units axis1['ramp'] = None # a possible list of ramps axis1['pos_min'] = self._min_second axis1['pos_max'] = self._max_second axis1['pos_step'] = self.step_second_axis axis1['vel_min'] = self._vel_min_second axis1['vel_max'] = self._vel_max_second axis1['vel_step'] = self._vel_step_second axis1['acc_min'] = None axis1['acc_max'] = None axis1['acc_step'] = None axis2 = {} axis2['label'] = self._third_axis_label axis2['ID'] = self._third_axis_ID axis2['unit'] = 'm' # the SI units axis2['ramp'] = None # a possible list of ramps axis2['pos_min'] = self._min_third axis2['pos_max'] = self._max_third axis2['pos_step'] = self.step_third_axis axis2['vel_min'] = self._vel_min_third axis2['vel_max'] = self._vel_max_third axis2['vel_step'] = self._vel_step_third axis2['acc_min'] = None axis2['acc_max'] = None axis2['acc_step'] = None # assign the parameter container for x to a name which will identify it constraints[axis0['label']] = axis0 constraints[axis1['label']] = axis1 constraints[axis2['label']] = axis2 return constraints def move_rel(self, param_dict): """Moves stage in given direction (relative movement) @param dict param_dict: dictionary, which passes all the relevant parameters, which should be changed. Usage: {'axis_label': <the-abs-pos-value>}. 'axis_label' must correspond to a label given to one of the axis. @return dict pos: dictionary with the current magnet position """ # There are sometimes connections problems therefore up to 3 attempts are started for attempt in range(3): try: for axis_label in param_dict: step = param_dict[axis_label] self._do_move_rel(axis_label, step) except: self.log.warning('Motor connection problem! Try again...') else: break else: self.log.error('Motor cannot move!') #The following two lines have been commented out to speed up #pos = self.get_pos() #return pos return param_dict def move_abs(self, param_dict): """Moves stage to absolute position @param dict param_dict: dictionary, which passes all the relevant parameters, which should be changed. Usage: {'axis_label': <the-abs-pos-value>}. 'axis_label' must correspond to a label given to one of the axis. The values for the axes are in millimeter, the value for the rotation is in degrees. @return dict pos: dictionary with the current axis position """ # There are sometimes connections problems therefore up to 3 attempts are started for attept in range(3): try: for axis_label in param_dict: move = param_dict[axis_label] self._do_move_abs(axis_label, move) while not self._motor_stopped(): time.sleep(0.02) except: self.log.warning('Motor connection problem! Try again...') else: break else: self.log.error('Motor cannot move!') #The following two lines have been commented out to speed up #pos = self.get_pos() #return pos return param_dict def abort(self): """Stops movement of the stage @return int: error code (0:OK, -1:error) """ constraints = self.get_constraints() try: for axis_label in constraints: self._write_xyz(axis_label, 'AB') while not self._motor_stopped(): time.sleep(0.2) return 0 except: self.log.error('MOTOR MOVEMENT NOT STOPPED!!!)') return -1 def get_pos(self, param_list=None): """ Gets current position of the stage arms @param list param_list: optional, if a specific position of an axis is desired, then the labels of the needed axis should be passed in the param_list. If nothing is passed, then from each axis the position is asked. @return dict: with keys being the axis labels and item the current position. """ constraints = self.get_constraints() param_dict = {} # unfortunately, probably due to connection problems this specific command sometimes failing # although it should run.... therefore some retries are added try: if param_list is not None: for axis_label in param_list: for attempt in range(5): # self.log.debug(attempt) try: pos = int( self._ask_xyz(axis_label, 'TT').split(":", 1)[1]) param_dict[axis_label] = pos * 1e-7 except: continue else: break else: for axis_label in constraints: for attempt in range(5): #self.log.debug(attempt) try: #pos = int(self._ask_xyz(axis_label,'TT')[8:]) pos = int( self._ask_xyz(axis_label, 'TT').split(":", 1)[1]) param_dict[axis_label] = pos * 1e-7 except: continue else: break return param_dict except: self.log.error('Could not find current xyz motor position') return -1 def get_status(self, param_list=None): """ Get the status of the position @param list param_list: optional, if a specific status of an axis is desired, then the labels of the needed axis should be passed in the param_list. If nothing is passed, then from each axis the status is asked. @return dict: with the axis label as key and the status number as item. The meaning of the return value is: Bit 0: Ready Bit 1: On target Bit 2: Reference drive active Bit 3: Joystick ON Bit 4: Macro running Bit 5: Motor OFF Bit 6: Brake ON Bit 7: Drive current active """ constraints = self.get_constraints() param_dict = {} try: if param_list is not None: for axis_label in param_list: status = self._ask_xyz(axis_label, 'TS').split(":", 1)[1] param_dict[axis_label] = status else: for axis_label in constraints: status = self._ask_xyz(axis_label, 'TS').split(":", 1)[1] param_dict[axis_label] = status return param_dict except: self.log.error('Status request unsuccessful') return -1 def calibrate(self, param_list=None): """ Calibrates the stage. @param dict param_list: param_list: optional, if a specific calibration of an axis is desired, then the labels of the needed axis should be passed in the param_list. If nothing is passed, then all connected axis will be calibrated. After calibration the stage moves to home position which will be the zero point for the passed axis. @return dict pos: dictionary with the current position of the ac#xis """ #constraints = self.get_constraints() param_dict = {} try: for axis_label in param_list: self._write_xyz(axis_label, 'FE2') while not self._motor_stopped(): time.sleep(0.2) for axis_label in param_list: self._write_xyz(axis_label, 'DH') except: self.log.error('Calibration did not work') for axis_label in param_list: param_dict[axis_label] = 0.0 self.move_abs(param_dict) pos = self.get_pos() return pos def get_velocity(self, param_list=None): """ Gets the current velocity for all connected axes in m/s. @param list param_list: optional, if a specific velocity of an axis is desired, then the labels of the needed axis should be passed as the param_list. If nothing is passed, then from each axis the velocity is asked. @return dict : with the axis label as key and the velocity as item. """ constraints = self.get_constraints() param_dict = {} try: if param_list is not None: for axis_label in param_list: vel = int(self._ask_xyz(axis_label, 'TY').split(":", 1)[1]) param_dict[axis_label] = vel * 1e-7 else: for axis_label in constraints: vel = int(self._ask_xyz(axis_label, 'TY').split(":", 1)[1]) param_dict[axis_label] = vel * 1e-7 return param_dict except: self.log.error('Could not find current axis velocity') return -1 def set_velocity(self, param_dict): """ Write new value for velocity in m/s. @param dict param_dict: dictionary, which passes all the relevant parameters, which should be changed. Usage: {'axis_label': <the-velocity-value>}. 'axis_label' must correspond to a label given to one of the axis. @return dict param_dict2: dictionary with the updated axis velocity """ #constraints = self.get_constraints() try: for axis_label in param_dict: vel = int(param_dict[axis_label] * 1.0e7) self._write_xyz(axis_label, 'SV{0:d}'.format((vel))) #The following two lines have been commented out to speed up #param_dict2 = self.get_velocity() #retrun param_dict2 return param_dict except: self.log.error('Could not set axis velocity') return -1 ########################## internal methods ################################## def _write_xyz(self, axis, command): '''this method just sends a command to the motor! DOES NOT RETURN AN ANSWER! @param axis string: name of the axis that should be asked @param command string: command @return error code (0:OK, -1:error) ''' constraints = self.get_constraints() try: #self.log.info(constraints[axis]['ID'] + command + '\n') self._serial_connection_xyz.write(constraints[axis]['ID'] + command + '\n') trash = self._read_answer_xyz() # deletes possible answers return 0 except: self.log.error('Command was no accepted') return -1 def _read_answer_xyz(self): '''this method reads the answer from the motor! @return answer string: answer of motor ''' still_reading = True answer = '' while still_reading == True: try: answer = answer + self._serial_connection_xyz.read()[:-1] except: still_reading = False #self.log.info(answer) return answer def _ask_xyz(self, axis, question): '''this method combines writing a command and reading the answer @param axis string: name of the axis that should be asked @param command string: command @return answer string: answer of motor ''' constraints = self.get_constraints() self._serial_connection_xyz.write(constraints[axis]['ID'] + question + '\n') answer = self._read_answer_xyz() return answer def _do_move_rel(self, axis, step): """internal method for the relative move @param axis string: name of the axis that should be moved @param float step: step in meter @return str axis: axis which is moved move float: absolute position to move to """ constraints = self.get_constraints() if not (abs(constraints[axis]['pos_step']) < abs(step)): self.log.warning('Cannot make the movement of the axis "{0}"' 'since the step is too small! Ignore command!') else: current_pos = self.get_pos(axis)[axis] move = current_pos + step self._do_move_abs(axis, move) return axis, move def _do_move_abs(self, axis, move): """internal method for the absolute move in meter @param axis string: name of the axis that should be moved @param float move: desired position in meter @return str axis: axis which is moved move float: absolute position to move to """ constraints = self.get_constraints() #self.log.info(axis + 'MA{0}'.format(int(move*1e8))) if not (constraints[axis]['pos_min'] <= move <= constraints[axis]['pos_max']): self.log.warning( 'Cannot make the movement of the axis "{0}"' 'since the border [{1},{2}] would be crossed! Ignore command!' ''.format(axis, constraints[axis]['pos_min'], constraints[axis]['pos_max'])) else: self._write_xyz(axis, 'MA{0}'.format(int( move * 1e7))) # 1e7 to convert meter to SI units #self._write_xyz(axis, 'MP') return axis, move def _in_movement_xyz(self): '''this method checks if the magnet is still moving and returns a dictionary which of the axis are moving. @return: dict param_dict: Dictionary displaying if axis are moving: 0 for immobile and 1 for moving ''' constraints = self.get_constraints() param_dict = {} for axis_label in constraints: tmp0 = int( self._ask_xyz(constraints[axis_label]['label'], 'TS')[8:]) param_dict[axis_label] = tmp0 % 2 return param_dict def _motor_stopped(self): '''this method checks if the magnet is still moving and returns False if it is moving and True of it is immobile @return: bool stopped: False for immobile and True for moving ''' param_dict = self._in_movement_xyz() stopped = True for axis_label in param_dict: if param_dict[axis_label] != 0: self.log.info(axis_label + ' is moving') stopped = False return stopped
class PulseStreamer(Base, PulserInterface): """ Methods to control the Swabian Instruments Pulse Streamer 8/2 Example config for copy-paste: pulsestreamer: module.Class: 'swabian_instruments.pulse_streamer.PulseStreamer' pulsestreamer_ip: '192.168.1.100' #pulsed_file_dir: 'C:\\Software\\pulsed_files' laser_channel: 0 uw_x_channel: 1 use_external_clock: False external_clock_option: 0 """ #_pulsestreamer_ip = ConfigOption('pulsestreamer_ip', '192.168.1.100', missing='warn') _pulsestreamer_ip = ConfigOption('pulsestreamer_ip', '169.254.8.2', missing='warn') _laser_channel = ConfigOption('laser_channel', 1, missing='warn') _uw_x_channel = ConfigOption('uw_x_channel', 3, missing='warn') _use_external_clock = ConfigOption('use_external_clock', False, missing='info') _external_clock_option = ConfigOption('external_clock_option', 0, missing='info') # 0: Internal (default), 1: External 125 MHz, 2: External 10 MHz __current_waveform = StatusVar(name='current_waveform', default={}) __current_waveform_name = StatusVar(name='current_waveform_name', default='') __sample_rate = StatusVar(name='sample_rate', default=1e9) def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) self.__current_status = -1 self.__currently_loaded_waveform = '' # loaded and armed waveform name self.__samples_written = 0 self._trigger = ps.TriggerStart.SOFTWARE self._laser_mw_on_state = ps.OutputState([self._laser_channel, self._uw_x_channel], 0, 0) def on_activate(self): """ Establish connection to pulse streamer and tell it to cancel all operations """ self.pulse_streamer = ps.PulseStreamer(self._pulsestreamer_ip) if self._use_external_clock: if int(self._external_clock_option) is 2: self.pulse_streamer.selectClock(ps.ClockSource.EXT_10MHZ) elif int(self._external_clock_option) is 1: self.pulse_streamer.selectClock(ps.ClockSource.EXT_125MHZ) elif int(self._external_clock_option) is 0: self.pulse_streamer.selectClock(ps.ClockSource.INTERNAL) else: self.log.error('pulsestreamer external clock selection not allowed') self.__samples_written = 0 self.__currently_loaded_waveform = '' self.current_status = 0 def on_deactivate(self): self.reset() del self.pulse_streamer def get_constraints(self): """ Retrieve the hardware constrains from the Pulsing device. @return constraints object: object with pulser constraints as attributes. Provides all the constraints (e.g. sample_rate, amplitude, total_length_bins, channel_config, ...) related to the pulse generator hardware to the caller. SEE PulserConstraints CLASS IN pulser_interface.py FOR AVAILABLE CONSTRAINTS!!! If you are not sure about the meaning, look in other hardware files to get an impression. If still additional constraints are needed, then they have to be added to the PulserConstraints class. Each scalar parameter is an ScalarConstraints object defined in core.util.interfaces. Essentially it contains min/max values as well as min step size, default value and unit of the parameter. PulserConstraints.activation_config differs, since it contain the channel configuration/activation information of the form: {<descriptor_str>: <channel_set>, <descriptor_str>: <channel_set>, ...} If the constraints cannot be set in the pulsing hardware (e.g. because it might have no sequence mode) just leave it out so that the default is used (only zeros). # Example for configuration with default values: constraints = PulserConstraints() constraints.sample_rate.min = 10.0e6 constraints.sample_rate.max = 12.0e9 constraints.sample_rate.step = 10.0e6 constraints.sample_rate.default = 12.0e9 constraints.a_ch_amplitude.min = 0.02 constraints.a_ch_amplitude.max = 2.0 constraints.a_ch_amplitude.step = 0.001 constraints.a_ch_amplitude.default = 2.0 constraints.a_ch_offset.min = -1.0 constraints.a_ch_offset.max = 1.0 constraints.a_ch_offset.step = 0.001 constraints.a_ch_offset.default = 0.0 constraints.d_ch_low.min = -1.0 constraints.d_ch_low.max = 4.0 constraints.d_ch_low.step = 0.01 constraints.d_ch_low.default = 0.0 constraints.d_ch_high.min = 0.0 constraints.d_ch_high.max = 5.0 constraints.d_ch_high.step = 0.01 constraints.d_ch_high.default = 5.0 constraints.waveform_length.min = 80 constraints.waveform_length.max = 64800000 constraints.waveform_length.step = 1 constraints.waveform_length.default = 80 constraints.waveform_num.min = 1 constraints.waveform_num.max = 32000 constraints.waveform_num.step = 1 constraints.waveform_num.default = 1 constraints.sequence_num.min = 1 constraints.sequence_num.max = 8000 constraints.sequence_num.step = 1 constraints.sequence_num.default = 1 constraints.subsequence_num.min = 1 constraints.subsequence_num.max = 4000 constraints.subsequence_num.step = 1 constraints.subsequence_num.default = 1 # If sequencer mode is available then these should be specified constraints.repetitions.min = 0 constraints.repetitions.max = 65539 constraints.repetitions.step = 1 constraints.repetitions.default = 0 constraints.event_triggers = ['A', 'B'] constraints.flags = ['A', 'B', 'C', 'D'] constraints.sequence_steps.min = 0 constraints.sequence_steps.max = 8000 constraints.sequence_steps.step = 1 constraints.sequence_steps.default = 0 # the name a_ch<num> and d_ch<num> are generic names, which describe UNAMBIGUOUSLY the # channels. Here all possible channel configurations are stated, where only the generic # names should be used. The names for the different configurations can be customary chosen. activation_conf = OrderedDict() activation_conf['yourconf'] = {'a_ch1', 'd_ch1', 'd_ch2', 'a_ch2', 'd_ch3', 'd_ch4'} activation_conf['different_conf'] = {'a_ch1', 'd_ch1', 'd_ch2'} activation_conf['something_else'] = {'a_ch2', 'd_ch3', 'd_ch4'} constraints.activation_config = activation_conf """ constraints = PulserConstraints() # The file formats are hardware specific. constraints.sample_rate.min = 1e9 constraints.sample_rate.max = 1e9 constraints.sample_rate.step = 0 constraints.sample_rate.default = 1e9 constraints.d_ch_low.min = 0.0 constraints.d_ch_low.max = 0.0 constraints.d_ch_low.step = 0.0 constraints.d_ch_low.default = 0.0 constraints.d_ch_high.min = 3.3 constraints.d_ch_high.max = 3.3 constraints.d_ch_high.step = 0.0 constraints.d_ch_high.default = 3.3 # sample file length max is not well-defined for PulseStreamer, which collates sequential identical pulses into # one. Total number of not-sequentially-identical pulses which can be stored: 1 M. constraints.waveform_length.min = 1 constraints.waveform_length.max = 134217728 constraints.waveform_length.step = 1 constraints.waveform_length.default = 1 # the name a_ch<num> and d_ch<num> are generic names, which describe UNAMBIGUOUSLY the # channels. Here all possible channel configurations are stated, where only the generic # names should be used. The names for the different configurations can be customary chosen. activation_config = OrderedDict() activation_config['all'] = frozenset({'d_ch1', 'd_ch2', 'd_ch3', 'd_ch4', 'd_ch5', 'd_ch6', 'd_ch7', 'd_ch8'}) constraints.activation_config = activation_config return constraints def pulser_on(self): """ Switches the pulsing device on. @return int: error code (0:OK, -1:error) """ if self._seq: self.pulse_streamer.stream(self._seq) self.pulse_streamer.startNow() self.__current_status = 1 return 0 else: self.log.error('no sequence/pulse pattern prepared for the pulse streamer') self.pulser_off() self.__current_status = -1 return -1 def pulser_off(self): """ Switches the pulsing device off. @return int: error code (0:OK, -1:error) """ self.__current_status = 0 self.pulse_streamer.constant(self._laser_mw_on_state) return 0 def load_waveform(self, load_dict): """ Loads a waveform to the specified channel of the pulsing device. @param dict|list load_dict: a dictionary with keys being one of the available channel index and values being the name of the already written waveform to load into the channel. Examples: {1: rabi_ch1, 2: rabi_ch2} or {1: rabi_ch2, 2: rabi_ch1} If just a list of waveform names if given, the channel association will be invoked from the channel suffix '_ch1', '_ch2' etc. {1: rabi_ch1, 2: rabi_ch2} or {1: rabi_ch2, 2: rabi_ch1} If just a list of waveform names if given, the channel association will be invoked from the channel suffix '_ch1', '_ch2' etc. A possible configuration can be e.g. ['rabi_ch1', 'rabi_ch2', 'rabi_ch3'] @return dict: Dictionary containing the actually loaded waveforms per channel. For devices that have a workspace (i.e. AWG) this will load the waveform from the device workspace into the channel. For a device without mass memory, this will make the waveform/pattern that has been previously written with self.write_waveform ready to play. Please note that the channel index used here is not to be confused with the number suffix in the generic channel descriptors (i.e. 'd_ch1', 'a_ch1'). The channel index used here is highly hardware specific and corresponds to a collection of digital and analog channels being associated to a SINGLE wavfeorm asset. """ if isinstance(load_dict, list): waveforms = list(set(load_dict)) elif isinstance(load_dict, dict): waveforms = list(set(load_dict.values())) else: self.log.error('Method load_waveform expects a list of waveform names or a dict.') return self.get_loaded_assets()[0] if len(waveforms) != 1: self.log.error('pulsestreamer pulser expects exactly one waveform name for load_waveform.') return self.get_loaded_assets()[0] waveform = waveforms[0] if waveform != self.__current_waveform_name: self.log.error('No waveform by the name "{0}" generated for pulsestreamer pulser.\n' 'Only one waveform at a time can be held.'.format(waveform)) return self.get_loaded_assets()[0] self._seq = self.pulse_streamer.createSequence() for channel_number, pulse_pattern in self.__current_waveform.items(): #print(pulse_pattern) swabian_channel_number = int(channel_number[-1])-1 self._seq.setDigital(swabian_channel_number,pulse_pattern) self.__currently_loaded_waveform = self.__current_waveform_name return self.get_loaded_assets()[0] def get_loaded_assets(self): """ Retrieve the currently loaded asset names for each active channel of the device. The returned dictionary will have the channel numbers as keys. In case of loaded waveforms the dictionary values will be the waveform names. In case of a loaded sequence the values will be the sequence name appended by a suffix representing the track loaded to the respective channel (i.e. '<sequence_name>_1'). @return (dict, str): Dictionary with keys being the channel number and values being the respective asset loaded into the channel, string describing the asset type ('waveform' or 'sequence') """ asset_type = 'waveform' if self.__currently_loaded_waveform else None asset_dict = {chnl_num: self.__currently_loaded_waveform for chnl_num in range(1, 9)} return asset_dict, asset_type def load_sequence(self, sequence_name): """ Loads a sequence to the channels of the device in order to be ready for playback. For devices that have a workspace (i.e. AWG) this will load the sequence from the device workspace into the channels. For a device without mass memory this will make the waveform/pattern that has been previously written with self.write_waveform ready to play. @param dict|list sequence_name: a dictionary with keys being one of the available channel index and values being the name of the already written waveform to load into the channel. Examples: {1: rabi_ch1, 2: rabi_ch2} or {1: rabi_ch2, 2: rabi_ch1} If just a list of waveform names if given, the channel association will be invoked from the channel suffix '_ch1', '_ch2' etc. @return dict: Dictionary containing the actually loaded waveforms per channel. """ self.log.debug('sequencing not implemented for pulsestreamer') return dict() def clear_all(self): """ Clears all loaded waveforms from the pulse generators RAM/workspace. @return int: error code (0:OK, -1:error) """ self.pulser_off() self.__currently_loaded_waveform = '' self.__current_waveform_name = '' self._seq = dict() self.__current_waveform = dict() def get_status(self): """ Retrieves the status of the pulsing hardware @return (int, dict): tuple with an integer value of the current status and a corresponding dictionary containing status description for all the possible status variables of the pulse generator hardware. """ status_dic = dict() status_dic[-1] = 'Failed Request or Failed Communication with device.' status_dic[0] = 'Device has stopped, but can receive commands.' status_dic[1] = 'Device is active and running.' return self.__current_status, status_dic def get_sample_rate(self): """ Get the sample rate of the pulse generator hardware @return float: The current sample rate of the device (in Hz) Do not return a saved sample rate in a class variable, but instead retrieve the current sample rate directly from the device. """ return self.__sample_rate def set_sample_rate(self, sample_rate): """ Set the sample rate of the pulse generator hardware. @param float sample_rate: The sampling rate to be set (in Hz) @return float: the sample rate returned from the device. Note: After setting the sampling rate of the device, retrieve it again for obtaining the actual set value and use that information for further processing. """ self.log.debug('PulseStreamer sample rate cannot be configured') return self.__sample_rate def get_analog_level(self, amplitude=None, offset=None): """ Retrieve the analog amplitude and offset of the provided channels. @param list amplitude: optional, if the amplitude value (in Volt peak to peak, i.e. the full amplitude) of a specific channel is desired. @param list offset: optional, if the offset value (in Volt) of a specific channel is desired. @return: (dict, dict): tuple of two dicts, with keys being the channel descriptor string (i.e. 'a_ch1') and items being the values for those channels. Amplitude is always denoted in Volt-peak-to-peak and Offset in volts. Note: Do not return a saved amplitude and/or offset value but instead retrieve the current amplitude and/or offset directly from the device. If nothing (or None) is passed then the levels of all channels will be returned. If no analog channels are present in the device, return just empty dicts. Example of a possible input: amplitude = ['a_ch1', 'a_ch4'], offset = None to obtain the amplitude of channel 1 and 4 and the offset of all channels {'a_ch1': -0.5, 'a_ch4': 2.0} {'a_ch1': 0.0, 'a_ch2': 0.0, 'a_ch3': 1.0, 'a_ch4': 0.0} """ return {},{} def set_analog_level(self, amplitude=None, offset=None): """ Set amplitude and/or offset value of the provided analog channel(s). @param dict amplitude: dictionary, with key being the channel descriptor string (i.e. 'a_ch1', 'a_ch2') and items being the amplitude values (in Volt peak to peak, i.e. the full amplitude) for the desired channel. @param dict offset: dictionary, with key being the channel descriptor string (i.e. 'a_ch1', 'a_ch2') and items being the offset values (in absolute volt) for the desired channel. @return (dict, dict): tuple of two dicts with the actual set values for amplitude and offset for ALL channels. If nothing is passed then the command will return the current amplitudes/offsets. Note: After setting the amplitude and/or offset values of the device, use the actual set return values for further processing. """ return {},{} def get_digital_level(self, low=None, high=None): """ Retrieve the digital low and high level of the provided channels. @param list low: optional, if a specific low value (in Volt) of a channel is desired. @param list high: optional, if a specific high value (in Volt) of a channel is desired. @return: (dict, dict): tuple of two dicts, with keys being the channel number and items being the values for those channels. Both low and high value of a channel is denoted in (absolute) Voltage. Note: Do not return a saved low and/or high value but instead retrieve the current low and/or high value directly from the device. If no entries provided then the levels of all channels where simply returned. If no digital channels provided, return just an empty dict. Example of a possible input: low = [1,4] to obtain the low voltage values of digital channel 1 an 4. A possible answer might be {1: -0.5, 4: 2.0} {} since no high request was performed. The major difference to analog signals is that digital signals are either ON or OFF, whereas analog channels have a varying amplitude range. In contrast to analog output levels, digital output levels are defined by a voltage, which corresponds to the ON status and a voltage which corresponds to the OFF status (both denoted in (absolute) voltage) In general there is no bijective correspondence between (amplitude, offset) and (value high, value low)! """ if low is None: low = [] if high is None: high = [] low_dict = {} high_dict = {} if low is [] and high is []: for channel in range(8): low_dict[channel] = 0.0 high_dict[channel] = 3.3 else: for channel in low: low_dict[channel] = 0.0 for channel in high: high_dict[channel] = 3.3 return low_dict, high_dict def set_digital_level(self, low=None, high=None): """ Set low and/or high value of the provided digital channel. @param dict low: dictionary, with key being the channel and items being the low values (in volt) for the desired channel. @param dict high: dictionary, with key being the channel and items being the high values (in volt) for the desired channel. @return (dict, dict): tuple of two dicts where first dict denotes the current low value and the second dict the high value. If nothing is passed then the command will return two empty dicts. Note: After setting the high and/or low values of the device, retrieve them again for obtaining the actual set value(s) and use that information for further processing. The major difference to analog signals is that digital signals are either ON or OFF, whereas analog channels have a varying amplitude range. In contrast to analog output levels, digital output levels are defined by a voltage, which corresponds to the ON status and a voltage which corresponds to the OFF status (both denoted in (absolute) voltage) In general there is no bijective correspondence between (amplitude, offset) and (value high, value low)! """ if low is None: low = {} if high is None: high = {} self.log.warning('PulseStreamer logic level cannot be adjusted!') return self.get_digital_level() def get_active_channels(self, ch=None): """ Get the active channels of the pulse generator hardware. @param list ch: optional, if specific analog or digital channels are needed to be asked without obtaining all the channels. @return dict: where keys denoting the channel string and items boolean expressions whether channel are active or not. Example for an possible input (order is not important): ch = ['a_ch2', 'd_ch2', 'a_ch1', 'd_ch5', 'd_ch1'] then the output might look like {'a_ch2': True, 'd_ch2': False, 'a_ch1': False, 'd_ch5': True, 'd_ch1': False} If no parameter (or None) is passed to this method all channel states will be returned. """ if ch is None: ch = {} d_ch_dict = {} if len(ch) < 1: for chnl in range(1, 9): d_ch_dict['d_ch{0}'.format(chnl)] = True else: for channel in ch: d_ch_dict[channel] = True return d_ch_dict def set_active_channels(self, ch=None): """ Set the active/inactive channels for the pulse generator hardware. The state of ALL available analog and digital channels will be returned (True: active, False: inactive). The actually set and returned channel activation must be part of the available activation_configs in the constraints. You can also activate/deactivate subsets of available channels but the resulting activation_config must still be valid according to the constraints. If the resulting set of active channels can not be found in the available activation_configs, the channel states must remain unchanged. @param dict ch: dictionary with keys being the analog or digital string generic names for the channels (i.e. 'd_ch1', 'a_ch2') with items being a boolean value. True: Activate channel, False: Deactivate channel @return dict: with the actual set values for ALL active analog and digital channels If nothing is passed then the command will simply return the unchanged current state. Note: After setting the active channels of the device, use the returned dict for further processing. Example for possible input: ch={'a_ch2': True, 'd_ch1': False, 'd_ch3': True, 'd_ch4': True} to activate analog channel 2 digital channel 3 and 4 and to deactivate digital channel 1. All other available channels will remain unchanged. """ if ch is None: ch = {} d_ch_dict = { 'd_ch1': True, 'd_ch2': True, 'd_ch3': True, 'd_ch4': True, 'd_ch5': True, 'd_ch6': True, 'd_ch7': True, 'd_ch8': True} return d_ch_dict def write_waveform(self, name, analog_samples, digital_samples, is_first_chunk, is_last_chunk, total_number_of_samples): """ Write a new waveform or append samples to an already existing waveform on the device memory. The flags is_first_chunk and is_last_chunk can be used as indicator if a new waveform should be created or if the write process to a waveform should be terminated. NOTE: All sample arrays in analog_samples and digital_samples must be of equal length! @param str name: the name of the waveform to be created/append to @param dict analog_samples: keys are the generic analog channel names (i.e. 'a_ch1') and values are 1D numpy arrays of type float32 containing the voltage samples. @param dict digital_samples: keys are the generic digital channel names (i.e. 'd_ch1') and values are 1D numpy arrays of type bool containing the marker states. @param bool is_first_chunk: Flag indicating if it is the first chunk to write. If True this method will create a new empty wavveform. If False the samples are appended to the existing waveform. @param bool is_last_chunk: Flag indicating if it is the last chunk to write. Some devices may need to know when to close the appending wfm. @param int total_number_of_samples: The number of sample points for the entire waveform (not only the currently written chunk) @return (int, list): Number of samples written (-1 indicates failed process) and list of created waveform names """ if analog_samples: self.log.debug('Analog not yet implemented for pulse streamer') return -1, list() if is_first_chunk: self.__current_waveform_name = name self.__samples_written = 0 # initalise to a dict of lists that describe pulse pattern in swabian language self.__current_waveform = {key:[] for key in digital_samples.keys()} for channel_number, samples in digital_samples.items(): new_channel_indices = np.where(samples[:-1] != samples[1:])[0] new_channel_indices = np.unique(new_channel_indices) # add in indices for the start and end of the sequence to simplify iteration new_channel_indices = np.insert(new_channel_indices, 0, [-1]) new_channel_indices = np.insert(new_channel_indices, new_channel_indices.size, [samples.shape[0] - 1]) pulses = [] for new_channel_index in range(1, new_channel_indices.size): pulse = [new_channel_indices[new_channel_index] - new_channel_indices[new_channel_index - 1], samples[new_channel_indices[new_channel_index - 1] + 1].astype(np.byte)] pulses.append(pulse) # extend (as opposed to rewrite) for chunky business #print(pulses) self.__current_waveform[channel_number].extend(pulses) return len(samples), [self.__current_waveform_name] def write_sequence(self, name, sequence_parameters): """ Write a new sequence on the device memory. @param str name: the name of the waveform to be created/append to @param list sequence_parameters: List containing tuples of length 2. Each tuple represents a sequence step. The first entry of the tuple is a list of waveform names (str); one for each channel. The second tuple element is a SequenceStep instance containing the sequencing parameters for this step. @return: int, number of sequence steps written (-1 indicates failed process) """ self.log.debug('Sequencing not yet implemented for pulse streamer') return -1 def get_waveform_names(self): """ Retrieve the names of all uploaded waveforms on the device. @return list: List of all uploaded waveform name strings in the device workspace. """ waveform_names = list() if self.__current_waveform_name != '' and self.__current_waveform_name is not None: waveform_names = [self.__current_waveform_name] return waveform_names def get_sequence_names(self): """ Retrieve the names of all uploaded sequence on the device. @return list: List of all uploaded sequence name strings in the device workspace. """ return list() def delete_waveform(self, waveform_name): """ Delete the waveform with name "waveform_name" from the device memory. @param str waveform_name: The name of the waveform to be deleted Optionally a list of waveform names can be passed. @return list: a list of deleted waveform names. """ return list() def delete_sequence(self, sequence_name): """ Delete the sequence with name "sequence_name" from the device memory. @param str sequence_name: The name of the sequence to be deleted Optionally a list of sequence names can be passed. @return list: a list of deleted sequence names. """ return list() def get_interleave(self): """ Check whether Interleave is ON or OFF in AWG. @return bool: True: ON, False: OFF Will always return False for pulse generator hardware without interleave. """ return False def set_interleave(self, state=False): """ Turns the interleave of an AWG on or off. @param bool state: The state the interleave should be set to (True: ON, False: OFF) @return bool: actual interleave status (True: ON, False: OFF) Note: After setting the interleave of the device, retrieve the interleave again and use that information for further processing. Unused for pulse generator hardware other than an AWG. """ if state: self.log.error('No interleave functionality available in FPGA pulser.\n' 'Interleave state is always False.') return False def reset(self): """ Reset the device. @return int: error code (0:OK, -1:error) """ self.pulse_streamer.reset() self.__currently_loaded_waveform = '' def has_sequence_mode(self): """ Asks the pulse generator whether sequence mode exists. @return: bool, True for yes, False for no. """ return False
class M3202A(Base, PulserInterface): """ Qudi module for the Keysight M3202A PXIe AWG card (1GHz sampling frequency) Example config for copy-paste: keysight_m3202a: module.Class: 'awg.keysight_M3202A.M3202A' awg_serial: 0000000000 # here the serial number of current AWG """ _modclass = 'M3202A' _modtype = 'hardware' # config options serial = ConfigOption(name='awg_serial', missing='error') __ch_map = { 'a_ch1': 1, 'a_ch2': 2, 'a_ch3': 3, 'a_ch4': 4 } def on_activate(self): self.analog_amplitudes = {} self.analog_offsets = {} # loaded sequence self.last_sequence = None # loaded waveforms, channel -> waveform name self.loaded_waveforms = {} # uploaded waveforms, waveform name -> instrument wfm number self.written_waveforms = {} self.chcfg = { 'a_ch1': M3202ChannelCfg(), 'a_ch2': M3202ChannelCfg(), 'a_ch3': M3202ChannelCfg(), 'a_ch4': M3202ChannelCfg(), } constraints = PulserConstraints() constraints.sample_rate.min = 4e8 constraints.sample_rate.max = 1e9 constraints.sample_rate.step = 1.0 constraints.sample_rate.default = 1e9 constraints.a_ch_amplitude.min = 0 constraints.a_ch_amplitude.max = 1.5 constraints.a_ch_amplitude.step = 0.01 constraints.a_ch_amplitude.default = 1.5 constraints.a_ch_offset.min = 0 constraints.a_ch_offset.max = 1.5 constraints.a_ch_offset.step = 0.01 constraints.a_ch_offset.default = 0.0 # FIXME: Enter the proper digital channel low constraints: constraints.d_ch_low.min = 0.0 constraints.d_ch_low.max = 0.0 constraints.d_ch_low.step = 0.0 constraints.d_ch_low.default = 0.0 # FIXME: Enter the proper digital channel high constraints: constraints.d_ch_high.min = 0.0 constraints.d_ch_high.max = 0.0 constraints.d_ch_high.step = 0.0 constraints.d_ch_high.default = 0.0 constraints.waveform_length.min = 30 constraints.waveform_length.max = 1e9 constraints.waveform_length.step = 10 constraints.waveform_length.default = 1000 # FIXME: Check the proper number for your device constraints.waveform_num.min = 1 constraints.waveform_num.max = 1024 constraints.waveform_num.step = 1 constraints.waveform_num.default = 1 # FIXME: Check the proper number for your device constraints.sequence_num.min = 1 constraints.sequence_num.max = 1 constraints.sequence_num.step = 1 constraints.sequence_num.default = 1 # FIXME: Check the proper number for your device constraints.subsequence_num.min = 0 constraints.subsequence_num.max = 0 constraints.subsequence_num.step = 0 constraints.subsequence_num.default = 0 # If sequencer mode is available then these should be specified constraints.repetitions.min = 0 constraints.repetitions.max = 65536 constraints.repetitions.step = 1 constraints.repetitions.default = 0 # ToDo: Check how many external triggers are available constraints.event_triggers = ['SOFT', 'EXT', 'SOFT_CYCLE', 'EXT_CYCLE'] constraints.flags = [] constraints.sequence_steps.min = 1 constraints.sequence_steps.max = 1024 constraints.sequence_steps.step = 1 constraints.sequence_steps.default = 1 activation_config = OrderedDict() activation_config['all'] = frozenset({'a_ch1', 'a_ch2', 'a_ch3', 'a_ch4'}) activation_config['one'] = frozenset({'a_ch1'}) activation_config['two'] = frozenset({'a_ch1', 'a_ch2'}) activation_config['three'] = frozenset({'a_ch1', 'a_ch2', 'a_ch3'}) constraints.activation_config = activation_config # FIXME: additional constraint really necessary? constraints.dac_resolution = {'min': 14, 'max': 14, 'step': 1, 'unit': 'bit'} constraints.sequence_option = SequenceOption.FORCED self._constraints = constraints self.awg = ksd1.SD_AOU() aouID = self.awg.openWithSerialNumberCompatibility( 'M3202A', self.serial, ksd1.SD_Compatibility.KEYSIGHT) # Check AWG Connection for errors if aouID < 0: self.awg.close() raise Exception('AWG Error: {0} {1}'.format(aouID, ksd1.SD_Error.getErrorMessage(aouID))) self.ser = self.awg.getSerialNumber() self.model = self.awg.getProductName() self.fwver = self.awg.getFirmwareVersion() self.hwver = self.awg.getHardwareVersion() self.chassis = self.awg.getChassis() self.ch_slot = self.awg.getSlot() self.reset() self.log.info('Keysight AWG Model: {} serial: {} ' 'FW Ver: {} HW Ver: {} Chassis: {} Slot: {}' ''.format(self.model, self.ser, self.fwver, self.hwver, self.chassis, self.ch_slot)) def on_deactivate(self): self.awg.close() def reset(self): """ Reset the device. @return int: error code (0:OK, -1:error) """ activation_dict = self.get_active_channels() active_channels = {chnl for chnl in activation_dict if activation_dict[chnl]} for chan in active_channels: ch = self.__ch_map[chan] self.log.debug('Stop Ch{} {}'.format(ch, self.awg.AWGstop(ch))) self.log.debug('Flush Ch{} {}'.format(ch, self.awg.AWGflush(ch))) self.log.debug( 'WaveShape Ch{} {}'.format( ch, self.awg.channelWaveShape(ch, ksd1.SD_Waveshapes.AOU_AWG))) self.awg.waveformFlush() # loaded sequence self.last_sequence = None # loaded waveforms, channel -> waveform name self.loaded_waveforms = {} # uploaded waveforms, waveform name -> instrument wfm number self.written_waveforms = {} amps = { ch: self._constraints.a_ch_amplitude.default for ch, en in self.get_active_channels().items() if en} offs = { ch: self._constraints.a_ch_offset.default for ch, en in self.get_active_channels().items() if en} self.set_analog_level(amps, offs) return 0 def get_constraints(self): """ Retrieve the hardware constrains from the Pulsing device. @return constraints object: object with pulser constraints as attributes. """ return self._constraints def pulser_on(self): """ Switches the pulsing device on. @return int: error code (0:OK, -1:error) """ if self.last_sequence is None: self.log.error('This AWG only supports sequences. Please put the waveform in a sequence and then load it.') return -1 else: self.log.debug('StartMultiple {}'.format(self.awg.AWGstartMultiple(0b1111))) return 0 def pulser_off(self): """ Switches the pulsing device off. @return int: error code (0:OK, -1:error) """ self.log.debug('StopMultiple {}'.format(self.awg.AWGstopMultiple(0b1111))) return 0 def load_waveform(self, load_dict): """ Loads a waveform to the specified channel of the pulsing device. @param load_dict: dict|list, a dictionary with keys being one of the available channel @return dict: Dictionary containing the actually loaded waveforms per channel. """ if isinstance(load_dict, list): new_dict = dict() for waveform in load_dict: channel = int(waveform.rsplit('_ch', 1)[1]) new_dict[channel] = waveform load_dict = new_dict # Get all active channels chnl_activation = self.get_active_channels() analog_channels = natural_sort( chnl for chnl in chnl_activation if chnl.startswith('a') and chnl_activation[chnl]) # Load waveforms into channels for chnl_num, waveform in load_dict.items(): self.loaded_waveforms[chnl_num] = waveform self.last_sequence = None return self.get_loaded_assets() def load_sequence(self, sequence_name): """ Loads a sequence to the channels of the device in order to be ready for playback. @param sequence_name: dict|list, a dictionary with keys being one of the available channel @return dict: Dictionary containing the actually loaded waveforms per channel. """ return self.get_loaded_assets() def get_loaded_assets(self): """ Retrieve the currently loaded asset names for each active channel of the device. @return (dict, str): Dictionary with keys being the channel number and values being the respective asset loaded into the channel, string describing the asset type ('waveform' or 'sequence') """ if self.last_sequence is None: return self.loaded_waveforms, 'waveform' return self.loaded_waveforms, 'sequence' def clear_all(self): """ Clears all loaded waveforms from the pulse generators RAM/workspace. @return int: error code (0:OK, -1:error) """ self.reset() return 0 def get_status(self): """ Retrieves the status of the pulsing hardware @return (int, dict): tuple with an interger value of the current status and a corresponding dictionary containing status description for all the possible status variables of the pulse generator hardware. """ status_dic = { -1: 'Failed Request or Communication', 0: 'Device has stopped, but can receive commands', 1: 'One channel running', 2: 'Two channels running', 3: 'Three channels running', 4: 'Four channels running' } current_status = 0 for ch in self.get_active_channels(): if self.awg.AWGisRunning(self.__ch_map[ch]): current_status += 1 # All the other status messages should have higher integer values then 1. return current_status, status_dic def get_sample_rate(self): """ Get the sample rate of the pulse generator hardware @return float: The current sample rate of the device (in Hz) Do not return a saved sample rate from an attribute, but instead retrieve the current sample rate directly from the device. """ return self.awg.clockGetFrequency() def set_sample_rate(self, sample_rate): """ Set the sample rate of the pulse generator hardware. @param float sample_rate: The sampling rate to be set (in Hz) @return float: the sample rate returned from the device (in Hz). """ return self.awg.clockSetFrequency(sample_rate, ksd1.SD) def get_analog_level(self, amplitude=None, offset=None): """ Retrieve the analog amplitude and offset of the provided channels. @param list amplitude: optional, if the amplitude value (in Volt peak to peak, i.e. the full amplitude) of a specific channel is desired. @param list offset: optional, if the offset value (in Volt) of a specific channel is desired. @return: (dict, dict): tuple of two dicts, with keys being the channel descriptor string """ if amplitude is None: amplitude = ['a_ch1', 'a_ch2', 'a_ch3', 'a_ch4'] if offset is None: offset = ['a_ch1', 'a_ch2', 'a_ch3', 'a_ch4'] ret_amp = {k: self.analog_amplitudes[k] for k in amplitude} ret_off = {k: self.analog_offsets[k] for k in offset} return ret_amp, ret_off def set_analog_level(self, amplitude=None, offset=None): """ Set amplitude and/or offset value of the provided analog channel(s). @param dict amplitude: dictionary, with key being the channel descriptor string (i.e. 'a_ch1', 'a_ch2') and items being the amplitude values (in Volt peak to peak, i.e. the full amplitude) for the desired channel. @param dict offset: dictionary, with key being the channel descriptor string (i.e. 'a_ch1', 'a_ch2') and items being the offset values (in absolute volt) for the desired channel. @return (dict, dict): tuple of two dicts with the actual set values for amplitude and offset for ALL channels. """ for ch, ampl in amplitude.items(): self.awg.channelAmplitude(self.__ch_map[ch], ampl) self.analog_amplitudes[ch] = ampl for ch, off in offset.items(): self.awg.channelOffset(self.__ch_map[ch], off) self.analog_offsets[ch] = off self.log.debug('analog amp: {} offset: {}' ''.format(self.analog_amplitudes, self.analog_offsets)) return self.analog_amplitudes, self.analog_offsets def get_digital_level(self, low=None, high=None): """ Retrieve the digital low and high level of the provided/all channels. @param list low: optional, if the low value (in Volt) of a specific channel is desired. @param list high: optional, if the high value (in Volt) of a specific channel is desired. @return: (dict, dict): tuple of two dicts, with keys being the channel descriptor strings (i.e. 'd_ch1', 'd_ch2') and items being the values for those channels. Both low and high value of a channel is denoted in volts. """ return {}, {} def set_digital_level(self, low=None, high=None): """ Set low and/or high value of the provided digital channel. @param dict low: dictionary, with key being the channel descriptor string (i.e. 'd_ch1', 'd_ch2') and items being the low values (in volt) for the desired channel. @param dict high: dictionary, with key being the channel descriptor string (i.e. 'd_ch1', 'd_ch2') and items being the high values (in volt) for the desired channel. @return (dict, dict): tuple of two dicts where first dict denotes the current low value and the second dict the high value for ALL digital channels. Keys are the channel descriptor strings (i.e. 'd_ch1', 'd_ch2') """ self.log.warning('no digital levels set') return {}, {} def get_active_channels(self, ch=None): """ Get the active channels of the pulse generator hardware. @param list ch: optional, if specific analog or digital channels are needed to be asked without obtaining all the channels. @return dict: where keys denoting the channel string and items boolean expressions whether channel are active or not. """ if ch is None: ch = ['a_ch1', 'a_ch2', 'a_ch3', 'a_ch4'] return {k: True for k in ch} def set_active_channels(self, ch=None): """ Set the active channels for the pulse generator hardware. @param dict ch: dictionary with keys being the analog or digital string generic names for the channels (i.e. 'd_ch1', 'a_ch2') with items being a boolean value. True: Activate channel, False: Deactivate channel @return dict: with the actual set values for ALL active analog and digital channels """ ch = ['a_ch1', 'a_ch2', 'a_ch3', 'a_ch4'] return {k: True for k in ch} def write_waveform(self, name, analog_samples, digital_samples, is_first_chunk, is_last_chunk, total_number_of_samples): """ Write a new waveform or append samples to an already existing waveform on the device memory. @param name: str, waveform name, human readabla @param analog_samples: numpy.ndarray of type float32 containing the voltage samples @param digital_samples: numpy.ndarray of type bool containing the marker states (if analog channels are active, this must be the same length as analog_samples) @param is_first_chunk: bool, flag indicating if it is the first chunk to write. If True this method will create a new empty wavveform. If False the samples are appended to the existing waveform. @param is_last_chunk: bool, flag indicating if it is the last chunk to write. Some devices may need to know when to close the appending wfm. @param total_number_of_samples: int, The number of sample points for the entire waveform (not only the currently written chunk) @return: (int, list) number of samples written (-1 indicates failed process) and list of created waveform names """ tstart = datetime.datetime.now() self.log.debug('@{} write wfm: {} first: {} last: {} {}'.format( datetime.datetime.now() - tstart, name, is_first_chunk, is_last_chunk, total_number_of_samples)) waveforms = list() min_samples = 30 if not (is_first_chunk and is_last_chunk): self.log.error('Chunked Write not supported by this device.') return -1, waveforms # Sanity checks if len(analog_samples) == 0: self.log.error('No analog samples passed to write_waveform.') return -1, waveforms if total_number_of_samples < min_samples: self.log.error('Unable to write waveform.' '\nNumber of samples to write ({0:d}) is ' 'smaller than the allowed minimum waveform length ({1:d}).' ''.format(total_number_of_samples, min_samples)) return -1, waveforms # determine active channels activation_dict = self.get_active_channels() active_channels = {chnl for chnl in activation_dict if activation_dict[chnl]} active_analog = natural_sort(chnl for chnl in active_channels if chnl.startswith('a')) # Sanity check of channel numbers if active_channels != set(analog_samples.keys()).union(set(digital_samples.keys())): self.log.error('Mismatch of channel activation and sample array dimensions for ' 'waveform creation.\nChannel activation is: {0}\nSample arrays have: ' ''.format(active_channels, set(analog_samples.keys()).union(set(digital_samples.keys())))) return -1, waveforms for a_ch in active_analog: a_ch_num = self.__ch_map[a_ch] wfm_name = '{0}_ch{1:d}'.format(name, a_ch_num) wfm = ksd1.SD_Wave() analog_samples[a_ch] = analog_samples[a_ch].astype('float64') / 2 self.log.debug('wfmobj: {} {} {} min: {} max: {}'.format( a_ch, name, wfm_name, np.min(analog_samples[a_ch]), np.max(analog_samples[a_ch]))) self.log.debug('@{} Before new wfm {}'.format(datetime.datetime.now() - tstart, a_ch)) wfmid = self._fast_newFromArrayDouble( wfm, ksd1.SD_WaveformTypes.WAVE_ANALOG, analog_samples[a_ch]) self.log.debug('@{} After new wfm {}'.format(datetime.datetime.now() - tstart, a_ch)) if wfmid < 0: self.log.error('Device error when creating waveform {} ch: {}: {} {}' ''.format(wfm_name, a_ch, wfmid, ksd1.SD_Error.getErrorMessage(wfmid))) return -1, waveforms if len(self.written_waveforms) > 0: wfm_nr = max(set(self.written_waveforms.values())) + 1 else: wfm_nr = 1 self.log.debug('@{} Before loading wfm {} '.format(datetime.datetime.now() - tstart, a_ch)) written = self.awg.waveformLoad(wfm, wfm_nr) self.log.debug('@{} Samples written: {} {} '.format(datetime.datetime.now() - tstart, a_ch, wfm, written)) if written < 0: self.log.error('Device error when uploading waveform {} id: {}: {} {}' ''.format(wfm, wfm_nr, written, ksd1.SD_Error.getErrorMessage(written))) return -1, waveforms self.written_waveforms[wfm_name] = wfm_nr waveforms.append(wfm_name) self.log.debug('@{} Finished writing waveforms'.format(datetime.datetime.now() - tstart)) return total_number_of_samples, waveforms def write_sequence(self, name, sequence_parameter_list): """ Write a new sequence on the device memory. @param name: str, the name of the waveform to be created/append to @param sequence_parameter_list: list, contains the parameters for each sequence step and the according waveform names. @return: int, number of sequence steps written (-1 indicates failed process) """ steps_written = 0 wfms_added = {} # Check if all waveforms are present on device memory avail_waveforms = set(self.get_waveform_names()) for waveform_tuple, param_dict in sequence_parameter_list: if not avail_waveforms.issuperset(waveform_tuple): self.log.error('Failed to create sequence "{0}" due to waveforms "{1}" not ' 'present in device memory.'.format(name, waveform_tuple)) return -1 active_analog = natural_sort(chnl for chnl in self.get_active_channels() if chnl.startswith('a')) num_tracks = len(active_analog) num_steps = len(sequence_parameter_list) for a_ch in active_analog: self.awg.AWGflush(self.__ch_map[a_ch]) self.awg.channelWaveShape(self.__ch_map[a_ch], ksd1.SD_Waveshapes.AOU_AWG) # Fill in sequence information for step, (wfm_tuple, seq_params) in enumerate(sequence_parameter_list, 1): # Set waveforms to play if num_tracks == len(wfm_tuple): for track, waveform in enumerate(wfm_tuple, 1): # Triggers !!! wfm_nr = self.written_waveforms[waveform] if seq_params['wait_for'] == 'SOFT': trig = ksd1.SD_TriggerModes.SWHVITRIG self.log.debug('Ch{} Trig SOFT'.format(track)) elif seq_params['wait_for'] == 'EXT': trig = ksd1.SD_TriggerModes.EXTTRIG self.log.debug('Ch{} Trig EXT'.format(track)) elif seq_params['wait_for'] == 'SOFT_CYCLE': trig = ksd1.SD_TriggerModes.SWHVITRIG_CYCLE self.log.debug('Ch{} Trig SOFT_CYCLE'.format(track)) elif seq_params['wait_for'] == 'EXT_CYCLE': trig = ksd1.SD_TriggerModes.EXTTRIG_CYCLE self.log.debug('Ch{} Trig EXT_CYCLE'.format(track)) else: self.log.debug('Ch{} TrigAuto'.format(track)) trig = ksd1.SD_TriggerModes.AUTOTRIG cycles = seq_params['repetitions'] + 1 prescale = 0 delay = 0 ret = self.awg.AWGqueueWaveform(track, wfm_nr, trig, delay, cycles, prescale) self.log.debug('Sequence: {} Ch{} {} No{}'.format( name, track, waveform, wfm_nr) ) self.log.debug('Sequence Step: {0} Ch{1} No{2} Trig: {3} Del: {4} Rep: {5} Pre: {6} -> {7}'.format( step, track, wfm_nr, trig, delay, cycles, prescale, ret) ) if ret < 0: self.log.error('Error queueing wfm: {} {}'.format(ret, ksd1.SD_Error.getErrorMessage(ret))) return steps_written wfms_added[track] = '{0}_{1:d}'.format(name, track) steps_written += 1 else: self.log.error( 'Unable to write sequence.\nLength of waveform tuple "{0}" does not ' 'match the number of sequence tracks.'.format(wfm_tuple) ) return -1 # more setup for a_ch in active_analog: self.log.debug('QueueConfig {}'.format( self.awg.AWGqueueConfig(self.__ch_map[a_ch], 1))) self.log.debug('channelAmpliude {}'.format( self.awg.channelAmplitude(self.__ch_map[a_ch], self.analog_amplitudes[a_ch]))) if num_steps == steps_written: self.last_sequence = name self.loaded_waveforms = wfms_added self.set_channel_triggers(active_analog, sequence_parameter_list) return steps_written def get_waveform_names(self): """ Retrieve the names of all uploaded waveforms on the device. @return list: List of all uploaded waveform name strings in the device workspace. """ return list(self.written_waveforms.keys()) def get_sequence_names(self): """ Retrieve the names of all uploaded sequence on the device. @return list: List of all uploaded sequence name strings in the device workspace. """ return [self.last_sequence] def delete_waveform(self, waveform_name): """ Delete the waveform with name "waveform_name" from the device memory. @param str waveform_name: The name of the waveform to be deleted Optionally a list of waveform names can be passed. @return list: a list of deleted waveform names. """ return [] def delete_sequence(self, sequence_name): """ Delete the sequence with name "sequence_name" from the device memory. @param str sequence_name: The name of the sequence to be deleted Optionally a list of sequence names can be passed. @return list: a list of deleted sequence names. """ return [] def get_interleave(self): """ Check whether Interleave is ON or OFF in AWG. @return bool: True: ON, False: OFF Will always return False for pulse generator hardware without interleave. """ return False def set_interleave(self, state=False): """ Turns the interleave of an AWG on or off. @param bool state: The state the interleave should be set to (True: ON, False: OFF) @return bool: actual interleave status (True: ON, False: OFF) """ return False def write(self, command): """ Sends a command string to the device. @param string command: string containing the command @return int: error code (0:OK, -1:error) """ return -1 def query(self, question): """ Asks the device a 'question' and receive and return an answer from it. @param string question: string containing the command @return string: the answer of the device to the 'question' in a string """ return '' def _fast_newFromArrayDouble(self, wfm, waveformType, waveformDataA, waveformDataB=None): """ Reimplement newArrayFromDouble() for numpy arrays for massive speed gains. Original signature: int SD_Wave::newFromArrayDouble( int waveformType, double[] waveformDataA, double[] waveformDataB=0)); @param object wfm: SD1 waveform object @param object waveformType: SD1 waveform Type @param ndarray waveformDataA: array containing samples @param ndarray waveformDataB: optional array containing samples @return int: id of waveform or error code """ c_double_p = ctypes.POINTER(ctypes.c_double) if len(waveformDataA) > 0 and (waveformDataB is None or len(waveformDataA) == len(waveformDataB)): if isinstance(waveformDataA, np.ndarray): # print(type(waveformDataA), waveformDataA.dtype) waveform_dataA_C = waveformDataA.ctypes.data_as(c_double_p) length = len(waveformDataA) else: waveform_dataA_C = (ctypes.c_double * len(waveformDataA))(*waveformDataA) length = waveform_dataA_C._length_ if waveformDataB is None: waveform_dataB_C = ctypes.c_void_p(0) else: if isinstance(waveformDataB, np.ndarray): waveform_dataB_C = waveformDataB.ctypes.data_as(c_double_p) else: waveform_dataB_C = (ctypes.c_double * len(waveformDataB))(*waveformDataB) # print('newFromArray DLL', length, type(waveform_dataA_C), type(waveform_dataB_C)) wfm._SD_Object__handle = wfm._SD_Object__core_dll.SD_Wave_newFromArrayDouble( waveformType, length, waveform_dataA_C, waveform_dataB_C) return wfm._SD_Object__handle else: wfm._SD_Object__handle = 0 return ksd1.SD_Error.INVALID_VALUE def set_channel_triggers(self, active_channels, sequence_parameter_list): """ Set up triggers and markers according to configuration @param list active_channels: active aeg channels @param list sequence_parameter_list: liust with all sequence elements """ for ch in active_channels: if self.chcfg[ch].enable_trigger: trig_err = self.awg.AWGtriggerExternalConfig( self.__ch_map[ch], self.chcfg[ch].trig_source, self.chcfg[ch].trig_behaviour, self.chcfg[ch].trig_sync ) # io is trigger in if trigger enabled if self.chcfg[ch].trig_source == 0: self.log.info('IO IN for Ch{} '.format(self.__ch_map[ch])) err = self.awg.triggerIOconfig(ksd1.SD_TriggerDirections.AOU_TRG_IN) if err < 0: self.log.error('Error configuring triggers: {} {}'.format( err, ksd1.SD_Error.getErrorMessage(err))) self.log.info('Trig: Ch{} src: {} beh: {} sync: {}'.format( self.__ch_map[ch], self.chcfg[ch].trig_source, self.chcfg[ch].trig_behaviour, self.chcfg[ch].trig_sync, trig_err )) mark_err = self.awg.AWGqueueMarkerConfig( self.__ch_map[ch], self.chcfg[ch].mark_mode, self.chcfg[ch].mark_pxi, self.chcfg[ch].mark_io, self.chcfg[ch].mark_value, self.chcfg[ch].mark_sync, self.chcfg[ch].mark_length, self.chcfg[ch].mark_delay ) # I/O connector is a marker *only* if it is not configured as a trigger if self.chcfg[ch].mark_mode != ksd1.SD_MarkerModes.DISABLED and self.chcfg[ch].mark_io == 1: self.log.info('IO OUT for Ch{} '.format(self.__ch_map[ch])) if not (self.chcfg[ch].enable_trigger and self.chcfg[ch].trig_source == 0): err = self.awg.triggerIOconfig(ksd1.SD_TriggerDirections.AOU_TRG_OUT) if err < 0: self.log.error('Error configuring marker: {} {}'.format( err, ksd1.SD_Error.getErrorMessage(err))) else: self.log.warning('IO Trigger cfg for ch {} overrides marker cfg!'.format(ch)) self.log.info('Ch {} mm: {} pxi: {} io: {} val: {}, sync: {} len: {} delay: {} err: {}'.format( self.__ch_map[ch], self.chcfg[ch].mark_mode, self.chcfg[ch].mark_pxi, self.chcfg[ch].mark_io, self.chcfg[ch].mark_value, self.chcfg[ch].mark_sync, self.chcfg[ch].mark_length, self.chcfg[ch].mark_delay, mark_err )) self.log.debug('QueueSyncMode {}'.format( self.awg.AWGqueueSyncMode(self.__ch_map[ch], self.chcfg[ch].queue_sync))) def sync_clock(self): err = self.awg.clockResetPhase(1, 0, 0.0) clk = self.awg.clockIOconfig(1) freq = self.awg.clockGetFrequency() sfreq = self.awg.clockGetSyncFrequency() sfreq2 = self.awg.clockSetFrequency(freq) self.log.info('err: {} Clkcfg: {} SyncFreq: {} SyncFreq: {} Freq: {}'.format(err, clk, sfreq, sfreq2, freq))
class CTC100(Base): """ This module implements communication with CTC100 temperature controllers or clones/licensed devices. ATTENTION: This module is untested and very likely broken. Example config for copy-paste: tempcontroller_ctc100: module.Class: 'CTC100_temperature.CTC100' interface: 'ASRL1::INSTR' fitlogic: 'fitlogic' # name of the fitlogic module, see default config """ _modclass = 'ctc100' _modtype = 'hardware' # config options _interface = ConfigOption('interface', missing='error') def on_activate(self): """ Activate modeule """ self.connect(self._interface) def on_deactivate(self): """ Deactivate modeule """ self.disconnect() def connect(self, interface): """ Connect to Instrument. @param str interface: visa interface identifier @return bool: connection success """ try: self.rm = visa.ResourceManager() self.inst = self.rm.open_resource(interface, baud_rate=9600, term_chars='\n', send_end=True) except visa.VisaIOError as e: self.log.exception("") return False else: return True def disconnect(self): """ Close the connection to the instrument. """ self.inst.close() self.rm.close() def get_channel_names(self): """ Get a list of channel names. @return list(str): list of channel names """ return self.inst.ask('getOutputNames?').split(', ') def is_channel_selected(self, channel): """ Check if a channel is selectes @param str channel: channel name @return bool: whether channel is selected """ return self.inst.ask(channel.replace(" ", "") + '.selected?' ).split(' = ')[-1] == 'On' def is_output_on(self): """ Check if device outputs are enabled. @return bool: wheter device outputs are enabled """ result = self.inst.ask('OutputEnable?').split()[2] return result == 'On' def get_temp_by_name(self, name): """ Get temperature by name. @return float: temperature value """ return self.inst.ask_for_values('{}.value?'.format(name))[0] def get_all_outputs(self): """ Get a list of all output names @return list(str): output names """ names = self.get_channel_names() raw = self.inst.ask('getOutputs?').split(', ') values = [] for substr in raw: values.append(float(substr)) return dict(zip(names, values)) def get_selected_channels(self): """ Get all selected channels. @return dict: dict of channel_name: bool indicating selected channels """ names = self.get_channel_names() values = [] for channel in names: values.append(self.is_channel_selected(channel)) return dict(zip(names, values)) def channel_off(self, channel): """ Turn off channel. @param channel str: name of channel to turn off """ return self.inst.ask('{}.Off'.format(channel)).split(' = ')[1] def enable_output(self): """ Turn on all outputs. @return bool: whether turning on was successful """ if self.is_output_on(): return True else: result = self.inst.ask('OutputEnable = On').split()[2] return result == 'On' def disable_output(self): """ Turn off all outputs. @return bool: whether turning off was successful """ if self.is_output_on(): result = self.inst.ask('OutputEnable = Off').split()[2] return result == 'Off' else: return True
class SlowCounterDummy(Base, SlowCounterInterface): """ Dummy hardware class to emulate a slow counter with various distributions. Example config for copy-paste: slow_counter_dummy: module.Class: 'slow_counter_dummy.SlowCounterDummy' clock_frequency: 100 # in Hz samples_number: 10 source_channels: 2 count_distribution: 'dark_bright_gaussian' # other options are: # 'uniform, 'exponential', 'single_poisson', 'dark_bright_poisson' # and 'single_gaussian'. """ _modclass = 'SlowCounterDummy' _modtype = 'hardware' # config _clock_frequency = ConfigOption('clock_frequency', 100, missing='warn') _samples_number = ConfigOption('samples_number', 10, missing='warn') source_channels = ConfigOption('source_channels', 2, missing='warn') dist = ConfigOption('count_distribution', 'dark_bright_gaussian') # 'No parameter "count_distribution" given in the configuration for the' # 'Slow Counter Dummy. Possible distributions are "dark_bright_gaussian",' # '"uniform", "exponential", "single_poisson", "dark_bright_poisson"' # 'and "single_gaussian".' def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) def on_activate(self): """ Initialisation performed during activation of the module. """ # parameters if self.dist == 'dark_bright_poisson': self.mean_signal = 250 self.contrast = 0.2 else: self.mean_signal = 260 * 1000 self.contrast = 0.3 self.mean_signal2 = self.mean_signal - self.contrast * self.mean_signal self.noise_amplitude = self.mean_signal * 0.1 self.life_time_bright = 0.08 # 80 millisecond self.life_time_dark = 0.04 # 40 milliseconds # needed for the life time simulation self.current_dec_time = self.life_time_bright self.curr_state_b = True self.total_time = 0.0 def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. """ self.log.warning('slowcounterdummy>deactivation') def get_constraints(self): """ Return a constraints class for the slow counter.""" constraints = SlowCounterConstraints() constraints.min_count_frequency = 5e-5 constraints.max_count_frequency = 5e5 constraints.counting_mode = [ CountingMode.CONTINUOUS, CountingMode.GATED, CountingMode.FINITE_GATED ] return constraints def set_up_clock(self, clock_frequency=None, clock_channel=None): """ Configures the hardware clock of the NiDAQ card to give the timing. @param float clock_frequency: if defined, this sets the frequency of the clock @param string clock_channel: if defined, this is the physical channel of the clock @return int: error code (0:OK, -1:error) """ if clock_frequency is not None: self._clock_frequency = float(clock_frequency) self.log.warning('slowcounterdummy>set_up_clock') time.sleep(0.1) return 0 def set_up_counter(self, counter_channels=None, sources=None, clock_channel=None, counter_buffer=None): """ Configures the actual counter with a given clock. @param string counter_channel: if defined, this is the physical channel of the counter @param string photon_source: if defined, this is the physical channel where the photons are to count from @param string clock_channel: if defined, this specifies the clock for the counter @return int: error code (0:OK, -1:error) """ self.log.warning('slowcounterdummy>set_up_counter') time.sleep(0.1) return 0 def get_counter(self, samples=None): """ Returns the current counts per second of the counter. @param int samples: if defined, number of samples to read in one go @return float: the photon counts per second """ count_data = np.array([ self._simulate_counts(samples) + i * self.mean_signal for i, ch in enumerate(self.get_counter_channels()) ]) time.sleep(1 / self._clock_frequency * samples) return count_data def get_counter_channels(self): """ Returns the list of counter channel names. @return tuple(str): channel names Most methods calling this might just care about the number of channels, though. """ return ['Ctr{0}'.format(i) for i in range(self.source_channels)] def _simulate_counts(self, samples=None): """ Simulate counts signal from an APD. This can be called for each dummy counter channel. @param int samples: if defined, number of samples to read in one go @return float: the photon counts per second """ if samples is None: samples = int(self._samples_number) else: samples = int(samples) timestep = 1 / self._clock_frequency * samples # count data will be written here in the NumPy array count_data = np.empty([samples], dtype=np.uint32) for i in range(samples): if self.dist == 'single_gaussian': count_data[i] = np.random.normal(self.mean_signal, self.noise_amplitude / 2) elif self.dist == 'dark_bright_gaussian': self.total_time = self.total_time + timestep if self.total_time > self.current_dec_time: if self.curr_state_b: self.curr_state_b = False self.current_dec_time = np.random.exponential( self.life_time_dark) count_data[i] = np.random.poisson(self.mean_signal) else: self.curr_state_b = True self.current_dec_time = np.random.exponential( self.life_time_bright) self.total_time = 0.0 count_data[i] = ( np.random.normal(self.mean_signal, self.noise_amplitude) * self.curr_state_b + np.random.normal(self.mean_signal2, self.noise_amplitude) * (1 - self.curr_state_b)) elif self.dist == 'uniform': count_data[i] = self.mean_signal + random.uniform( -self.noise_amplitude / 2, self.noise_amplitude / 2) elif self.dist == 'exponential': count_data[i] = np.random.exponential(self.mean_signal) elif self.dist == 'single_poisson': count_data[i] = np.random.poisson(self.mean_signal) elif self.dist == 'dark_bright_poisson': self.total_time = self.total_time + timestep if self.total_time > self.current_dec_time: if self.curr_state_b: self.curr_state_b = False self.current_dec_time = np.random.exponential( self.life_time_dark) count_data[i] = np.random.poisson(self.mean_signal) else: self.curr_state_b = True self.current_dec_time = np.random.exponential( self.life_time_bright) self.total_time = 0.0 count_data[i] = ( np.random.poisson(self.mean_signal) * self.curr_state_b + np.random.poisson(self.mean_signal2) * (1 - self.curr_state_b)) else: # make uniform as default count_data[0][i] = self.mean_signal + random.uniform( -self.noise_amplitude / 2, self.noise_amplitude / 2) return count_data def close_counter(self): """ Closes the counter and cleans up afterwards. @return int: error code (0:OK, -1:error) """ self.log.warning('slowcounterdummy>close_counter') return 0 def close_clock(self, power=0): """ Closes the clock and cleans up afterwards. @return int: error code (0:OK, -1:error) """ self.log.warning('slowcounterdummy>close_clock') return 0
class OkFpgaPulser(Base, PulserInterface): """ Methods to control Pulse Generator running on OK FPGA. Chan PIN ---------- Ch1 A3 Ch2 C5 Ch3 D6 Ch4 B6 Ch5 C7 Ch6 B8 Ch7 D9 Ch8 C9 Example config for copy-paste: fpga_pulser_ok: module.Class: 'fpga_fastcounter.fast_pulser_qo.OkFpgaPulser' fpga_serial: '143400058N' fpga_type: 'XEM6310_LX150' """ _modclass = 'pulserinterface' _modtype = 'hardware' _fpga_serial = ConfigOption(name='fpga_serial', missing='error') _fpga_type = ConfigOption(name='fpga_type', default='XEM6310_LX150', missing='warn') __current_waveform = StatusVar(name='current_waveform', default=np.zeros(32, dtype='uint8')) __current_waveform_name = StatusVar(name='current_waveform_name', default='') __sample_rate = StatusVar(name='sample_rate', default=950e6) def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) self.__current_status = -1 self.__currently_loaded_waveform = '' # loaded and armed waveform name self.__samples_written = 0 self._fp3support = False self.fpga = None # Reference to the OK FrontPanel instance def on_activate(self): self.__samples_written = 0 self.__currently_loaded_waveform = '' self.fpga = ok.FrontPanel() self._connect_fpga() self.set_sample_rate(self.__sample_rate) def on_deactivate(self): self._disconnect_fpga() @__current_waveform.representer def _convert_current_waveform(self, waveform_bytearray): return np.frombuffer(waveform_bytearray, dtype='uint8') @__current_waveform.constructor def _recover_current_waveform(self, waveform_nparray): return bytearray(waveform_nparray.tobytes()) def get_constraints(self): """ Retrieve the hardware constrains from the Pulsing device. @return constraints object: object with pulser constraints as attributes. Provides all the constraints (e.g. sample_rate, amplitude, total_length_bins, channel_config, ...) related to the pulse generator hardware to the caller. SEE PulserConstraints CLASS IN pulser_interface.py FOR AVAILABLE CONSTRAINTS!!! If you are not sure about the meaning, look in other hardware files to get an impression. If still additional constraints are needed, then they have to be added to the PulserConstraints class. Each scalar parameter is an ScalarConstraints object defined in cor.util.interfaces. Essentially it contains min/max values as well as min step size, default value and unit of the parameter. PulserConstraints.activation_config differs, since it contain the channel configuration/activation information of the form: {<descriptor_str>: <channel_set>, <descriptor_str>: <channel_set>, ...} If the constraints cannot be set in the pulsing hardware (e.g. because it might have no sequence mode) just leave it out so that the default is used (only zeros). """ constraints = PulserConstraints() constraints.sample_rate.min = 500e6 constraints.sample_rate.max = 950e6 constraints.sample_rate.step = 450e6 constraints.sample_rate.default = 950e6 constraints.a_ch_amplitude.min = 0.0 constraints.a_ch_amplitude.max = 0.0 constraints.a_ch_amplitude.step = 0.0 constraints.a_ch_amplitude.default = 0.0 constraints.a_ch_offset.min = 0.0 constraints.a_ch_offset.max = 0.0 constraints.a_ch_offset.step = 0.0 constraints.a_ch_offset.default = 0.0 constraints.d_ch_low.min = 0.0 constraints.d_ch_low.max = 0.0 constraints.d_ch_low.step = 0.0 constraints.d_ch_low.default = 0.0 constraints.d_ch_high.min = 3.3 constraints.d_ch_high.max = 3.3 constraints.d_ch_high.step = 0.0 constraints.d_ch_high.default = 3.3 constraints.waveform_length.min = 1024 constraints.waveform_length.max = 134217728 constraints.waveform_length.step = 1 constraints.waveform_length.default = 1024 # the name a_ch<num> and d_ch<num> are generic names, which describe UNAMBIGUOUSLY the # channels. Here all possible channel configurations are stated, where only the generic # names should be used. The names for the different configurations can be customary chosen. activation_config = OrderedDict() activation_config['all'] = frozenset( {'d_ch1', 'd_ch2', 'd_ch3', 'd_ch4', 'd_ch5', 'd_ch6', 'd_ch7', 'd_ch8'}) constraints.activation_config = activation_config constraints.sequence_option = SequenceOption.NON return constraints def pulser_on(self): """ Switches the pulsing device on. @return int: error code (0:OK, -1:error) """ self.__current_status = 1 return self.write(0x01) def pulser_off(self): """ Switches the pulsing device off. @return int: error code (0:OK, -1:error) """ self.__current_status = 0 return self.write(0x00) def load_waveform(self, load_dict): """ Loads a waveform to the specified channel of the pulsing device. For devices that have a workspace (i.e. AWG) this will load the waveform from the device workspace into the channel. For a device without mass memory this will make the waveform/pattern that has been previously written with self.write_waveform ready to play. @param dict|list load_dict: a dictionary with keys being one of the available channel index and values being the name of the already written waveform to load into the channel. Examples: {1: rabi_ch1, 2: rabi_ch2} or {1: rabi_ch2, 2: rabi_ch1} If just a list of waveform names if given, the channel association will be invoked from the channel suffix '_ch1', '_ch2' etc. @return dict: Dictionary containing the actually loaded waveforms per channel. """ # Since only one waveform can be present at a time check if only a single name is given if isinstance(load_dict, list): waveforms = list(set(load_dict)) elif isinstance(load_dict, dict): waveforms = list(set(load_dict.values())) else: self.log.error('Method load_waveform expects a list of waveform names or a dict.') return self.get_loaded_assets()[0] if len(waveforms) != 1: self.log.error('FPGA pulser expects exactly one waveform name for load_waveform.') return self.get_loaded_assets()[0] waveform = waveforms[0] if waveform != self.__current_waveform_name: self.log.error('No waveform by the name "{0}" generated for FPGA pulser.\n' 'Only one waveform at a time can be held.'.format(waveform)) return self.get_loaded_assets()[0] # calculate size of the two bytearrays to be transmitted. The biggest part is tranfered # in 1024 byte blocks and the rest is transfered in 32 byte blocks big_bytesize = (len(self.__current_waveform) // 1024) * 1024 small_bytesize = len(self.__current_waveform) - big_bytesize # try repeatedly to upload the samples to the FPGA RAM # stop if the upload was successful loop_count = 0 while True: loop_count += 1 # reset FPGA self.reset() # upload sequence if big_bytesize != 0: # enable sequence write mode in FPGA self.write((255 << 24) + 2) # write to FPGA DDR2-RAM self.fpga.WriteToBlockPipeIn(0x80, 1024, self.__current_waveform[0:big_bytesize]) if small_bytesize != 0: # enable sequence write mode in FPGA self.write((8 << 24) + 2) # write to FPGA DDR2-RAM self.fpga.WriteToBlockPipeIn(0x80, 32, self.__current_waveform[big_bytesize:]) # check if upload was successful self.write(0x00) # start the pulse sequence self.__current_status = 1 self.write(0x01) # wait for 600ms time.sleep(0.6) # get status flags from FPGA flags = self.query() self.__current_status = 0 self.write(0x00) # check if the memory readout works. if flags == 0: self.log.info('Loading of waveform "{0}" to FPGA was successful.\n' 'Upload attempts needed: {1}'.format(waveform, loop_count)) self.__currently_loaded_waveform = waveform break if loop_count == 10: self.log.error('Unable to upload waveform to FPGA.\n' 'Abort loading after 10 failed attempts.') self.reset() break return self.get_loaded_assets()[0] def load_sequence(self, sequence_name): """ Loads a sequence to the channels of the device in order to be ready for playback. For devices that have a workspace (i.e. AWG) this will load the sequence from the device workspace into the channels. For a device without mass memory this will make the waveform/pattern that has been previously written with self.write_waveform ready to play. @param dict|list sequence_name: a dictionary with keys being one of the available channel index and values being the name of the already written waveform to load into the channel. Examples: {1: rabi_ch1, 2: rabi_ch2} or {1: rabi_ch2, 2: rabi_ch1} If just a list of waveform names if given, the channel association will be invoked from the channel suffix '_ch1', '_ch2' etc. @return dict: Dictionary containing the actually loaded waveforms per channel. """ self.log.warning('FPGA digital pulse generator has no sequencing capabilities.\n' 'load_sequence call ignored.') return dict() def get_loaded_assets(self): """ Retrieve the currently loaded asset names for each active channel of the device. The returned dictionary will have the channel numbers as keys. In case of loaded waveforms the dictionary values will be the waveform names. In case of a loaded sequence the values will be the sequence name appended by a suffix representing the track loaded to the respective channel (i.e. '<sequence_name>_1'). @return (dict, str): Dictionary with keys being the channel number and values being the respective asset loaded into the channel, string describing the asset type ('waveform' or 'sequence') """ asset_type = 'waveform' if self.__currently_loaded_waveform else None asset_dict = {chnl_num: self.__currently_loaded_waveform for chnl_num in range(1, 9)} return asset_dict, asset_type def clear_all(self): """ Clears all loaded waveforms from the pulse generators RAM/workspace. @return int: error code (0:OK, -1:error) """ self.pulser_off() self.__currently_loaded_waveform = '' self.__current_waveform_name = '' # just for good measures, write and load a empty waveform self.__current_waveform = bytearray(np.zeros(32)) self.__samples_written = 32 self.load_waveform([self.__current_waveform_name]) return 0 def get_status(self): """ Retrieves the status of the pulsing hardware @return (int, dict): tuple with an integer value of the current status and a corresponding dictionary containing status description for all the possible status variables of the pulse generator hardware. """ status_dic = dict() status_dic[-1] = 'Failed Request or Failed Communication with device.' status_dic[0] = 'Device has stopped, but can receive commands.' status_dic[1] = 'Device is active and running.' return self.__current_status, status_dic def get_sample_rate(self): """ Get the sample rate of the pulse generator hardware @return float: The current sample rate of the device (in Hz) """ return self.__sample_rate def set_sample_rate(self, sample_rate): """ Set the sample rate of the pulse generator hardware. @param float sample_rate: The sampling rate to be set (in Hz) @return float: the sample rate returned from the device (in Hz). Note: After setting the sampling rate of the device, use the actually set return value for further processing. """ if self.__current_status == 1: self.log.error('Can`t change the sample rate while the FPGA is running.') return self.__sample_rate # Round sample rate either to 500MHz or 950MHz since no other values are possible. if sample_rate < 725e6: self.__sample_rate = 500e6 bitfile_name = 'pulsegen_8chnl_500MHz_{0}.bit'.format(self._fpga_type.split('_')[1]) else: self.__sample_rate = 950e6 bitfile_name = 'pulsegen_8chnl_950MHz_{0}.bit'.format(self._fpga_type.split('_')[1]) bitfile_path = os.path.join(get_main_dir(), 'thirdparty', 'qo_fpga', bitfile_name) self.fpga.ConfigureFPGA(bitfile_path) self.log.info('FPGA pulse generator configured with {0}'.format(bitfile_path)) if self.fpga.IsFrontPanel3Supported(): self._fp3support = True else: self._fp3support = False self.log.warning('FrontPanel3 is not supported. ' 'Please check if the FPGA is directly connected by USB3.') self.__current_status = 0 return self.__sample_rate def get_analog_level(self, amplitude=None, offset=None): """ Retrieve the analog amplitude and offset of the provided channels. @param list amplitude: optional, if the amplitude value (in Volt peak to peak, i.e. the full amplitude) of a specific channel is desired. @param list offset: optional, if the offset value (in Volt) of a specific channel is desired. @return: (dict, dict): tuple of two dicts, with keys being the channel descriptor string (i.e. 'a_ch1') and items being the values for those channels. Amplitude is always denoted in Volt-peak-to-peak and Offset in volts. Note: Do not return a saved amplitude and/or offset value but instead retrieve the current amplitude and/or offset directly from the device. If nothing (or None) is passed then the levels of all channels will be returned. If no analog channels are present in the device, return just empty dicts. Example of a possible input: amplitude = ['a_ch1', 'a_ch4'], offset = None to obtain the amplitude of channel 1 and 4 and the offset of all channels {'a_ch1': -0.5, 'a_ch4': 2.0} {'a_ch1': 0.0, 'a_ch2': 0.0, 'a_ch3': 1.0, 'a_ch4': 0.0} """ self.log.warning('The FPGA has no analog channels.') return dict(), dict() def set_analog_level(self, amplitude=None, offset=None): """ Set amplitude and/or offset value of the provided analog channel(s). @param dict amplitude: dictionary, with key being the channel descriptor string (i.e. 'a_ch1', 'a_ch2') and items being the amplitude values (in Volt peak to peak, i.e. the full amplitude) for the desired channel. @param dict offset: dictionary, with key being the channel descriptor string (i.e. 'a_ch1', 'a_ch2') and items being the offset values (in absolute volt) for the desired channel. @return (dict, dict): tuple of two dicts with the actual set values for amplitude and offset for ALL channels. If nothing is passed then the command will return the current amplitudes/offsets. Note: After setting the amplitude and/or offset values of the device, use the actual set return values for further processing. """ self.log.warning('The FPGA has no analog channels.') return dict(), dict() def get_digital_level(self, low=None, high=None): """ Retrieve the digital low and high level of the provided/all channels. @param list low: optional, if the low value (in Volt) of a specific channel is desired. @param list high: optional, if the high value (in Volt) of a specific channel is desired. @return: (dict, dict): tuple of two dicts, with keys being the channel descriptor strings (i.e. 'd_ch1', 'd_ch2') and items being the values for those channels. Both low and high value of a channel is denoted in volts. Note: Do not return a saved low and/or high value but instead retrieve the current low and/or high value directly from the device. If nothing (or None) is passed then the levels of all channels are being returned. If no digital channels are present, return just an empty dict. Example of a possible input: low = ['d_ch1', 'd_ch4'] to obtain the low voltage values of digital channel 1 an 4. A possible answer might be {'d_ch1': -0.5, 'd_ch4': 2.0} {'d_ch1': 1.0, 'd_ch2': 1.0, 'd_ch3': 1.0, 'd_ch4': 4.0} Since no high request was performed, the high values for ALL channels are returned (here 4). """ if low: low_dict = {chnl: 0.0 for chnl in low} else: low_dict = {'d_ch{0:d}'.format(chnl + 1): 0.0 for chnl in range(8)} if high: high_dict = {chnl: 3.3 for chnl in high} else: high_dict = {'d_ch{0:d}'.format(chnl + 1): 3.3 for chnl in range(8)} return low_dict, high_dict def set_digital_level(self, low=None, high=None): """ Set low and/or high value of the provided digital channel. @param dict low: dictionary, with key being the channel descriptor string (i.e. 'd_ch1', 'd_ch2') and items being the low values (in volt) for the desired channel. @param dict high: dictionary, with key being the channel descriptor string (i.e. 'd_ch1', 'd_ch2') and items being the high values (in volt) for the desired channel. @return (dict, dict): tuple of two dicts where first dict denotes the current low value and the second dict the high value for ALL digital channels. Keys are the channel descriptor strings (i.e. 'd_ch1', 'd_ch2') If nothing is passed then the command will return the current voltage levels. Note: After setting the high and/or low values of the device, use the actual set return values for further processing. """ self.log.warning('FPGA pulse generator logic level cannot be adjusted!') return self.get_digital_level() def get_active_channels(self, ch=None): """ Get the active channels of the pulse generator hardware. @param list ch: optional, if specific analog or digital channels are needed to be asked without obtaining all the channels. @return dict: where keys denoting the channel string and items boolean expressions whether channel are active or not. Example for an possible input (order is not important): ch = ['a_ch2', 'd_ch2', 'a_ch1', 'd_ch5', 'd_ch1'] then the output might look like {'a_ch2': True, 'd_ch2': False, 'a_ch1': False, 'd_ch5': True, 'd_ch1': False} If no parameter (or None) is passed to this method all channel states will be returned. """ if ch: d_ch_dict = {chnl: True for chnl in ch} else: d_ch_dict = {'d_ch1': True, 'd_ch2': True, 'd_ch3': True, 'd_ch4': True, 'd_ch5': True, 'd_ch6': True, 'd_ch7': True, 'd_ch8': True} return d_ch_dict def set_active_channels(self, ch=None): """ Set the active/inactive channels for the pulse generator hardware. The state of ALL available analog and digital channels will be returned (True: active, False: inactive). The actually set and returned channel activation must be part of the available activation_configs in the constraints. You can also activate/deactivate subsets of available channels but the resulting activation_config must still be valid according to the constraints. If the resulting set of active channels can not be found in the available activation_configs, the channel states must remain unchanged. @param dict ch: dictionary with keys being the analog or digital string generic names for the channels (i.e. 'd_ch1', 'a_ch2') with items being a boolean value. True: Activate channel, False: Deactivate channel @return dict: with the actual set values for ALL active analog and digital channels If nothing is passed then the command will simply return the unchanged current state. Note: After setting the active channels of the device, use the returned dict for further processing. Example for possible input: ch={'a_ch2': True, 'd_ch1': False, 'd_ch3': True, 'd_ch4': True} to activate analog channel 2 digital channel 3 and 4 and to deactivate digital channel 1. All other available channels will remain unchanged. """ self.log.warning('The channels of the FPGA are always active.') return self.get_active_channels() def write_waveform(self, name, analog_samples, digital_samples, is_first_chunk, is_last_chunk, total_number_of_samples): """ Write a new waveform or append samples to an already existing waveform on the device memory. The flags is_first_chunk and is_last_chunk can be used as indicator if a new waveform should be created or if the write process to a waveform should be terminated. NOTE: All sample arrays in analog_samples and digital_samples must be of equal length! @param str name: the name of the waveform to be created/append to @param dict analog_samples: keys are the generic analog channel names (i.e. 'a_ch1') and values are 1D numpy arrays of type float32 containing the voltage samples. @param dict digital_samples: keys are the generic digital channel names (i.e. 'd_ch1') and values are 1D numpy arrays of type bool containing the marker states. @param bool is_first_chunk: Flag indicating if it is the first chunk to write. If True this method will create a new empty wavveform. If False the samples are appended to the existing waveform. @param bool is_last_chunk: Flag indicating if it is the last chunk to write. Some devices may need to know when to close the appending wfm. @param int total_number_of_samples: The number of sample points for the entire waveform (not only the currently written chunk) @return (int, list): Number of samples written (-1 indicates failed process) and list of created waveform names """ if self.__current_status != 0: self.log.error('FPGA is not idle, so the waveform can`t be written at this time.') return -1, list() if analog_samples: self.log.error('FPGA pulse generator is purely digital and does not support waveform ' 'generation with analog samples.') return -1, list() if not digital_samples: if total_number_of_samples > 0: self.log.warning('No samples handed over for waveform generation.') return -1, list() else: self.__current_waveform = bytearray(np.zeros(32)) self.__samples_written = 32 self.__current_waveform_name = '' return 0, list() # Initialize waveform array if this is the first chunk to write # Also append zero-timebins to waveform if the length is no integer multiple of 32 if is_first_chunk: self.__samples_written = 0 self.__current_waveform_name = name if total_number_of_samples % 32 != 0: number_of_zeros = 32 - (total_number_of_samples % 32) self.__current_waveform = np.zeros(total_number_of_samples + number_of_zeros, dtype='uint8') self.log.warning('FPGA pulse sequence length is no integer multiple of 32 samples.' '\nAppending {0:d} zero-samples to the sequence.' ''.format(number_of_zeros)) else: self.__current_waveform = np.zeros(total_number_of_samples, dtype='uint8') # Determine which part of the waveform array should be written chunk_length = len(digital_samples[list(digital_samples)[0]]) write_end_index = self.__samples_written + chunk_length # Encode samples for each channel in bit mask and create waveform array for chnl, samples in digital_samples.items(): # get channel index in range 0..7 chnl_ind = int(chnl.rsplit('ch', 1)[1]) - 1 # Represent bool values as np.uint8 uint8_samples = samples.view('uint8') # left shift 0/1 values to bit position corresponding to channel index np.left_shift(uint8_samples, chnl_ind, out=uint8_samples) # Add samples to waveform array np.add(self.__current_waveform[self.__samples_written:write_end_index], uint8_samples, out=self.__current_waveform[self.__samples_written:write_end_index]) # Convert numpy array to bytearray self.__current_waveform = bytearray(self.__current_waveform.tobytes()) # increment the current write index self.__samples_written += chunk_length return chunk_length, [self.__current_waveform_name] def write_sequence(self, name, sequence_parameters): """ Write a new sequence on the device memory. @param str name: the name of the waveform to be created/append to @param dict sequence_parameters: dictionary containing the parameters for a sequence @return: int, number of sequence steps written (-1 indicates failed process) """ self.log.warning('FPGA digital pulse generator has no sequencing capabilities.\n' 'write_sequence call ignored.') return -1 def get_waveform_names(self): """ Retrieve the names of all uploaded waveforms on the device. @return list: List of all uploaded waveform name strings in the device workspace. """ waveform_names = list() if self.__current_waveform_name != '' and self.__current_waveform_name is not None: waveform_names = [self.__current_waveform_name] return waveform_names def get_sequence_names(self): """ Retrieve the names of all uploaded sequence on the device. @return list: List of all uploaded sequence name strings in the device workspace. """ return list() def delete_waveform(self, waveform_name): """ Delete the waveform with name "waveform_name" from the device memory. @param str waveform_name: The name of the waveform to be deleted Optionally a list of waveform names can be passed. @return list: a list of deleted waveform names. """ return list() def delete_sequence(self, sequence_name): """ Delete the sequence with name "sequence_name" from the device memory. @param str sequence_name: The name of the sequence to be deleted Optionally a list of sequence names can be passed. @return list: a list of deleted sequence names. """ return list() def get_interleave(self): """ Check whether Interleave is ON or OFF in AWG. @return bool: True: ON, False: OFF Will always return False for pulse generator hardware without interleave. """ return False def set_interleave(self, state=False): """ Turns the interleave of an AWG on or off. @param bool state: The state the interleave should be set to (True: ON, False: OFF) @return bool: actual interleave status (True: ON, False: OFF) Note: After setting the interleave of the device, retrieve the interleave again and use that information for further processing. Unused for pulse generator hardware other than an AWG. """ if state: self.log.error('No interleave functionality available in FPGA pulser.\n' 'Interleave state is always False.') return False def write(self, command): """ Sends a command string to the device. @param str command: string containing the command @return int: error code (0:OK, -1:error) """ if not isinstance(command, int): return -1 self.fpga.SetWireInValue(0x00, command) self.fpga.UpdateWireIns() return 0 def query(self, question=None): """ Asks the device a 'question' and receive and return an answer from it. @param str question: string containing the command @return string: the answer of the device to the 'question' in a string """ self.fpga.UpdateWireOuts() return self.fpga.GetWireOutValue(0x20) def reset(self): """ Reset the device. @return int: error code (0:OK, -1:error) """ self.write(0x04) self.write(0x00) return 0 def _connect_fpga(self): # connect to FPGA by serial number self.fpga.OpenBySerial(self._fpga_serial) # upload configuration bitfile to FPGA self.set_sample_rate(self.__sample_rate) # Check connection if not self.fpga.IsFrontPanelEnabled(): self.current_status = -1 self.log.error('ERROR: FrontPanel is not enabled in FPGA pulse generator!') self.__current_status = -1 return self.__current_status else: self.current_status = 0 self.log.info('FPGA pulse generator connected') return self.__current_status def _disconnect_fpga(self): """ stop FPGA and disconnect """ # set FPGA in reset state self.write(0x04) self.__current_status = -1 del self.fpga return self.__current_status
class MotorRotationZaber(Base, MotorInterface): """unstable: Christoph Müller, Simon Schmitt This is the Interface class to define the controls for the simple microwave hardware. Example config for copy-paste: motorstage_zaber: module.Class: 'motor.zaber_motor_rotation_stage.MotorRotationZaber' com_port_zaber: 'ASRL1::INSTR' zaber_baud_rate: 9600 zaber_timeout: 1000 zaber_term_char: '\n' zaber_axis_label: 'phi' zaber_angle_min: -1e5 # in degrees zaber_angle_max: 1e5 # in degrees zaber_angle_step: 1e-5 # in degrees zaber_velocity_min: 1e-3 # in degrees/s zaber_velocity_max: 10 # in degrees/s zaber_velocity_step: -1e-3 # in degrees/s zaber_micro_step_size: 234.375e-6 zaber_speed_conversion: 9.375 """ _modclass = 'MotorRotation' _modtype = 'hardware' _com_port_rot = ConfigOption('com_port_zaber', 'ASRL1::INSTR', missing='warn') _rot_baud_rate = ConfigOption('zaber_baud_rate', 9600, missing='warn') _rot_timeout = ConfigOption('zaber_timeout', 5000, missing='warn') #TIMEOUT shorter? _rot_term_char = ConfigOption('zaber_term_char', '\n', missing='warn') _axis_label = ConfigOption('zaber_axis_label', 'phi', missing='warn') _min_angle = ConfigOption('zaber_angle_min', -1e5, missing='warn') _max_angle = ConfigOption('zaber_angle_max', 1e5, missing='warn') _min_step = ConfigOption('zaber_angle_step', 1e-5, missing='warn') _min_vel = ConfigOption('zaber_velocity_min', 1e-3, missing='warn') _max_vel = ConfigOption('zaber_velocity_max', 10, missing='warn') _step_vel = ConfigOption('zaber_velocity_step', 1e-3, missing='warn') _micro_step_size = ConfigOption('zaber_micro_step_size', 234.375e-6, missing='warn') velocity_conversion = ConfigOption('zaber_speed_conversion', 9.375, missing='warn') def __init__(self, **kwargs): super().__init__(**kwargs) def on_activate(self): """ Initialisation performed during activation of the module. """ self._serial_connection_rot = serial.Serial( port=self._com_port_rot, baudrate=self._rot_baud_rate, bytesize=8, parity='N', stopbits=1, timeout=self._rot_timeout) return 0 def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. """ self._serial_connection_rot.close() return 0 def get_constraints(self): """ Retrieve the hardware constrains from the motor device. @return dict: dict with constraints for the sequence generation and GUI Provides all the constraints for the xyz stage and rot stage (like total movement, velocity, ...) Each constraint is a tuple of the form (min_value, max_value, stepsize) """ constraints = OrderedDict() rot = {} rot['label'] = self._axis_label rot['ID'] = None rot['unit'] = '°' rot['ramp'] = None rot['pos_min'] = self._min_angle rot['pos_max'] = self._max_angle rot['pos_step'] = self._min_step rot['vel_min'] = self._min_vel rot['vel_max'] = self._max_vel rot['vel_step'] = self._step_vel rot['acc_min'] = None rot['acc_max'] = None rot['acc_step'] = None # assign the parameter container to a name which will identify it constraints[rot['label']] = rot return constraints def move_rel(self, param_dict): """Moves stage by a given angle (relative movement) @param dict param_dict: Dictionary with axis name and relative movement in deg @return dict velocity: Dictionary with axis name and final position in deg """ pos={} try: for axis_label in param_dict: angle = param_dict[axis_label] if abs(angle) >= self._micro_step_size: data = int(angle / self._micro_step_size) self._write_rot([1,21,data]) pos[axis_label] = self._read_answer_rot() * self._micro_step_size # stage sends signal after motion finished else: self.log.warning('Desired step "{0}" is too small. Minimum is "{1}"' .format(angle, self._micro_step_size)) pos = self.get_pos(param_dict.keys()) except: self.log.error('relative movement of zaber rotation stage is not possible') pos = self.get_pos(param_dict.keys()) return pos def move_abs(self, param_dict): """Moves stage to an absolute angle (absolute movement) @param dict param_dict: Dictionary with axis name and target position in deg @return dict velocity: Dictionary with axis name and final position in deg """ pos = {} try: for axis_label in param_dict: angle = param_dict[axis_label] data = int(self._map_angle(angle) / self._micro_step_size) self._write_rot([1,20,data]) pos[axis_label] = self._read_answer_rot() * self._micro_step_size # stage sends signal after motion finished except: self.log.error('absolute movement of zaber rotation stage is not possible') pos = self.get_pos(param_dict.keys()) return pos def abort(self): """Stops movement of the stage @return int: error code (0:OK, -1:error) """ try: self._write_rot([1, 23, 0]) while not self._motor_stopped(): time.sleep(0.2) return 0 except: self.log.error('ROTATIONAL MOVEMENT NOT STOPPED!!!)') return -1 def get_pos(self,param_list=None): """ Gets current position of the rotation stage @param list param_list: List with axis name @return dict pos: Dictionary with axis name and pos in deg """ constraints = self.get_constraints() try: pos = {} if param_list is not None: for axis_label in param_list: answer = self._ask_rot([1, 60, 0]) time.sleep(0.2) pos[axis_label] = answer * self._micro_step_size return pos else: for axis_label in constraints: answer = self._ask_rot([1, 60, 0]) time.sleep(0.2) pos[axis_label] = answer * self._micro_step_size return pos except: self.log.error('Cannot find position of zaber-rotation-stage') return -1 def get_status(self,param_list=None): """ Get the status of the position @param list param_list: optional, if a specific status of an axis is desired, then the labels of the needed axis should be passed in the param_list. If nothing is passed, then from each axis the status is asked. @return dict status: · 0 - idle, not currently executing any instructions · 1 - executing a home instruction · 10 - executing a manual move (i.e. the manual control knob is turned) · 20 - executing a move absolute instruction · 21 - executing a move relative instruction · 22 - executing a move at constant speed instruction · 23 - executing a stop instruction (i.e. decelerating) """ constraints = self.get_constraints() status = {} try: if param_list is not None: for axis_label in param_list: status[axis_label] = self._ask_rot([1, 54, 0]) time.sleep(0.1) return status else: for axis_label in constraints: status[axis_label] = self._ask_rot([1, 54, 0]) time.sleep(0.1) return status except: self.log.error('Could not get status') return -1 def calibrate(self, param_list=None): """ Calibrates the rotation motor @param list param_list: Dictionary with axis name @return dict pos: Dictionary with axis name and pos in deg """ constraints = self.get_constraints() pos = {} try: if param_list is not None: for axis_label in param_list: self._write_rot([1, 1, 0]) pos[axis_label] = self._read_answer_rot() * self._micro_step_size # stage sends signal after motion finished else: for axis_label in constraints: self._write_rot([1, 1, 0]) pos[axis_label] = self._read_answer_rot() * self._micro_step_size # stage sends signal after motion finished except: self.log.error('Could not calibrate zaber rotation stage!') pos = self.get_pos() return pos def get_velocity(self, param_list=None): """ Asks current value for velocity. @param list param_list: Dictionary with axis name @return dict velocity: Dictionary with axis name and velocity in deg/s """ constraints = self.get_constraints() velocity = {} try: if param_list is not None: for axis_label in param_list: data = self._ask_rot([1, 53, 42]) velocity[axis_label] = data*self.velocity_conversion*self._micro_step_size else: for axis_label in constraints: data = self._ask_rot([1, 53, 42]) velocity[axis_label] = data*self.velocity_conversion*self._micro_step_size return velocity except: self.log.error('Could not set rotational velocity') return -1 def set_velocity(self, param_dict): """ Write new value for velocity. @param dict param_dict: Dictionary with axis name and target velocity in deg/s @return dict velocity: Dictionary with axis name and target velocity in deg/s """ velocity = {} try: for axis_label in param_dict: speed = param_dict[axis_label] if speed <= self._max_vel: speed = int(speed/self.velocity_conversion/self._micro_step_size) self._write_rot([1,42, speed]) velocity[axis_label] = self._read_answer_rot()*self.velocity_conversion*self._micro_step_size # stage sends signal after motion finished else: self.log.warning('Desired velocity "{0}" is too high. Maximum is "{1}"' .format(velocity,self._max_vel)) velocity = self.get_velocity() except: self.log.error('Could not set rotational velocity') velocity = self.get_velocity() return velocity ########################## internal methods ################################## def _write_rot(self, list): ''' sending a command encode in a list to the rotation stage, requires [1, commandnumber, value] @param list list: command in a list form @return errorcode''' try: xx = list[0] yy = list[1] zz = list[2] z4 = 0 z3 = 0 z2 = 0 z1 = 0 base = 256 if zz >= 0: if zz/base**3 >= 1: z4 = int(zz/base**3) #since int(8.9999)=8 ! zz -= z4*base**3 if zz/base**2 >= 1: z3 = int(zz/base**2) zz -= z3*base**2 if zz/base >= 1: z2 = int(zz/base) zz -= z2*base z1 = zz else: z4 = 255 zz += base**3 if zz/base**2 >= 1: z3 =int(zz/base**2) zz -= z3*base**2 if zz/base >= 1: z2 = int(zz/base) zz -= z2*base z1 = zz sends = [xx,yy,z1,z2,z3,z4] for ii in range (6): self._serial_connection_rot.write(chr(sends[ii]).encode('latin')) return 0 except: self.log.error('Command was not sent to zaber rotation stage') return -1 def _read_answer_rot(self): '''this method reads the answer from the motor! return 6 bytes from the receive buffer there must be 6 bytes to receive (no error checking) @return answer float: answer of motor coded in a single float ''' r = [0, 0, 0, 0, 0, 0] for i in range(6): r[i] = ord(self._serial_connection_rot.read(1)) yy = r[1] z1 = r[2] z2 = r[3] z3 = r[4] z4 = r[5] answer = z1 + z2 * 256 + z3 * 256 ** 2 + z4 * 256 ** 3 if yy == 255: #yy is command number and 255 implies error self.log.error('error nr. ' + str(answer)) return answer def _ask_rot(self,list): '''this method combines writing a command and reading the answer @param list list: list encoded command @return answer float: answer of motor coded in a single float ''' self._write_rot(list) time.sleep(0.1) answer=self._read_answer_rot() return answer def _motor_stopped(self): '''checks if the rotation stage is still moving @return: bool stopped: True if motor is not moving, False otherwise''' stopped=True status = self.get_status() if status: stopped=False return stopped def _map_angle(self, init_angle): '''maps the angle if larger or lower than 360° to inbetween 0° and 360° @params init_angle: initial angle, possible not element of {0°,360°} @return: float angle: Angle between 0° and 360°''' angle = init_angle%360 return angle
class NIPulser(Base, PulserInterface): """ Pulse generator using NI-DAQmx """ _modtype = 'PulserInterface' _modclass = 'hardware' self.device = ConfigOption('device', 'Dev0', missing='warn') def on_activate(self): """ Activate module """ config = self.getConfiguration() if 'pulsed_file_dir' in config.keys(): self.pulsed_file_dir = config['pulsed_file_dir'] if not os.path.exists(self.pulsed_file_dir): homedir = get_home_dir() self.pulsed_file_dir = os.path.join(homedir, 'pulsed_files') self.log.warning( 'The directory defined in parameter "pulsed_file_dir" in the config for ' 'SequenceGeneratorLogic class does not exist!\nThe default home directory\n' '{0}\n will be taken instead.'.format( self.pulsed_file_dir)) else: homedir = get_home_dir() self.pulsed_file_dir = os.path.join(homedir, 'pulsed_files') self.log.warning( 'No parameter "pulsed_file_dir" was specified in the config for NIPulser ' 'as directory for the pulsed files!\nThe default home directory\n{0}\n' 'will be taken instead.'.format(self.pulsed_file_dir)) self.host_waveform_directory = self._get_dir_for_name( 'sampled_hardware_files') self.pulser_task = daq.TaskHandle() daq.DAQmxCreateTask('NI Pulser', daq.byref(self.pulser_task)) self.current_status = -1 self.current_loaded_asset = None self.init_constraints() # analog voltage self.min_volts = -10 self.max_volts = 10 self.sample_rate = 1000 self.a_names = [] self.d_names = [] self.set_active_channels({ k: True for k in self.constraints['activation_config']['analog_only'] }) #self.sample_rate = self.get_sample_rate() def on_deactivate(self): """ Deactivate module """ self.close_pulser_task() def init_constraints(self): """ Build a pulser constraints dictionary with information from the NI card. """ device = self.device constraints = {} ch_map = OrderedDict() n = 2048 ao_max_freq = daq.float64() ao_min_freq = daq.float64() ao_physical_chans = ctypes.create_string_buffer(n) ao_voltage_ranges = np.zeros(16, dtype=np.float64) ao_clock_support = daq.bool32() do_max_freq = daq.float64() do_lines = ctypes.create_string_buffer(n) do_ports = ctypes.create_string_buffer(n) product_dev_type = ctypes.create_string_buffer(n) product_cat = daq.int32() serial_num = daq.uInt32() product_num = daq.uInt32() daq.DAQmxGetDevAOMinRate(device, daq.byref(ao_min_freq)) self.log.debug('Analog min freq: {0}'.format(ao_min_freq.value)) daq.DAQmxGetDevAOMaxRate(device, daq.byref(ao_max_freq)) self.log.debug('Analog max freq: {0}'.format(ao_max_freq.value)) daq.DAQmxGetDevAOSampClkSupported(device, daq.byref(ao_clock_support)) self.log.debug('Analog supports clock: {0}'.format( ao_clock_support.value)) daq.DAQmxGetDevAOPhysicalChans(device, ao_physical_chans, n) analog_channels = str(ao_physical_chans.value, encoding='utf-8').split(', ') self.log.debug('Analog channels: {0}'.format(analog_channels)) daq.DAQmxGetDevAOVoltageRngs( device, ao_voltage_ranges.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), len(ao_voltage_ranges)) self.log.debug('Analog voltage range: {0}'.format( ao_voltage_ranges[0:2])) daq.DAQmxGetDevDOMaxRate(self.device, daq.byref(do_max_freq)) self.log.debug('Digital max freq: {0}'.format(do_max_freq.value)) daq.DAQmxGetDevDOLines(device, do_lines, n) digital_channels = str(do_lines.value, encoding='utf-8').split(', ') self.log.debug('Digital channels: {0}'.format(digital_channels)) daq.DAQmxGetDevDOPorts(device, do_ports, n) digital_bundles = str(do_ports.value, encoding='utf-8').split(', ') self.log.debug('Digital ports: {0}'.format(digital_bundles)) daq.DAQmxGetDevSerialNum(device, daq.byref(serial_num)) self.log.debug('Card serial number: {0}'.format(serial_num.value)) daq.DAQmxGetDevProductNum(device, daq.byref(product_num)) self.log.debug('Product number: {0}'.format(product_num.value)) daq.DAQmxGetDevProductType(device, product_dev_type, n) product = str(product_dev_type.value, encoding='utf-8') self.log.debug('Product name: {0}'.format(product)) daq.DAQmxGetDevProductCategory(device, daq.byref(product_cat)) self.log.debug(product_cat.value) for n, ch in enumerate(analog_channels): ch_map['a_ch{0:d}'.format(n + 1)] = ch for n, ch in enumerate(digital_channels): ch_map['d_ch{0:d}'.format(n + 1)] = ch constraints['sample_rate'] = { 'min': ao_min_freq.value, 'max': ao_max_freq.value, 'step': 0.0, 'unit': 'Samples/s' } # The file formats are hardware specific. The sequence_generator_logic will need this # information to choose the proper output format for waveform and sequence files. constraints['waveform_format'] = 'ndarray' constraints['sequence_format'] = None # the stepsize will be determined by the DAC in combination with the # maximal output amplitude (in Vpp): constraints['a_ch_amplitude'] = { 'min': 0, 'max': ao_voltage_ranges[1], 'step': 0.0, 'unit': 'Vpp' } constraints['a_ch_offset'] = { 'min': ao_voltage_ranges[0], 'max': ao_voltage_ranges[1], 'step': 0.0, 'unit': 'V' } constraints['d_ch_low'] = { 'min': 0.0, 'max': 0.0, 'step': 0.0, 'unit': 'V' } constraints['d_ch_high'] = { 'min': 5.0, 'max': 5.0, 'step': 0.0, 'unit': 'V' } constraints['sampled_file_length'] = { 'min': 2, 'max': 1e12, 'step': 0, 'unit': 'Samples' } constraints['digital_bin_num'] = { 'min': 2, 'max': 1e12, 'step': 0, 'unit': '#' } constraints['waveform_num'] = { 'min': 1, 'max': 1, 'step': 0, 'unit': '#' } constraints['sequence_num'] = { 'min': 0, 'max': 0, 'step': 0, 'unit': '#' } constraints['subsequence_num'] = { 'min': 0, 'max': 0, 'step': 0, 'unit': '#' } # If sequencer mode is enable than sequence_param should be not just an # empty dictionary. sequence_param = OrderedDict() constraints['sequence_param'] = sequence_param activation_config = OrderedDict() activation_config['analog_only'] = [ k for k in ch_map.keys() if k.startswith('a') ] activation_config['digital_only'] = [ k for k in ch_map.keys() if k.startswith('d') ] activation_config['stuff'] = [ 'a_ch4', 'd_ch1', 'd_ch2', 'd_ch3', 'd_ch4' ] constraints['activation_config'] = activation_config self.channel_map = ch_map self.constraints = constraints def configure_pulser_task(self): """ Clear pulser task and set to current settings. @return: """ a_channels = [self.channel_map[k] for k in self.a_names] d_channels = [self.channel_map[k] for k in self.d_names] # clear task daq.DAQmxClearTask(self.pulser_task) # add channels if len(a_channels) > 0: print(self.a_names, a_channels) daq.DAQmxCreateAOVoltageChan(self.pulser_task, ', '.join(a_channels), ', '.join(self.a_names), self.min_volts, self.max_volts, daq.DAQmx_Val_Volts, '') if len(d_channels) > 0: print(self.d_names, d_channels) daq.DAQmxCreateDOChan(self.pulser_task, ', '.join(d_channels), ', '.join(self.d_names), daq.DAQmx_Val_ChanForAllLines) # set sampling frequency daq.DAQmxCfgSampClkTiming(self.pulser_task, 'OnboardClock', self.sample_rate, daq.DAQmx_Val_Rising, daq.DAQmx_Val_ContSamps, 10 * self.sample_rate) # write assets def close_pulser_task(self): """ Clear tasks. @return int: error code (0:OK, -1:error) """ retval = 0 try: # stop the task daq.DAQmxStopTask(self.pulser_task) except: self.log.exception('Error while closing NI pulser.') retval = -1 try: # clear the task daq.DAQmxClearTask(self.pulser_task) except: self.log.exception('Error while clearing NI pulser.') retval = -1 return retval def get_constraints(self): """ Retrieve the hardware constrains from the Pulsing device. @return dict: dict with constraints for the sequence generation and GUI """ return self.constraints def pulser_on(self): """ Switches the pulsing device on. @return int: error code (0:OK, -1:error) """ try: daq.DAQmxStartTask(self.pulser_task) except: self.log.exception('Error starting NI pulser.') return -1 return 0 def pulser_off(self): """ Switches the pulsing device off. @return int: error code (0:OK, -1:error) """ try: daq.DAQmxStopTask(self.pulser_task) except: self.log.exception('Error stopping NI pulser.') return -1 return 0 def upload_asset(self, asset_name=None): """ Upload an already hardware conform file to the device mass memory. Also loads these files into the device workspace if present. Does NOT load waveforms/sequences/patterns into channels. @param asset_name: string, name of the ensemble/sequence to be uploaded @return int: error code (0:OK, -1:error) If nothing is passed, method will be skipped. This method has no effect when using pulser hardware without own mass memory (i.e. PulseBlaster, FPGA) """ self.log.debug( 'NI pulser has no own storage capability.\n"upload_asset" call ignored.' ) return 0 def load_asset(self, asset_name, load_dict=None): """ Loads a sequence or waveform to the specified channel of the pulsing device. For devices that have a workspace (i.e. AWG) this will load the asset from the device workspace into the channel. For a device without mass memory this will transfer the waveform/sequence/pattern data directly to the device so that it is ready to play. @param str asset_name: The name of the asset to be loaded @param dict load_dict: a dictionary with keys being one of the available channel numbers and items being the name of the already sampled waveform/sequence files. Examples: {1: rabi_Ch1, 2: rabi_Ch2} {1: rabi_Ch2, 2: rabi_Ch1} This parameter is optional. If none is given then the channel association is invoked from the file name, i.e. the appendix (_ch1, _ch2 etc.) @return int: error code (0:OK, -1:error) """ # ignore if no asset_name is given if asset_name is None: self.log.warning('"load_asset" called with asset_name = None.') return 0 # check if asset exists saved_assets = self.get_saved_asset_names() if asset_name not in saved_assets: self.log.error('No asset with name "{0}" found for NI pulser.\n' '"load_asset" call ignored.'.format(asset_name)) return -1 # get samples from file filepath = os.path.join(self.host_waveform_directory, asset_name + '.npz') self.samples = np.load(filepath) self.current_loaded_asset = asset_name def get_loaded_asset(self): """ Retrieve the currently loaded asset name of the device. @return str: Name of the current asset ready to play. (no filename) """ return self.current_loaded_asset def clear_all(self): """ Clears all loaded waveforms from the pulse generators RAM/workspace. @return int: error code (0:OK, -1:error) """ pass def get_status(self): """ Retrieves the status of the pulsing hardware @return (int, dict): tuple with an interger value of the current status and a corresponding dictionary containing status description for all the possible status variables of the pulse generator hardware. """ status_dict = { -1: 'Failed Request or Communication', 0: 'Device has stopped, but can receive commands.', 1: 'Device is active and running.' } task_done = daq.bool32 try: daq.DAQmxIsTaskDone(self.pulser_task, daq.byref(task_done)) current_status = 0 if task_done.value else 1 except: self.log.exception('Error while getting pulser state.') current_status = -1 return current_status, status_dict def get_sample_rate(self): """ Get the sample rate of the pulse generator hardware @return float: The current sample rate of the device (in Hz) Do not return a saved sample rate from an attribute, but instead retrieve the current sample rate directly from the device. """ rate = daq.float64() daq.DAQmxGetSampClkRate(self.pulser_task, daq.byref(rate)) return rate.value def set_sample_rate(self, sample_rate): """ Set the sample rate of the pulse generator hardware. @param float sample_rate: The sampling rate to be set (in Hz) @return float: the sample rate returned from the device (in Hz). Note: After setting the sampling rate of the device, use the actually set return value for further processing. """ task = self.pulser_task source = 'OnboardClock' rate = sample_rate edge = daq.DAQmx_Val_Rising mode = daq.DAQmx_Val_ContSamps samples = 10000 daq.DAQmxCfgSampClkTiming(task, source, rate, edge, mode, samples) self.sample_rate = self.get_sample_rate() return self.sample_rate def get_analog_level(self, amplitude=None, offset=None): """ Retrieve the analog amplitude and offset of the provided channels. @param list amplitude: optional, if the amplitude value (in Volt peak to peak, i.e. the full amplitude) of a specific channel is desired. @param list offset: optional, if the offset value (in Volt) of a specific channel is desired. @return: (dict, dict): tuple of two dicts, with keys being the channel descriptor string (i.e. 'a_ch1') and items being the values for those channels. Amplitude is always denoted in Volt-peak-to-peak and Offset in volts. Note: Do not return a saved amplitude and/or offset value but instead retrieve the current amplitude and/or offset directly from the device. If nothing (or None) is passed then the levels of all channels will be returned. If no analog channels are present in the device, return just empty dicts. Example of a possible input: amplitude = ['a_ch1', 'a_ch4'], offset = None to obtain the amplitude of channel 1 and 4 and the offset of all channels {'a_ch1': -0.5, 'a_ch4': 2.0} {'a_ch1': 0.0, 'a_ch2': 0.0, 'a_ch3': 1.0, 'a_ch4': 0.0} The major difference to digital signals is that analog signals are always oscillating or changing signals, otherwise you can use just digital output. In contrast to digital output levels, analog output levels are defined by an amplitude (here total signal span, denoted in Voltage peak to peak) and an offset (a value around which the signal oscillates, denoted by an (absolute) voltage). In general there is no bijective correspondence between (amplitude, offset) and (value high, value low)! """ amp_dict = {} off_dict = {} return amp_dict, off_dict def set_analog_level(self, amplitude=None, offset=None): """ Set amplitude and/or offset value of the provided analog channel(s). @param dict amplitude: dictionary, with key being the channel descriptor string (i.e. 'a_ch1', 'a_ch2') and items being the amplitude values (in Volt peak to peak, i.e. the full amplitude) for the desired channel. @param dict offset: dictionary, with key being the channel descriptor string (i.e. 'a_ch1', 'a_ch2') and items being the offset values (in absolute volt) for the desired channel. @return (dict, dict): tuple of two dicts with the actual set values for amplitude and offset for ALL channels. If nothing is passed then the command will return the current amplitudes/offsets. Note: After setting the amplitude and/or offset values of the device, use the actual set return values for further processing. The major difference to digital signals is that analog signals are always oscillating or changing signals, otherwise you can use just digital output. In contrast to digital output levels, analog output levels are defined by an amplitude (here total signal span, denoted in Voltage peak to peak) and an offset (a value around which the signal oscillates, denoted by an (absolute) voltage). In general there is no bijective correspondence between (amplitude, offset) and (value high, value low)! """ return self.get_analog_level(amplitude, offset) def get_digital_level(self, low=None, high=None): """ Retrieve the digital low and high level of the provided/all channels. @param list low: optional, if the low value (in Volt) of a specific channel is desired. @param list high: optional, if the high value (in Volt) of a specific channel is desired. @return: (dict, dict): tuple of two dicts, with keys being the channel descriptor strings (i.e. 'd_ch1', 'd_ch2') and items being the values for those channels. Both low and high value of a channel is denoted in volts. Note: Do not return a saved low and/or high value but instead retrieve the current low and/or high value directly from the device. If nothing (or None) is passed then the levels of all channels are being returned. If no digital channels are present, return just an empty dict. Example of a possible input: low = ['d_ch1', 'd_ch4'] to obtain the low voltage values of digital channel 1 an 4. A possible answer might be {'d_ch1': -0.5, 'd_ch4': 2.0} {'d_ch1': 1.0, 'd_ch2': 1.0, 'd_ch3': 1.0, 'd_ch4': 4.0} Since no high request was performed, the high values for ALL channels are returned (here 4). The major difference to analog signals is that digital signals are either ON or OFF, whereas analog channels have a varying amplitude range. In contrast to analog output levels, digital output levels are defined by a voltage, which corresponds to the ON status and a voltage which corresponds to the OFF status (both denoted in (absolute) voltage) In general there is no bijective correspondence between (amplitude, offset) and (value high, value low)! """ # all digital levels are 5V or whatever the hardware provides and is not changeable channels = self.get_active_channels() if low is None: low_dict = {ch: 0 for ch, v in channels.items() if v} else: low_dict = {ch: 0 for ch in low} if high is None: high_dict = {ch: 5 for ch, v in channels.items() if v} else: high_dict = {ch: 5 for ch in high} return low_dict, high_dict def set_digital_level(self, low=None, high=None): """ Set low and/or high value of the provided digital channel. @param dict low: dictionary, with key being the channel descriptor string (i.e. 'd_ch1', 'd_ch2') and items being the low values (in volt) for the desired channel. @param dict high: dictionary, with key being the channel descriptor string (i.e. 'd_ch1', 'd_ch2') and items being the high values (in volt) for the desired channel. @return (dict, dict): tuple of two dicts where first dict denotes the current low value and the second dict the high value for ALL digital channels. Keys are the channel descriptor strings (i.e. 'd_ch1', 'd_ch2') If nothing is passed then the command will return the current voltage levels. Note: After setting the high and/or low values of the device, use the actual set return values for further processing. The major difference to analog signals is that digital signals are either ON or OFF, whereas analog channels have a varying amplitude range. In contrast to analog output levels, digital output levels are defined by a voltage, which corresponds to the ON status and a voltage which corresponds to the OFF status (both denoted in (absolute) voltage) In general there is no bijective correspondence between (amplitude, offset) and (value high, value low)! """ # digital levels not settable on NI card return self.get_digital_level(low, high) def get_active_channels(self, ch=None): """ Get the active channels of the pulse generator hardware. @param list ch: optional, if specific analog or digital channels are needed to be asked without obtaining all the channels. @return dict: where keys denoting the channel string and items boolean expressions whether channel are active or not. Example for an possible input (order is not important): ch = ['a_ch2', 'd_ch2', 'a_ch1', 'd_ch5', 'd_ch1'] then the output might look like {'a_ch2': True, 'd_ch2': False, 'a_ch1': False, 'd_ch5': True, 'd_ch1': False} If no parameter (or None) is passed to this method all channel states will be returned. """ buffer_size = 2048 buf = ctypes.create_string_buffer(buffer_size) daq.DAQmxGetTaskChannels(self.pulser_task, buf, buffer_size) ni_ch = str(buf.value, encoding='utf-8').split(', ') if ch is None: return {k: k in ni_ch for k, v in self.channel_map.items()} else: return {k: k in ni_ch for k in ch} def set_active_channels(self, ch=None): """ Set the active channels for the pulse generator hardware. @param dict ch: dictionary with keys being the analog or digital string generic names for the channels (i.e. 'd_ch1', 'a_ch2') with values being a boolean value. True: Activate channel, False: Deactivate channel @return dict: with the actual set values for ALL active analog and digital channels If nothing is passed then the command will simply return the unchanged current state. Note: After setting the active channels of the device, use the returned dict for further processing. Example for possible input: ch={'a_ch2': True, 'd_ch1': False, 'd_ch3': True, 'd_ch4': True} to activate analog channel 2 digital channel 3 and 4 and to deactivate digital channel 1. The hardware itself has to handle, whether separate channel activation is possible. """ self.a_names = [k for k, v in ch.items() if k.startswith('a') and v] self.a_names.sort() self.d_names = [k for k, v in ch.items() if k.startswith('d') and v] self.d_names.sort() # apply changed channels self.configure_pulser_task() return self.get_active_channels() def get_uploaded_asset_names(self): """ Retrieve the names of all uploaded assets on the device. @return list: List of all uploaded asset name strings in the current device directory. This is no list of the file names. Unused for pulse generators without sequence storage capability (PulseBlaster, FPGA). """ # no storage return [] def get_saved_asset_names(self): """ Retrieve the names of all sampled and saved assets on the host PC. This is no list of the file names. @return list: List of all saved asset name strings in the current directory of the host PC. """ file_list = self._get_filenames_on_host() saved_assets = [] for filename in file_list: if filename.endswith('.npz'): asset_name = filename.rsplit('.', 1)[0] if asset_name not in saved_assets: saved_assets.append(asset_name) return saved_assets def delete_asset(self, asset_name): """ Delete all files associated with an asset with the passed asset_name from the device memory (mass storage as well as i.e. awg workspace/channels). @param str asset_name: The name of the asset to be deleted Optionally a list of asset names can be passed. @return list: a list with strings of the files which were deleted. Unused for pulse generators without sequence storage capability (PulseBlaster, FPGA). """ # no storage return 0 def set_asset_dir_on_device(self, dir_path): """ Change the directory where the assets are stored on the device. @param str dir_path: The target directory @return int: error code (0:OK, -1:error) Unused for pulse generators without changeable file structure (PulseBlaster, FPGA). """ # no storage return 0 def get_asset_dir_on_device(self): """ Ask for the directory where the hardware conform files are stored on the device. @return str: The current file directory Unused for pulse generators without changeable file structure (i.e. PulseBlaster, FPGA). """ # no storage return '' def get_interleave(self): """ Check whether Interleave is ON or OFF in AWG. @return bool: True: ON, False: OFF Will always return False for pulse generator hardware without interleave. """ return False def set_interleave(self, state=False): """ Turns the interleave of an AWG on or off. @param bool state: The state the interleave should be set to (True: ON, False: OFF) @return bool: actual interleave status (True: ON, False: OFF) Note: After setting the interleave of the device, retrieve the interleave again and use that information for further processing. Unused for pulse generator hardware other than an AWG. """ return False def tell(self, command): """ Sends a command string to the device. @param string command: string containing the command @return int: error code (0:OK, -1:error) """ return 0 def ask(self, question): """ Asks the device a 'question' and receive and return an answer from it. @param string question: string containing the command @return string: the answer of the device to the 'question' in a string """ return '' def reset(self): """ Reset the device. @return int: error code (0:OK, -1:error) """ try: daq.DAQmxResetDevice(self.device) except: self.log.exception('Could not reset NI device {0}'.format( self.device)) return -1 return 0 def has_sequence_mode(self): """ Asks the pulse generator whether sequence mode exists. @return: bool, True for yes, False for no. """ return False def _get_dir_for_name(self, name): """ Get the path to the pulsed sub-directory 'name'. @param name: string, name of the folder @return: string, absolute path to the directory with folder 'name'. """ path = os.path.join(self.pulsed_file_dir, name) if not os.path.exists(path): os.makedirs(os.path.abspath(path)) return os.path.abspath(path) def _get_filenames_on_host(self): """ Get the full filenames of all assets saved on the host PC. @return: list, The full filenames of all assets saved on the host PC. """ filename_list = [ f for f in os.listdir(self.host_waveform_directory) if f.endswith('.npz') ] return filename_list
class SaveLogic(GenericLogic): """ A general class which saves all kinds of data in a general sense. """ _modclass = 'savelogic' _modtype = 'logic' _win_data_dir = ConfigOption('win_data_directory', 'C:/Data/') _unix_data_dir = ConfigOption('unix_data_directory', 'Data') log_into_daily_directory = ConfigOption('log_into_daily_directory', False, missing='warn') # Matplotlib style definition for saving plots mpl_qd_style = { 'axes.prop_cycle': cycler('color', [ '#1f17f4', '#ffa40e', '#ff3487', '#008b00', '#17becf', '#850085' ]) + cycler('marker', ['o', 's', '^', 'v', 'D', 'd']), 'axes.edgecolor': '0.3', 'xtick.color': '0.3', 'ytick.color': '0.3', 'axes.labelcolor': 'black', 'font.size': '14', 'lines.linewidth': '2', 'figure.figsize': '12, 6', 'lines.markeredgewidth': '0', 'lines.markersize': '5', 'axes.spines.right': True, 'axes.spines.top': True, 'xtick.minor.visible': True, 'ytick.minor.visible': True, 'savefig.dpi': '180' } def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) # locking for thread safety self.lock = Mutex() # name of active POI, default to empty string self.active_poi_name = '' # Some default variables concerning the operating system: self.os_system = None # Chech which operation system is used and include a case if the # directory was not found in the config: if sys.platform in ('linux', 'darwin'): self.os_system = 'unix' self.data_dir = self._unix_data_dir elif 'win32' in sys.platform or 'AMD64' in sys.platform: self.os_system = 'win' self.data_dir = self._win_data_dir else: raise Exception('Identify the operating system.') # start logging into daily directory? if not isinstance(self.log_into_daily_directory, bool): self.log.warning( 'log entry in configuration is not a ' 'boolean. Falling back to default setting: False.') self.log_into_daily_directory = False self._daily_loghandler = None def on_activate(self): """ Definition, configuration and initialisation of the SaveLogic. """ if self.log_into_daily_directory: # adds a log handler for logging into daily directory self._daily_loghandler = DailyLogHandler( '%Y%m%d-%Hh%Mm%Ss-qudi.log', self) self._daily_loghandler.setFormatter( logging.Formatter( '%(asctime)s %(name)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S')) self._daily_loghandler.setLevel(logging.DEBUG) logging.getLogger().addHandler(self._daily_loghandler) else: self._daily_loghandler = None def on_deactivate(self): if self._daily_loghandler is not None: # removes the log handler logging into the daily directory logging.getLogger().removeHandler(self._daily_loghandler) @property def dailylog(self): """ Returns the daily log handler. """ return self._daily_loghandler def dailylog_set_level(self, level): """ Sets the log level of the daily log handler @param level int: log level, see logging """ self._daily_loghandler.setLevel(level) def save_data(self, data, filepath=None, parameters=None, filename=None, filelabel=None, timestamp=None, filetype='text', fmt='%.15e', delimiter='\t', plotfig=None): """ General save routine for data. @param dictionary data: Dictionary containing the data to be saved. The keys should be strings containing the data header/description. The corresponding items are one or more 1D arrays or one 2D array containing the data (list or numpy.ndarray). Example: data = {'Frequency (MHz)': [1,2,4,5,6]} data = {'Frequency': [1, 2, 4], 'Counts': [234, 894, 743, 423]} data = {'Frequency (MHz),Counts':[[1,234], [2,894],...[30,504]]} @param string filepath: optional, the path to the directory, where the data will be saved. If the specified path does not exist yet, the saving routine will try to create it. If no path is passed (default filepath=None) the saving routine will create a directory by the name of the calling module inside the daily data directory. If no calling module can be inferred and/or the requested path can not be created the data will be saved in a subfolder of the daily data directory called UNSPECIFIED @param dictionary parameters: optional, a dictionary with all parameters you want to save in the header of the created file. @parem string filename: optional, if you really want to fix your own filename. If passed, the whole file will have the name <filename> If nothing is specified the save logic will generate a filename either based on the module name from which this method was called, or it will use the passed filelabel if that is speficied. You also need to specify the ending of the filename! @parem string filelabel: optional, if filelabel is set and no filename was specified, the savelogic will create a name which looks like YYYY-MM-DD_HHh-MMm-SSs_<filelabel>.dat The timestamp will be created at runtime if no user defined timestamp was passed. @param datetime timestamp: optional, a datetime.datetime object. You can create this object with datetime.datetime.now() in the calling module if you want to fix the timestamp for the filename. Be careful when passing a filename and a timestamp, because then the timestamp will be ignored. @param string filetype: optional, the file format the data should be saved in. Valid inputs are 'text', 'xml' and 'npz'. Default is 'text'. @param string or list of strings fmt: optional, format specifier for saved data. See python documentation for "Format Specification Mini-Language". If you want for example save a float in scientific notation with 6 decimals this would look like '%.6e'. For saving integers you could use '%d', '%s' for strings. The default is '%.15e' for numbers and '%s' for str. If len(data) > 1 you should pass a list of format specifiers; one for each item in the data dict. If only one specifier is passed but the data arrays have different data types this can lead to strange behaviour or failure to save right away. @param string delimiter: optional, insert here the delimiter, like '\n' for new line, '\t' for tab, ',' for a comma ect. 1D data ======= 1D data should be passed in a dictionary where the data trace should be assigned to one identifier like {'<identifier>':[list of values]} {'Numbers of counts':[1.4, 4.2, 5, 2.0, 5.9 , ... , 9.5, 6.4]} You can also pass as much 1D arrays as you want: {'Frequency (MHz)':list1, 'signal':list2, 'correlations': list3, ...} 2D data ======= 2D data should be passed in a dictionary where the matrix like data should be assigned to one identifier like {'<identifier>':[[1,2,3],[4,5,6],[7,8,9]]} which will result in: <identifier> 1 2 3 4 5 6 7 8 9 YOU ARE RESPONSIBLE FOR THE IDENTIFIER! DO NOT FORGET THE UNITS FOR THE SAVED TIME TRACE/MATRIX. """ start_time = time.time() # Create timestamp if none is present if timestamp is None: timestamp = datetime.datetime.now() # Try to cast data array into numpy.ndarray if it is not already one # Also collect information on arrays in the process and do sanity checks found_1d = False found_2d = False multiple_dtypes = False arr_length = [] arr_dtype = [] max_row_num = 0 max_line_num = 0 for keyname in data: # Cast into numpy array if not isinstance(data[keyname], np.ndarray): try: data[keyname] = np.array(data[keyname]) except: self.log.error( 'Casting data array of type "{0}" into numpy.ndarray failed. ' 'Could not save data.'.format(type(data[keyname]))) return -1 # determine dimensions if data[keyname].ndim < 3: length = data[keyname].shape[0] arr_length.append(length) if length > max_line_num: max_line_num = length if data[keyname].ndim == 2: found_2d = True width = data[keyname].shape[1] if max_row_num < width: max_row_num = width else: found_1d = True max_row_num += 1 else: self.log.error( 'Found data array with dimension >2. Unable to save data.') return -1 # determine array data types if len(arr_dtype) > 0: if arr_dtype[-1] != data[keyname].dtype: multiple_dtypes = True arr_dtype.append(data[keyname].dtype) # Raise error if data contains a mixture of 1D and 2D arrays if found_2d and found_1d: self.log.error( 'Passed data dictionary contains 1D AND 2D arrays. This is not allowed. ' 'Either fit all data arrays into a single 2D array or pass multiple 1D ' 'arrays only. Saving data failed!') return -1 # try to trace back the functioncall to the class which was calling it. try: frm = inspect.stack()[1] # this will get the object, which called the save_data function. mod = inspect.getmodule(frm[0]) # that will extract the name of the class. module_name = mod.__name__.split('.')[-1] except: # Sometimes it is not possible to get the object which called the save_data function # (such as when calling this from the console). module_name = 'UNSPECIFIED' # determine proper file path if filepath is None: filepath = self.get_path_for_module(module_name) elif not os.path.exists(filepath): os.makedirs(filepath) self.log.info( 'Custom filepath does not exist. Created directory "{0}"' ''.format(filepath)) # create filelabel if none has been passed if filelabel is None: filelabel = module_name if self.active_poi_name != '': filelabel = self.active_poi_name.replace(' ', '_') + '_' + filelabel # determine proper unique filename to save if none has been passed if filename is None: filename = timestamp.strftime('%Y%m%d-%H%M-%S' + '_' + filelabel + '.dat') # Check format specifier. if not isinstance(fmt, str) and len(fmt) != len(data): self.log.error( 'Length of list of format specifiers and number of data items differs. ' 'Saving not possible. Please pass exactly as many format specifiers as ' 'data arrays.') return -1 # Create header string for the file header = 'Saved Data from the class {0} on {1}.\n' \ ''.format(module_name, timestamp.strftime('%d.%m.%Y at %Hh%Mm%Ss')) header += '\nParameters:\n===========\n\n' # Include the active POI name (if not empty) as a parameter in the header if self.active_poi_name != '': header += 'Measured at POI: {0}\n'.format(self.active_poi_name) # add the parameters if specified: if parameters is not None: # check whether the format for the parameters have a dict type: if isinstance(parameters, dict): for entry, param in parameters.items(): if isinstance(param, float): header += '{0}: {1:.16e}\n'.format(entry, param) else: header += '{0}: {1}\n'.format(entry, param) # make a hardcore string conversion and try to save the parameters directly: else: self.log.error( 'The parameters are not passed as a dictionary! The SaveLogic will ' 'try to save the parameters nevertheless.') header += 'not specified parameters: {0}\n'.format(parameters) header += '\nData:\n=====\n' # write data to file # FIXME: Implement other file formats # write to textfile if filetype == 'text': # Reshape data if multiple 1D arrays have been passed to this method. # If a 2D array has been passed, reformat the specifier if len(data) != 1: identifier_str = '' if multiple_dtypes: field_dtypes = list( zip([ 'f{0:d}'.format(i) for i in range(len(arr_dtype)) ], arr_dtype)) new_array = np.empty(max_line_num, dtype=field_dtypes) for i, keyname in enumerate(data): identifier_str += keyname + delimiter field = 'f{0:d}'.format(i) length = data[keyname].size new_array[field][:length] = data[keyname] if length < max_line_num: if isinstance(data[keyname][0], str): new_array[field][length:] = 'nan' else: new_array[field][length:] = np.nan else: new_array = np.empty([max_line_num, max_row_num], arr_dtype[0]) for i, keyname in enumerate(data): identifier_str += keyname + delimiter length = data[keyname].size new_array[:length, i] = data[keyname] if length < max_line_num: if isinstance(data[keyname][0], str): new_array[length:, i] = 'nan' else: new_array[length:, i] = np.nan # discard old data array and use new one data = {identifier_str: new_array} elif found_2d: keyname = list(data.keys())[0] identifier_str = keyname.replace(', ', delimiter).replace( ',', delimiter) data[identifier_str] = data.pop(keyname) else: identifier_str = list(data)[0] header += list(data)[0] self.save_array_as_text(data=data[identifier_str], filename=filename, filepath=filepath, fmt=fmt, header=header, delimiter=delimiter, comments='#', append=False) # write npz file and save parameters in textfile elif filetype == 'npz': header += str(list(data.keys()))[1:-1] np.savez_compressed(filepath + '/' + filename[:-4], **data) self.save_array_as_text(data=[], filename=filename[:-4] + '_params.dat', filepath=filepath, fmt=fmt, header=header, delimiter=delimiter, comments='#', append=False) else: self.log.error( 'Only saving of data as textfile and npz-file is implemented. Filetype "{0}" is not ' 'supported yet. Saving as textfile.'.format(filetype)) self.save_array_as_text(data=data[identifier_str], filename=filename, filepath=filepath, fmt=fmt, header=header, delimiter=delimiter, comments='#', append=False) #-------------------------------------------------------------------------------------------- # Save thumbnail figure of plot if plotfig is not None: # create Metadata metadata = dict() metadata['Title'] = 'Image produced by qudi: ' + module_name metadata['Author'] = 'qudi - Software Suite' metadata[ 'Subject'] = 'Find more information on: https://github.com/Ulm-IQO/qudi' metadata[ 'Keywords'] = 'Python 3, Qt, experiment control, automation, measurement, software, framework, modular' metadata['Producer'] = 'qudi - Software Suite' if timestamp is not None: metadata['CreationDate'] = timestamp metadata['ModDate'] = timestamp else: metadata['CreationDate'] = time metadata['ModDate'] = time # determine the PDF-Filename fig_fname_vector = os.path.join(filepath, filename)[:-4] + '_fig.pdf' # Create the PdfPages object to which we will save the pages: # The with statement makes sure that the PdfPages object is closed properly at # the end of the block, even if an Exception occurs. with PdfPages(fig_fname_vector) as pdf: pdf.savefig(plotfig, bbox_inches='tight', pad_inches=0.05) # We can also set the file's metadata via the PdfPages object: pdf_metadata = pdf.infodict() for x in metadata: pdf_metadata[x] = metadata[x] # determine the PNG-Filename and save the plain PNG fig_fname_image = os.path.join(filepath, filename)[:-4] + '_fig.png' plotfig.savefig(fig_fname_image, bbox_inches='tight', pad_inches=0.05) # Use Pillow (an fork for PIL) to attach metadata to the PNG png_image = Image.open(fig_fname_image) png_metadata = PngImagePlugin.PngInfo() # PIL can only handle Strings, so let's convert our times metadata['CreationDate'] = metadata['CreationDate'].strftime( '%Y%m%d-%H%M-%S') metadata['ModDate'] = metadata['ModDate'].strftime( '%Y%m%d-%H%M-%S') for x in metadata: # make sure every value of the metadata is a string if not isinstance(metadata[x], str): metadata[x] = str(metadata[x]) # add the metadata to the picture png_metadata.add_text(x, metadata[x]) # save the picture again, this time including the metadata png_image.save(fig_fname_image, "png", pnginfo=png_metadata) # close matplotlib figure plt.close(plotfig) self.log.debug( 'Time needed to save data: {0:.2f}s'.format(time.time() - start_time)) #---------------------------------------------------------------------------------- def save_array_as_text(self, data, filename, filepath='', fmt='%.15e', header='', delimiter='\t', comments='#', append=False): """ An Independent method, which can save a 1D or 2D numpy.ndarray as textfile. Can append to files. """ # write to file. Append if requested. if append: with open(os.path.join(filepath, filename), 'ab') as file: np.savetxt(file, data, fmt=fmt, delimiter=delimiter, header=header, comments=comments) else: with open(os.path.join(filepath, filename), 'wb') as file: np.savetxt(file, data, fmt=fmt, delimiter=delimiter, header=header, comments=comments) return def get_daily_directory(self): """ Creates the daily directory. @return string: path to the daily directory. If the daily directory does not exits in the specified <root_dir> path in the config file, then it is created according to the following scheme: <root_dir>\<year>\<month>\<yearmonthday> and the filepath is returned. There should be always a filepath returned. """ # First check if the directory exists and if not then the default # directory is taken. if not os.path.exists(self.data_dir): # Check if the default directory does exist. If yes, there is # no need to create it, since it will overwrite the existing # data there. if not os.path.exists(self.data_dir): os.makedirs(self.data_dir) self.log.warning( 'The specified Data Directory in the ' 'config file does not exist. Using default for ' '{0} system instead. The directory {1} was ' 'created'.format(self.os_system, self.data_dir)) # That is now the current directory: current_dir = os.path.join(self.data_dir, time.strftime("%Y"), time.strftime("%m")) folder_exists = False # Flag to indicate that the folder does not exist. if os.path.exists(current_dir): # Get only the folders without the files there: folderlist = [ d for d in os.listdir(current_dir) if os.path.isdir(os.path.join(current_dir, d)) ] # Search if there is a folder which starts with the current date: for entry in folderlist: if (time.strftime("%Y%m%d") in (entry[:2])): current_dir = os.path.join(current_dir, str(entry)) folder_exists = True break if not folder_exists: current_dir = os.path.join(current_dir, time.strftime("%Y%m%d")) self.log.info('Creating directory for today\'s data in \n' '{0}'.format(current_dir)) # The exist_ok=True is necessary here to prevent Error 17 "File Exists" # Details at http://stackoverflow.com/questions/12468022/python-fileexists-error-when-making-directory os.makedirs(current_dir, exist_ok=True) return current_dir def get_path_for_module(self, module_name): """ Method that creates a path for 'module_name' where data are stored. @param string module_name: Specify the folder, which should be created in the daily directory. The module_name can be e.g. 'Confocal'. @return string: absolute path to the module name """ dir_path = os.path.join(self.get_daily_directory(), module_name) if not os.path.exists(dir_path): os.makedirs(dir_path) return dir_path
class MicrowaveSmbv(Base, MicrowaveInterface): """ Hardware file to control a R&S SMBV100A microwave device. Example config for copy-paste: mw_source_smbv: module.Class: 'microwave.mw_source_smbv.MicrowaveSmbv' gpib_address: 'GPIB0::12::INSTR' gpib_address: 'GPIB0::12::INSTR' gpib_timeout: 10 """ _modclass = 'MicrowaveSmbv' _modtype = 'hardware' # visa address of the hardware : this can be over ethernet, the name is here for # backward compatibility _address = ConfigOption('gpib_address', missing='error') _timeout = ConfigOption('gpib_timeout', 10, missing='warn') # to limit the power to a lower value that the hardware can provide _max_power = ConfigOption('max_power', None) # Indicate how fast frequencies within a list or sweep mode can be changed: _FREQ_SWITCH_SPEED = 0.003 # Frequency switching speed in s (acc. to specs) def on_activate(self): """ Initialisation performed during activation of the module. """ self._timeout = self._timeout * 1000 # trying to load the visa connection to the module self.rm = visa.ResourceManager() try: self._connection = self.rm.open_resource(self._address, timeout=self._timeout) except: self.log.error('Could not connect to the address >>{}<<.'.format( self._address)) raise self.model = self._connection.query('*IDN?').split(',')[1] self.log.info('MW {} initialised and connected.'.format(self.model)) self._command_wait('*CLS') self._command_wait('*RST') return def on_deactivate(self): """ Cleanup performed during deactivation of the module. """ self.rm.close() return def _command_wait(self, command_str): """ Writes the command in command_str via ressource manager and waits until the device has finished processing it. @param command_str: The command to be written """ self._connection.write(command_str) self._connection.write('*WAI') while int(float(self._connection.query('*OPC?'))) != 1: time.sleep(0.2) return def get_limits(self): """ Create an object containing parameter limits for this microwave source. @return MicrowaveLimits: device-specific parameter limits """ limits = MicrowaveLimits() limits.supported_modes = (MicrowaveMode.CW, MicrowaveMode.SWEEP) # values for SMBV100A limits.min_power = -145 limits.max_power = 30 limits.min_frequency = 9e3 limits.max_frequency = 6e9 if self.model == 'SMB100A': limits.max_frequency = 3.2e9 limits.list_minstep = 0.1 limits.list_maxstep = limits.max_frequency - limits.min_frequency limits.list_maxentries = 1 limits.sweep_minstep = 0.1 limits.sweep_maxstep = limits.max_frequency - limits.min_frequency limits.sweep_maxentries = 10001 # in case a lower maximum is set in config file if self._max_power is not None and self._max_power < limits.max_power: limits.max_power = self._max_power return limits def off(self): """ Switches off any microwave output. Must return AFTER the device is actually stopped. @return int: error code (0:OK, -1:error) """ mode, is_running = self.get_status() if not is_running: return 0 self._connection.write('OUTP:STAT OFF') self._connection.write('*WAI') while int(float(self._connection.query('OUTP:STAT?'))) != 0: time.sleep(0.2) return 0 def get_status(self): """ Gets the current status of the MW source, i.e. the mode (cw, list or sweep) and the output state (stopped, running) @return str, bool: mode ['cw', 'list', 'sweep'], is_running [True, False] """ is_running = bool(int(float(self._connection.query('OUTP:STAT?')))) mode = self._connection.query(':FREQ:MODE?').strip('\n').lower() if mode == 'swe': mode = 'sweep' return mode, is_running def get_power(self): """ Gets the microwave output power. @return float: the power set at the device in dBm """ # This case works for cw AND sweep mode return float(self._connection.query(':POW?')) def get_frequency(self): """ Gets the frequency of the microwave output. Returns single float value if the device is in cw mode. Returns list like [start, stop, step] if the device is in sweep mode. Returns list of frequencies if the device is in list mode. @return [float, list]: frequency(s) currently set for this device in Hz """ mode, is_running = self.get_status() if 'cw' in mode: return_val = float(self._connection.query(':FREQ?')) elif 'sweep' in mode: start = float(self._connection.query(':FREQ:STAR?')) stop = float(self._connection.query(':FREQ:STOP?')) step = float(self._connection.query(':SWE:STEP?')) return_val = [start + step, stop, step] return return_val def cw_on(self): """ Switches on cw microwave output. Must return AFTER the device is actually running. @return int: error code (0:OK, -1:error) """ current_mode, is_running = self.get_status() if is_running: if current_mode == 'cw': return 0 else: self.off() if current_mode != 'cw': self._command_wait(':FREQ:MODE CW') self._connection.write(':OUTP:STAT ON') self._connection.write('*WAI') dummy, is_running = self.get_status() while not is_running: time.sleep(0.2) dummy, is_running = self.get_status() return 0 def set_cw(self, frequency=None, power=None): """ Configures the device for cw-mode and optionally sets frequency and/or power @param float frequency: frequency to set in Hz @param float power: power to set in dBm @return tuple(float, float, str): with the relation current frequency in Hz, current power in dBm, current mode """ mode, is_running = self.get_status() if is_running: self.off() # Activate CW mode if mode != 'cw': self._command_wait(':FREQ:MODE CW') # Set CW frequency if frequency is not None: self._command_wait(':FREQ {0:f}'.format(frequency)) # Set CW power if power is not None: self._command_wait(':POW {0:f}'.format(power)) # Return actually set values mode, dummy = self.get_status() actual_freq = self.get_frequency() actual_power = self.get_power() return actual_freq, actual_power, mode def list_on(self): """ Switches on the list mode microwave output. Must return AFTER the device is actually running. @return int: error code (0:OK, -1:error) """ self.log.error('List mode not available for this microwave hardware!') return -1 def set_list(self, frequency=None, power=None): """ Configures the device for list-mode and optionally sets frequencies and/or power @param list frequency: list of frequencies in Hz @param float power: MW power of the frequency list in dBm @return tuple(list, float, str): current frequencies in Hz, current power in dBm, current mode """ self.log.error('List mode not available for this microwave hardware!') mode, dummy = self.get_status() return self.get_frequency(), self.get_power(), mode def reset_listpos(self): """ Reset of MW list mode position to start (first frequency step) @return int: error code (0:OK, -1:error) """ self.log.error('List mode not available for this microwave hardware!') return -1 def sweep_on(self): """ Switches on the sweep mode. @return int: error code (0:OK, -1:error) """ current_mode, is_running = self.get_status() if is_running: if current_mode == 'sweep': return 0 else: self.off() if current_mode != 'sweep': self._command_wait(':FREQ:MODE SWEEP') self._connection.write(':OUTP:STAT ON') dummy, is_running = self.get_status() while not is_running: time.sleep(0.2) dummy, is_running = self.get_status() return 0 def set_sweep(self, start=None, stop=None, step=None, power=None): """ Configures the device for sweep-mode and optionally sets frequency start/stop/step and/or power @return float, float, float, float, str: current start frequency in Hz, current stop frequency in Hz, current frequency step in Hz, current power in dBm, current mode """ mode, is_running = self.get_status() if is_running: self.off() if mode != 'sweep': self._command_wait(':FREQ:MODE SWEEP') if (start is not None) and (stop is not None) and (step is not None): self._connection.write(':SWE:MODE STEP') self._connection.write(':SWE:SPAC LIN') self._connection.write('*WAI') self._connection.write(':FREQ:START {0:f}'.format(start - step)) self._connection.write(':FREQ:STOP {0:f}'.format(stop)) self._connection.write(':SWE:STEP:LIN {0:f}'.format(step)) self._connection.write('*WAI') if power is not None: self._connection.write(':POW {0:f}'.format(power)) self._connection.write('*WAI') self._command_wait('TRIG:FSW:SOUR EXT') actual_power = self.get_power() freq_list = self.get_frequency() mode, dummy = self.get_status() return freq_list[0], freq_list[1], freq_list[2], actual_power, mode def reset_sweeppos(self): """ Reset of MW sweep mode position to start (start frequency) @return int: error code (0:OK, -1:error) """ self._command_wait(':ABOR:SWE') return 0 def set_ext_trigger(self, pol, timing): """ Set the external trigger for this device with proper polarization. @param TriggerEdge pol: polarisation of the trigger (basically rising edge or falling edge) @param float timing: estimated time between triggers @return object, float: current trigger polarity [TriggerEdge.RISING, TriggerEdge.FALLING], trigger timing """ mode, is_running = self.get_status() if is_running: self.off() if pol == TriggerEdge.RISING: edge = 'POS' elif pol == TriggerEdge.FALLING: edge = 'NEG' else: self.log.warning( 'No valid trigger polarity passed to microwave hardware module.' ) edge = None if edge is not None: self._command_wait(':TRIG1:SLOP {0}'.format(edge)) polarity = self._connection.query(':TRIG1:SLOP?') if 'NEG' in polarity: return TriggerEdge.FALLING, timing else: return TriggerEdge.RISING, timing def trigger(self): """ Trigger the next element in the list or sweep mode programmatically. @return int: error code (0:OK, -1:error) Ensure that the Frequency was set AFTER the function returns, or give the function at least a save waiting time. """ # WARNING: # The manual trigger functionality was not tested for this device! # Might not work well! Please check that! self._connection.write('*TRG') time.sleep(self._FREQ_SWITCH_SPEED) # that is the switching speed return 0
class MicrowaveAgilent(Base, MicrowaveInterface): """ This is the Interface class to define the controls for the simple microwave hardware. The hardware file was tested using the model N9310A. """ _modclass = 'MicrowaveAgilent' _modtype = 'hardware' _usb_address = ConfigOption('usb_address', missing='error') _usb_timeout = ConfigOption('usb_timeout', 100, missing='warn') def on_activate(self): """ Initialisation performed during activation of the module. """ try: self._usb_timeout = self._usb_timeout # trying to load the visa connection to the module self.rm = visa.ResourceManager() self._usb_connection = self.rm.open_resource( resource_name=self._usb_address, timeout=self._usb_timeout) self.log.info('MWAGILENT initialised and connected to hardware.') self.model = self._usb_connection.query('*IDN?').split(',')[1] self._FREQ_SWITCH_SPEED = 0.09 # Frequency switching speed in s (acc. to specs) #set trigger of Sweep and Point to be FALLING self.set_ext_trigger() except: self.log.error('This is MWagilent: could not connect to the GPIB ' 'address >>{}<<.'.format(self._usb_address)) def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. """ self._usb_connection.close() self.rm.close() return def off(self): """ Switches off any microwave output. @return int: error code (0:OK, -1:error) """ # turn of sweeping (both "list" or ”sweep“) self._usb_connection.write(':SWEep:RF:STATe OFF') while int(float(self._usb_connection.query(':SWEep:RF:STATe?'))) != 0: time.sleep(0.2) # check if running mode, is_running = self.get_status() if not is_running: return 0 self._usb_connection.write(':RFO:STAT OFF') while int(float(self._usb_connection.query(':RFO:STAT?'))) != 0: time.sleep(0.2) #self._mode ="cw" return 0 def get_status(self): """ Gets the current status of the MW source, i.e. the mode (cw, list or sweep) and the output state (stopped, running) @return str, bool: mode ['cw', 'list', 'sweep'], is_running [True, False] """ is_running = bool( int(float(self._usb_connection.ask(":RFOutput:STATe?")))) if bool(int(float(self._usb_connection.ask(":SWEep:RF:STATe?")))): if self._usb_connection.ask(":SWEep:TYPE?") == "STEP": mode = "sweep" else: mode = "list" else: mode = "cw" return mode, is_running def get_power(self): """ Gets the microwave output power. @return float: the power set at the device in dBm """ mode, is_running = self.get_status() if mode == 'list': #add the moment all powers in the list file should be the same self._usb_connection.write(':LIST:ROW:GOTO {0:e}'.format(1)) return float(self._usb_connection.ask(':LIST:Amplitude?')) else: return float(self._usb_connection.query(':AMPL:CW?')) def get_frequency(self): """ Gets the frequency of the microwave output. @return float: frequency (in Hz), which is currently set for this device """ mode, is_running = self.get_status() if 'cw' in mode: return_val = float(self._usb_connection.query(':FREQ:CW?')) elif 'sweep' in mode: start = float(self._usb_connection.ask(':SWE:RF:STAR?')) stop = float(self._usb_connection.ask(':SWE:RF:STOP?')) num_of_points = int(self._usb_connection.ask(':SWE:STEP:POIN?')) freq_range = stop - start step = freq_range / (num_of_points - 1) return_val = [start, stop, step] elif 'list' in mode: # get the number of rows and initalize output arraz current_rows = int(self._usb_connection.ask(':LIST:RF:POINts?')) return_val = np.zeros((current_rows, 1)) for ii in range(current_rows): # go to respective row self._usb_connection.write(':LIST:ROW:GOTO {0:e}'.format(ii + 1)) return_val[ii] = float(self._command_wait(':LIST:RF?')) return return_val def cw_on(self): """ Switches on any preconfigured microwave output. @return int: error code (0:OK, -1:error) """ current_mode, is_running = self.get_status() if is_running: if current_mode == 'cw': return 0 else: self.off() self._usb_connection.write(':RFO:STAT ON') while not is_running: time.sleep(0.2) dummy, is_running = self.get_status() return 0 def set_cw(self, freq=None, power=None, useinterleave=None): """ Sets the MW mode to cw and additionally frequency and power #For agilent device there is no CW mode, so just do nothing @param float freq: frequency to set in Hz @param float power: power to set in dBm @param bool useinterleave: If this mode exists you can choose it. @return int: error code (0:OK, -1:error) Interleave option is used for arbitrary waveform generator devices. """ mode, is_running = self.get_status() if is_running: self.off() if freq is not None: self.set_frequency(freq) if power is not None: self.set_power(power) if useinterleave is not None: self.log.warning("No interleave available at the moment!") mode, is_running = self.get_status() actual_freq = self.get_frequency() actual_power = self.get_power() return actual_freq, actual_power, mode def list_on(self): """ Switches on the list mode. @return int: error code (1: ready, 0:not ready, -1:error) """ current_mode, is_running = self.get_status() if is_running: if current_mode == 'list': return 0 else: self.off() try: self._usb_connection.write(":SWEep:TYPE LIST") self._usb_connection.write(':SWE:RF:STAT ON') while int(float( self._usb_connection.query(':SWEep:RF:STATe?'))) != 1: time.sleep(0.2) self._usb_connection.write(':RFO:STAT ON') dummy, is_running = self.get_status() while not is_running: time.sleep(0.2) dummy, is_running = self.get_status() return 0 except: self.log.warning("Turning on of List mode does not work") return -1 def set_list(self, freq=None, power=None): """ There is no list mode for agilent # Also the list is created by giving 'start_freq, step, stop_freq' @param list freq: list of frequencies in Hz @param float power: MW power of the frequency list in dBm """ mode, is_running = self.get_status() if is_running: self.off() if freq is not None: num_of_freq = len(freq) current_rows = int(self._usb_connection.ask(':LIST:RF:POINts?')) # adapt the length of the list while current_rows != num_of_freq: if current_rows > num_of_freq: for kk in range(int(current_rows - num_of_freq)): #always delete the second row (first might not work) self._usb_connection.write( ':LIST:ROW:DELete {0:e}'.format(2)) time.sleep(0.05) elif current_rows < num_of_freq: for kk in range(int(num_of_freq - current_rows)): self._usb_connection.write( ':LIST:ROW:INsert {0:e}'.format(2)) time.sleep(0.05) current_rows = int( self._usb_connection.ask(':LIST:RF:POINts?')) self.log.info("adjusting list length again") for ii in range(current_rows): self._usb_connection.write(':LIST:ROW:GOTO {0:e}'.format(ii + 1)) time.sleep(0.1) self._usb_connection.write(':LIST:RF {0:e} Hz'.format( freq[ii])) time.sleep(0.25) if power is not None: self._usb_connection.write( ':LIST:Amplitude {0:e} dBm'.format(power)) # seems to need some time time.sleep(0.25) else: if power is not None: current_rows = int( self._usb_connection.ask(':LIST:RF:POINts?')) for ii in range(current_rows): self._usb_connection.write( ':LIST:ROW:GOTO {0:e}'.format(ii + 1)) self._usb_connection.write( ':LIST:Amplitude {0:e} dBm'.format(power)) # seems to need some time time.sleep(0.5) else: self.log.warning("Not changing freq or power!") self._usb_connection.write(':SWE:REP CONT') self._usb_connection.write(':SWE:STRG EXT') #self._usb_connection.write(':SWE:STRG:SLOP EXTP') self._usb_connection.write(':SWE:PTRG EXT') self._usb_connection.write(':SWE:PTRG:SLOP EXTP') self._usb_connection.write(':SWE:DIR:UP') self.set_ext_trigger() # self._usb_connection.write(':RFO:STAT ON') # self._usb_connection.write(':SWE:RF:STAT ON') actual_power = self.get_power() # dont take actual frequencz arraz at the moment since this is far too slow #actual_freq = self.get_frequency() actual_freq = freq mode, dummy = self.get_status() return actual_freq, actual_power, mode def reset_listpos(self): """ Reset of MW List Mode position to start from first given frequency @return int: error code (0:OK, -1:error) """ try: self._usb_connection.write(':RFO:STAT OFF') self._usb_connection.write(':SWEep:RF:STATe OFF') self._usb_connection.write(':LIST:ROW:GOTO 1') self._usb_connection.write(':SWEep:RF:STATe ON') self._usb_connection.write(':RFO:STAT ON') return 0 except: self.log.error("Reset of list position did not work") return -1 def sweep_on(self): """ Switches on the list mode. @return int: error code (0:OK, -1:error) """ mode, is_running = self.get_status() if is_running: if mode == 'sweep': return 0 else: self.off() try: self._usb_connection.write(":SWEep:TYPE STEP") self._usb_connection.write(':SWE:RF:STAT ON') while int(float( self._usb_connection.query(':SWEep:RF:STATe?'))) != 1: time.sleep(0.5) self._usb_connection.write(':RFO:STAT ON') dummy, is_running = self.get_status() while not is_running: time.sleep(0.5) dummy, is_running = self.get_status() #self._usb_connection.write('*WAI') return 0 except: self.log.error("Turning on of sweep mode did not work!") return -1 def set_sweep(self, start, stop, step, power): """ @param start: @param stop: @param step: @param power: @return: """ #self._usb_connection.write(':SOUR:POW ' + str(power)) #self._usb_connection.write('*WAI') mode, is_running = self.get_status() if is_running: self.off() n = int(stop - start) / step + 1 self._usb_connection.write(':SWE:RF:STAR {0:e} Hz'.format(start)) self._usb_connection.write(':SWE:RF:STOP {0:e} Hz'.format(stop)) self._usb_connection.write(':SWE:STEP:POIN {0}'.format(n)) #self._usb_connection.write(':SWE:STEP:DWEL 10 ms') self.set_power(power) self._usb_connection.write(':SWE:REP CONT') self._usb_connection.write(':SWE:STRG EXT') # self._usb_connection.write(':SWE:STRG:SLOP EXTP') self._usb_connection.write(':SWE:PTRG EXT') # self._usb_connection.write(':SWE:PTRG:SLOP EXTP') #self._usb_connection.write(':SWE:DIR:UP') #self._usb_connection.write('*WAI') self.set_ext_trigger() # short waiting time to prevent crashes time.sleep(0.2) freq_start = float(self._usb_connection.ask(':SWE:RF:STAR?')) freq_stop = float(self._usb_connection.ask(':SWE:RF:STOP?')) num_of_points = int(self._usb_connection.ask(':SWE:STEP:POIN?')) freq_range = freq_stop - freq_start freq_step = freq_range / (num_of_points - 1) freq_power = self.get_power() mode = 'sweep' return freq_start, freq_stop, freq_step, freq_power, mode def _turn_off_output(self, repetitions=10): self._usb_connection.write(':RFO:STAT OFF') dummy, is_running = self.get_status() index = 0 while is_running and index < repetitions: time.sleep(0.5) dummy, is_running = self.get_status() index = +1 index = 0 self._usb_connection.write(':SWE:RF:STAT OFF') while int(float(self._usb_connection.query( ':SWEep:RF:STATe?'))) != 0 and index < repetitions: time.sleep(0.5) index = +1 def _turn_on_output(self, repetitions=10): self._usb_connection.write(':SWE:RF:STAT ON') index = 0 while int(float(self._usb_connection.query( ':SWEep:RF:STATe?'))) != 1 and index < repetitions: time.sleep(0.5) index = +1 self._usb_connection.write(':RFO:STAT ON') dummy, is_running = self.get_status() index = 0 while not is_running and index < repetitions: time.sleep(0.5) dummy, is_running = self.get_status() index = +1 def reset_sweeppos(self): """ Reset of MW List Mode position to start from first given frequency @return int: error code (0:OK, -1:error) """ # turn off the sweepmode and the rf output and turn it on again # unfortunately sleep times seem to be neccessary time.sleep(0.5) self._turn_off_output() time.sleep(0.2) self._turn_on_output() return 0 def set_ext_trigger(self, pol=TriggerEdge.FALLING): """ Set the external trigger for this device with proper polarization. @param str source: channel name, where external trigger is expected. @param str pol: polarisation of the trigger (basically rising edge or falling edge) @return int: error code (0:OK, -1:error) """ if pol == TriggerEdge.RISING: edge = 'EXTP' elif pol == TriggerEdge.FALLING: edge = 'EXTN' else: return -1 try: self._usb_connection.write(':SWE:PTRG:SLOP {0}'.format(edge)) time.sleep(0.5) self._usb_connection.write(':SWE:STRG:SLOP {0}'.format(edge)) except: self.log.error("Setting of trigger did not work!") return -1 return 0 def trigger(self): """ Trigger the next element in the list or sweep mode programmatically. @return int: error code (0:OK, -1:error) """ start_freq = self.get_frequency() self._usb_connection.write(':TRIGger:IMMediate') time.sleep(self._FREQ_SWITCH_SPEED) curr_freq = self.get_frequency() if start_freq == curr_freq: self.log.error( 'Internal trigger for Agilent MW source did not work!') return -1 return 0 def get_limits(self): limits = MicrowaveLimits() limits.supported_modes = (MicrowaveMode.CW, MicrowaveMode.LIST, MicrowaveMode.SWEEP) limits.min_frequency = 9.0e3 limits.max_frequency = 3.0e9 limits.min_power = -144 limits.max_power = 10 limits.list_minstep = 0.1 limits.list_maxstep = 3.0e9 limits.list_maxentries = 4000 limits.sweep_minstep = 0.1 limits.sweep_maxstep = 3.0e9 limits.sweep_maxentries = 10001 if self.model == 'N9310A': limits.min_frequency = 9e3 limits.max_frequency = 3.0e9 limits.min_power = -127 limits.max_power = 20 else: self.log.warning( 'Model string unknown, hardware limits may be wrong.') #limits.list_maxstep = limits.max_frequency #limits.sweep_maxstep = limits.max_frequency return limits def set_power(self, power=0.): """ Sets the microwave output power. @param float power: the power (in dBm) set for this device @return int: error code (0:OK, -1:error) """ if power is not None: self._command_wait(':AMPL:CW {0:f}'.format(power)) return 0 else: return -1 def set_frequency(self, freq=None): """ Sets the frequency of the microwave output. @param float freq: the frequency (in Hz) set for this device @return int: error code (0:OK, -1:error) """ if freq is not None: self._command_wait(':FREQ:CW {0:e} Hz'.format(freq)) return 0 else: return -1 def _command_wait(self, command_str): """ Writes the command in command_str via USB and waits until the device has finished processing it. @param command_str: The command to be written """ self._usb_connection.write(command_str) self._usb_connection.write('*WAI') while int(float(self._usb_connection.query('*OPC?'))) != 1: time.sleep(0.2) return
class CameraThorlabs(Base, CameraInterface): """ Main class of the module Example config for copy-paste: thorlabs_camera: module.Class: 'camera.thorlabs.thorlabs_DCx.CameraThorlabs' default_exposure: 0.1 default_gain: 1.0 id_camera: 0 # if more tha one camera is present """ _modtype = 'camera' _modclass = 'hardware' _default_exposure = ConfigOption('default_exposure', 0.1) _default_gain = ConfigOption('default_gain', 1.0) _id_camera = ConfigOption('id_camera', 0) # if more than one camera is present _dll = None _camera_handle = None _exposure = _default_exposure _gain = _default_gain _width = 0 _height = 0 _pos_x = 0 _pos_y = 0 _bit_depth = 0 _cam = None _acquiring = False _live = False _last_acquisition_mode = None # useful if config changes during acq _sensor_info = None _image_memory = None _image_pid = None def on_activate(self): """ Initialisation performed during activation of the module. """ # Load the dll if present self._load_dll() self._connect_camera() self._init_camera() def _check_error(self, code, message): """ Check that the code means OK and log message as error if not. Return True if OK, False otherwise. """ if code != IS_SUCCESS: self.log.error(message) return False else: return True def _check_int_range(self, value, mini, maxi ,message): """ Check that value is in the range [mini, maxi] and log message as error if not. Return True if OK. """ if value < mini or value > maxi: self.log.error('{} - Value {} must be between {} and {}'.format(message, value, mini, maxi)) return False else: return True def _load_dll(self): """ Load the dll for the camera """ try: if platform.system() == "Windows": if platform.architecture()[0] == "64bit": self._dll = ctypes.cdll.uc480_64 else: self._dll = ctypes.cdll.uc480 # for Linux elif platform.system() == "Linux": self._dll = ctypes.cdll.LoadLibrary('libueye_api.so') else: self.log.error("Can not detect operating system to load Thorlabs DLL.") except OSError: self.log.error("Can not log Thorlabs DLL.") def _connect_camera(self): """ Connect to the camera and get basic info on it """ number_of_cameras = ctypes.c_int(0) self._dll.is_GetNumberOfCameras(byref(number_of_cameras)) if number_of_cameras.value < 1: self.log.error("No Thorlabs camera detected.") elif number_of_cameras.value - 1 < self._id_camera: self.log.error("A Thorlabs camera has been detected but the id specified above the number of camera(s)") else: self._camera_handle = ctypes.c_int(0) ret = self._dll.is_InitCamera(ctypes.pointer(self._camera_handle)) self._check_error(ret, "Could not initialize camera") self._sensor_info = SENSORINFO() self._dll.is_GetSensorInfo(self._camera_handle, byref(self._sensor_info)) self.log.debug('Connected to camera : {}'.format(str(self._sensor_info.strSensorName))) def _init_camera(self): """ Set the parameters of the camera for our usage """ # Color mode code = self._dll.is_SetColorMode(self._camera_handle, ctypes.c_int(IS_SET_CM_Y8)) self._check_error(code, "Could set color mode IS_SET_CM_Y8") self._bit_depth = 8 # Image size self.set_image_size(self._sensor_info.nMaxWidth, self._sensor_info.nMaxHeight) # Image position self.set_image_position(0, 0) # Binning code = self._dll.is_SetBinning(self._camera_handle, ctypes.c_int(0)) # Disable binning self._check_error(code, "Could set binning disabled") # Sub sampling code = self._dll.is_SetSubSampling(self._camera_handle, ctypes.c_int(0)) # Disable sub sampling self._check_error(code, "Could set sub sampling disabled") # Allocate image memory self._image_pid = ctypes.c_int() self._image_memory = ctypes.c_char_p() code = self._dll.is_AllocImageMem( self._camera_handle, self._width, self._height, self._bit_depth, byref(self._image_memory), byref(self._image_pid)) self._check_error(code, "Could not allocate image memory") # Set image memory code = self._dll.is_SetImageMem(self._camera_handle, self._image_memory, self._image_pid) self._check_error(code, "Could not set image memory") # Set auto exit code = self._dll.is_EnableAutoExit(self._camera_handle, 1) # Enable auto-exit self._check_error(code, "Could not set auto exit") self.set_exposure(self._exposure) self.set_gain(self._gain) def set_image_size(self, width=None, height=None): """ Set the size of the image, here the camera will acquire only part of the image from a given position """ if width is not None: width = int(width) self._check_int_range(width, 1, self._sensor_info.nMaxWidth, 'Can not set image width') self._width = width if height is not None: height = int(height) self._check_int_range(height, 1, self._sensor_info.nMaxHeight, 'Can not set image height') self._height = height code = self._dll.is_SetImageSize(self._camera_handle, ctypes.c_int(self._width), ctypes.c_int(self._height)) return self._check_error(code, "Could not set image size") def set_image_position(self, pos_x, pos_y): """ Set image position reference coordinate """ if pos_x is not None: pos_x = int(pos_x) self._check_int_range(pos_x, 0, self._sensor_info.nMaxWidth-1, 'Can not set image position x') self._pos_x = pos_x if pos_y is not None: pos_y = int(pos_y) self._check_int_range(pos_y, 0, self._sensor_info.nMaxHeight-1, 'Can not set image position y') self._pos_y = pos_y code = self._dll.is_SetImagePos(self._camera_handle, ctypes.c_int(self._pos_x), ctypes.c_int(self._pos_y)) return self._check_error(code, "Could not set image position") def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. """ self._dll.is_ExitCamera(self._camera_handle) self._acquiring = False self._live = False def get_name(self): """ Return a name for the camera """ return self._sensor_info.strSensorName def get_size(self): """ Return the max size of the camera """ return self._width, self._height def support_live_acquisition(self): """ Return whether or not this camera support live acquisition """ return True def start_live_acquisition(self): """ Set the camera in live mode """ if self.get_ready_state(): self._acquiring = True self._live = True code = self._dll.is_CaptureVideo(self._camera_handle, c_int(IS_DONT_WAIT)) no_error = self._check_error(code, "Could not start live acquisition") if not no_error: self._acquiring = False self._live = False return False return True else: return False def start_single_acquisition(self): """ Start the acquisition of a single image """ if self.get_ready_state(): self._acquiring = True code = self._dll.is_FreezeVideo(self._camera_handle, c_int(IS_WAIT)) self._acquiring = False return self._check_error(code, "Could not start single acquisition") else: return False def stop_acquisition(self): """ Stop live acquisition """ no_error = True if self._acquiring: code = self._dll.is_StopLiveVideo(self._camera_handle, c_int(IS_FORCE_VIDEO_STOP)) no_error = self._check_error(code, "Could not stop acquisition") self._acquiring = False self._live = False return no_error def get_acquired_data(self): """ Return last acquired data from the dll """ # Allocate memory for image: img_size = self._width * self._height c_array = ctypes.c_char * img_size c_img = c_array() # copy camera memory to accessible memory code = self._dll.is_CopyImageMem(self._camera_handle, self._image_memory, self._image_pid, c_img) self._check_error(code, "Could copy image to memory") # Convert to numpy 2d array of float from 0 to 1 img_array = np.frombuffer(c_img, dtype=ctypes.c_ubyte) img_array = img_array.astype(float) img_array.shape = np.array((self._height, self._width)) return img_array def get_bit_depth(self): """ Return the bit depth of the image """ return self._bit_depth def set_exposure(self, time): """ Set the exposure in second Return the new exposure """ exp = c_double(time * 1e3) # in ms new_exp = c_double(0) code = self._dll.is_SetExposureTime(self._camera_handle, exp, byref(new_exp)) self._check_error(code, "Could not set exposure") self._exposure = float(new_exp.value)/1000 # in ms return self._exposure def get_exposure(self): """ Return current exposure """ return self._exposure def get_ready_state(self): """ Return whether or not the camera is ready for an acquisition """ if self.module_state()!='idle': return False return not self._acquiring def set_gain(self, gain): """ Set the gain """ pass def get_gain(self): """ Get the gain """ return self._gain
class MicrowaveSMR(Base, MicrowaveInterface): """ The hardware control for the device Rohde and Schwarz of type SMR. The command structure has been tested for type SMR20. Not tested on the device types SMR27, SMR30, SMR40 For additional information concerning the commands to communicate via the GPIB connection through visa, please have a look at: http://cdn.rohde-schwarz.com/pws/dl_downloads/dl_common_library/dl_manuals/gb_1/s/smr_1/smr_20-40.pdf Example config for copy-paste: mw_source_smr: module.Class: 'microwave.mw_source_smr.MicrowaveSMR' gpib_address: 'GPIB0::28::INSTR' gpib_timeout: 10 """ _modclass = 'MicrowaveSMR' _modtype = 'hardware' _gpib_address = ConfigOption('gpib_address', missing='error') _gpib_timeout = ConfigOption('gpib_timeout', 10, missing='warn') # Indicate how fast frequencies within a list or sweep mode can be changed: _FREQ_SWITCH_SPEED = 0.01 # Frequency switching speed in s (acc. to specs) def on_activate(self): """ Initialisation performed during activation of the module. """ self._LIST_DWELL = 10e-3 # Dwell time for list mode to set how long # the device should stay at one list entry. # here dwell time can be between 1ms and 1s self._SWEEP_DWELL = 10e-3 # Dwell time for sweep mode to set how long # the device should stay at one list entry. # here dwell time can be between 10ms and 5s # trying to load the visa connection to the module self.rm = visa.ResourceManager() try: # such a stupid stuff, the timeout is specified here in ms not in # seconds any more, take that into account. self._gpib_connection = self.rm.open_resource( self._gpib_address, timeout=self._gpib_timeout*1000) self._gpib_connection.write_termination = "\r\n" self._gpib_connection.read_termination = None self.log.info('MicrowaveSMR: initialised and connected to ' 'hardware.') except: self.log.error('MicrowaveSMR: could not connect to the GPIB ' 'address "{0}".'.format(self._gpib_address)) self._FREQ_MAX = float(self._ask('FREQuency? MAX')) self._FREQ_MIN = float(self._ask('FREQuency? MIN')) self._POWER_MAX = float(self._ask('POWER? MAX')) self._POWER_MIN = float(self._ask('POWER? MIN')) # although it is the step mode, this number should be the same for the # list mode: self._LIST_FREQ_STEP_MIN = float(self._ask(':SOURce:FREQuency:STEP? MIN')) self._LIST_FREQ_STEP_MAX = float(self._ask(':SOURce:FREQuency:STEP? MAX')) self._SWEEP_FREQ_STEP_MIN = float(self._ask(':SOURce:SWEep:FREQuency:STEP? MIN')) self._SWEEP_FREQ_STEP_MAX = float(self._ask(':SOURce:SWEep:FREQuency:STEP? MAX')) # the return will be a list telling how many are free and occupied, i.e. # [free, occupied] and the sum of that is the total list entries. max_list_entries = self._ask('SOUR:LIST:FREE?') self._MAX_LIST_ENTRIES = sum([int(entry) for entry in max_list_entries.strip().split(',')]) # FIXME: Not quite sure about this: self._MAX_SWEEP_ENTRIES = 10001 # extract the options from the device: message = self._ask('*OPT?').strip().split(',') self._OPTIONS = [entry for entry in message if entry != '0'] # get the info from the device: message = self._ask('*IDN?').strip().split(',') self._BRAND = message[0] self._MODEL = message[1] self._SERIALNUMBER = message[2] self._FIRMWARE_VERSION = message[3] self.log.info('Load the device model "{0}" from "{1}" with the serial' 'number "{2}" and the firmware version "{3}" ' 'successfully.'.format(self._MODEL, self._BRAND, self._SERIALNUMBER, self._FIRMWARE_VERSION)) def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. """ # self.off() # turn the device off in case it is running # self._gpib_connection.close() # close the gpib connection # self.rm.close() # close the resource manager return def get_limits(self): """ Retrieve the limits of the device. @return: object MicrowaveLimits: Serves as a container for the limits of the microwave device. """ limits = MicrowaveLimits() limits.supported_modes = (MicrowaveMode.CW, MicrowaveMode.LIST) # the sweep mode seems not to work properly, comment it out: #MicrowaveMode.SWEEP) limits.min_frequency = self._FREQ_MIN limits.max_frequency = self._FREQ_MAX limits.min_power = self._POWER_MIN limits.max_power = self._POWER_MAX limits.list_minstep = self._LIST_FREQ_STEP_MIN limits.list_maxstep = self._LIST_FREQ_STEP_MAX limits.list_maxentries = self._MAX_LIST_ENTRIES limits.sweep_minstep = self._SWEEP_FREQ_STEP_MIN limits.sweep_maxstep = self._SWEEP_FREQ_STEP_MAX limits.sweep_maxentries = self._MAX_SWEEP_ENTRIES return limits def off(self): """ Switches off any microwave output. Must return AFTER the device is actually stopped. @return int: error code (0:OK, -1:error) """ mode, is_running = self.get_status() if not is_running: return 0 self._write(':OUTP OFF') if mode == 'list': self._write(':FREQ:MODE CW') # check whether while int(float(self._ask('OUTP:STAT?').strip())) != 0: time.sleep(0.2) return 0 def get_status(self): """ Get the current status of the MW source, i.e. the mode (cw, list or sweep) and the output state (stopped, running). @return str, bool: mode ['cw', 'list', 'sweep'], is_running [True, False] """ is_running = bool(int(self._ask('OUTP:STAT?'))) mode = self._ask(':FREQ:MODE?').strip().lower() # The modes 'fix' and 'cw' are treated the same in the SMR device, # therefore, 'fix' is converted to 'cw': if mode == 'fix': mode = 'cw' # rename the mode according to the interface if mode == 'swe': mode = 'sweep' return mode, is_running def get_power(self): """ Gets the microwave output power. @return float: the power set at the device in dBm """ mode, dummy = self.get_status() if 'list' in mode: power_list = self._ask(':LIST:POW?').strip().split(',') # THIS AMBIGUITY IN THE RETURN VALUE TYPE IS NOT GOOD AT ALL!!! #FIXME: Correct that as soon as possible in the interface!!! return np.array([float(power) for power in power_list]) else: return float(self._ask(':POW?')) def get_frequency(self): """ Gets the frequency of the microwave output. @return float|list: frequency(s) currently set for this device in Hz Returns single float value if the device is in cw mode. Returns list like [start, stop, step] if the device is in sweep mode. Returns list of frequencies if the device is in list mode. """ # THIS AMBIGUITY IN THE RETURN VALUE TYPE IS NOT GOOD AT ALL!!! # FIXME: Correct that as soon as possible in the interface!!! mode, is_running = self.get_status() if 'cw' in mode: return_val = float(self._ask(':FREQ?')) elif 'sweep' in mode: start = float(self._ask(':FREQ:STAR?')) stop = float(self._ask(':FREQ:STOP?')) step = float(self._ask(':SWE:STEP?')) return_val = [start+step, stop, step] elif 'list' in mode: # Exclude first frequency entry, since that is a duplicate due to # trigger issues if triggered from external sources, like NI card. freq_list = self._ask(':LIST:FREQ?').strip().split(',') if len(freq_list) > 1: freq_list.pop() return_val = np.array([float(freq) for freq in freq_list]) else: self.log.error('Mode Unknown! Cannot determine Frequency!') return return_val def cw_on(self): """ Switches on cw microwave output. @return int: error code (0:OK, -1:error) Must return AFTER the device is actually running. """ current_mode, is_running = self.get_status() if is_running: if current_mode == 'cw': return 0 else: self.off() if current_mode != 'cw': self._write(':FREQ:MODE CW') self._write(':OUTP:STAT ON') self._write('*WAI') dummy, is_running = self.get_status() while not is_running: time.sleep(0.2) dummy, is_running = self.get_status() return 0 def set_cw(self, frequency=None, power=None): """ Configures the device for cw-mode and optionally sets frequency and/or power @param float frequency: frequency to set in Hz @param float power: power to set in dBm @return tuple(float, float, str): with the relation current frequency in Hz, current power in dBm, current mode """ mode, is_running = self.get_status() if is_running: self.off() # Activate CW mode if mode != 'cw': self._write(':FREQ:MODE CW') # Set CW frequency if frequency is not None: self._write(':FREQ {0:f}'.format(frequency)) # Set CW power if power is not None: self._write(':POW {0:f}'.format(power)) # Return actually set values mode, dummy = self.get_status() actual_freq = self.get_frequency() actual_power = self.get_power() return actual_freq, actual_power, mode def list_on(self): """ Switches on the list mode microwave output. Must return AFTER the device is actually running. @return int: error code (0:OK, -1:error) """ current_mode, is_running = self.get_status() if is_running: if current_mode == 'list': return 0 else: self.off() self._write(':LIST:LEARN') self._write(':FREQ:MODE LIST') self._write(':OUTP:STAT ON') dummy, is_running = self.get_status() while not is_running: time.sleep(0.2) dummy, is_running = self.get_status() return 0 def set_list(self, frequency=None, power=None): """ Configures the device for list-mode and optionally sets frequencies and/or power @param list frequency: list of frequencies in Hz @param float power: MW power of the frequency list in dBm @return tuple(list, float, str): current frequencies in Hz, current power in dBm, current mode """ mode, is_running = self.get_status() if is_running: self.off() # Bug in the micro controller of SMR20: # check the amount of entries, since the timeout is not working properly # and the SMR20 overwrites for too big entries the device-internal # memory such that the current firmware becomes corrupt. That is an # extreme annoying bug. Therefore catch too long lists. if len(frequency) > self._MAX_LIST_ENTRIES: self.log.error('The frequency list exceeds the hardware limitation ' 'of {0} list entries. Aborting creation of a list ' 'due to potential overwrite of the firmware on the ' 'device.'.format(self._MAX_LIST_ENTRIES)) else: self._write(':SOUR:LIST:MODE STEP') # It seems that we have to set a DWEL for the device, but it is not so # clear why it is necessary. At least there was a hint in the manual for # that and the instrument displays an error, when this parameter is not # set in the list mode (even it should be set by default): self._write(':SOUR:LIST:DWEL {0}'.format(self._LIST_DWELL)) self._write(':TRIG1:LIST:SOUR EXT') self._write(':TRIG1:SLOP NEG') # delete all list entries and create/select a new list self._write(':SOUR:LIST:DEL:ALL') self._write(':SOUR:LIST:SEL "LIST1"') FreqString = '' PowerString = '' for f in frequency[:-1]: FreqString += ' {0:f}Hz,'.format(f) PowerString +=' {0:f}dBm,'.format(power) FreqString += ' {0:f}Hz'.format(frequency[-1]) PowerString +=' {0:f}dBm'.format(power) self._write(':SOUR:LIST:FREQ' + FreqString) self._write(':SOUR:LIST:POW' + PowerString) self._write(':OUTP:AMOD FIX') # Apply settings in hardware self._write(':LIST:LEARN') # If there are timeout problems after this command, update the smiq # firmware to > 5.90 as there was a problem with excessive wait # times after issuing :LIST:LEARN over a GPIB connection in # firmware 5.88. self._write(':FREQ:MODE LIST') N = int(np.round(float(self._ask(':SOUR:LIST:FREQ:POIN?')))) if N != len(frequency): self.log.error('The input Frequency list does not corresponds ' 'to the generated List from the SMR20.') actual_freq = self.get_frequency() actual_power_list = self.get_power() # in list mode we get a power list! # THIS AMBIGUITY IN THE RETURN VALUE TYPE IS NOT GOOD AT ALL!!! # FIXME: Ahh this is so shitty with the return value!!! actual_power = actual_power_list[0] mode, dummy = self.get_status() return actual_freq, actual_power, mode def reset_listpos(self): """ Reset of MW List Mode position to start from first given frequency @return int: error code (0:OK, -1:error) """ self._gpib_connection.write(':ABOR:LIST') return 0 def sweep_on(self): """ Switches on the sweep mode. @return int: error code (0:OK, -1:error) """ mode, is_running = self.get_status() if is_running: if mode == 'sweep': return 0 else: self.off() if mode != 'sweep': self._write('SOUR:FREQ:MODE SWE') self._write(':OUTP:STAT ON') dummy, is_running = self.get_status() while not is_running: time.sleep(0.2) dummy, is_running = self.get_status() return 0 def set_sweep(self, start=None, stop=None, step=None, power=None): """ Configures the device for sweep-mode and optionally sets frequency start/stop/step and/or power @return float, float, float, float, str: current start frequency in Hz, current stop frequency in Hz, current frequency step in Hz, current power in dBm, current mode """ mode, is_running = self.get_status() if is_running: self.off() if mode != 'sweep': self._write('SOUR:FREQ:MODE SWE') self._write(':SOUR:SWE:FREQ:SPAC LIN') self._write(':SOUR:SWE:FREQ:STEP {0}'.format()) if (start is not None) and (stop is not None) and (step is not None): self._write(':FREQ:START {0}'.format(start - step)) self._write(':FREQ:STOP {0}'.format(stop)) self._write(':SWE:FREQ:STEP {0}'.format(step)) if power is not None: self._write(':POW {0:f}'.format(power)) self._write(':TRIG:SOUR EXT') actual_power = self.get_power() freq_list = self.get_frequency() mode, dummy = self.get_status() return freq_list[0], freq_list[1], freq_list[2], actual_power, mode def reset_sweeppos(self): """ Reset of MW sweep mode position to start (start frequency) @return int: error code (0:OK, -1:error) """ self._command_wait(':ABORT') return 0 def set_ext_trigger(self, pol, timing): """ Set the external trigger for this device with proper polarization. @param float timing: estimated time between triggers @param TriggerEdge pol: polarisation of the trigger (basically rising edge or falling edge) @return object, float: current trigger polarity [TriggerEdge.RISING, TriggerEdge.FALLING], trigger timing """ if pol == TriggerEdge.RISING: edge = 'POS' elif pol == TriggerEdge.FALLING: edge = 'NEG' else: self.log.warning('No valid trigger polarity passed to microwave hardware module.') edge = None self._write(':TRIG1:LIST:SOUR EXT') self._write(':TRIG1:SLOP NEG') if edge is not None: self._write(':TRIG1:SLOP {0}'.format(edge)) polarity = self._ask(':TRIG1:SLOP?') if 'NEG' in polarity: return TriggerEdge.FALLING, timing else: return TriggerEdge.RISING, timing # ================== Non interface commands: ================== def _set_power(self, power): """ Sets the microwave output power. @param float power: the power (in dBm) set for this device @return float: actual power set (in dBm) """ # every time a single power is set, the CW mode is activated! self._write(':FREQ:MODE CW') self._write('*WAI') self._write(':POW {0:f};'.format(power)) actual_power = self.get_power() return actual_power def _set_frequency(self, freq): """ Sets the frequency of the microwave output. @param float freq: the frequency (in Hz) set for this device @return int: error code (0:OK, -1:error) """ # every time a single frequency is set, the CW mode is activated! self._write(':FREQ:MODE CW') self._write('*WAI') self._write(':FREQ {0:e}'.format(freq)) # {:e} means a representation in float with exponential style return 0 def turn_AM_on(self, depth): """ Turn on the Amplitude Modulation mode. @param float depth: modulation depth in percent (from 0 to 100%). @return int: error code (0:OK, -1:error) Set the Amplitude modulation based on an external DC signal source and switch on the device after configuration. """ self._write('AM:SOUR EXT') self._write('AM:EXT:COUP DC') self._write('AM {0:f}'.format(float(depth))) self._write('AM:STAT ON') return 0 def turn_AM_off(self): """ Turn off the Amlitude Modulation Mode. @return int: error code (0:OK, -1:error) """ self._write(':AM:STAT OFF') return 0 def trigger(self): """ Trigger the next element in the list or sweep mode programmatically. @return int: error code (0:OK, -1:error) Ensure that the Frequency was set AFTER the function returns, or give the function at least a save waiting time. """ self._gpib_connection.write('*TRG') time.sleep(self._FREQ_SWITCH_SPEED) # that is the switching speed return 0 def reset_device(self): """ Resets the device and sets the default values.""" self._write(':SYSTem:PRESet') self._write('*RST') self._write(':OUTP OFF') return 0 def _ask(self, question): """ Ask wrapper. @param str question: a question to the device @return: the received answer """ return self._gpib_connection.query(question) def _write(self, command, wait=True): """ Write wrapper. @param str command: a command to the device @param bool wait: optional, is the wait statement should be skipped. @return: str: the statuscode of the write command. """ statuscode = self._gpib_connection.write(command) if wait: self._gpib_connection.write('*WAI') return statuscode
class ConfocalScannerDummy(Base, ConfocalScannerInterface): """ Dummy confocal scanner. Produces a picture with several gaussian spots. """ _modclass = 'ConfocalScannerDummy' _modtype = 'hardware' # connectors fitlogic = Connector(interface='FitLogic') # config _clock_frequency = ConfigOption('clock_frequency', 100, missing='warn') def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) # Internal parameters self._line_length = None self._voltage_range = [-10, 10] self._position_range = [[0, 100e-6], [0, 100e-6], [0, 100e-6], [0, 1e-6]] self._current_position = [0, 0, 0, 0][0:len(self.get_scanner_axes())] self._num_points = 500 def on_activate(self): """ Initialisation performed during activation of the module. """ self._fit_logic = self.fitlogic() # put randomly distributed NVs in the scanner, first the x,y scan self._points = np.empty([self._num_points, 7]) # amplitude self._points[:, 0] = np.random.normal(4e5, 1e5, self._num_points) # x_zero self._points[:, 1] = np.random.uniform(self._position_range[0][0], self._position_range[0][1], self._num_points) # y_zero self._points[:, 2] = np.random.uniform(self._position_range[1][0], self._position_range[1][1], self._num_points) # sigma_x self._points[:, 3] = np.random.normal(0.7e-6, 0.1e-6, self._num_points) # sigma_y self._points[:, 4] = np.random.normal(0.7e-6, 0.1e-6, self._num_points) # theta self._points[:, 5] = 10 # offset self._points[:, 6] = 0 # now also the z-position # gaussian_function(self,x_data=None,amplitude=None, x_zero=None, sigma=None, offset=None): self._points_z = np.empty([self._num_points, 4]) # amplitude self._points_z[:, 0] = np.random.normal(1, 0.05, self._num_points) # x_zero self._points_z[:, 1] = np.random.uniform(45e-6, 55e-6, self._num_points) # sigma self._points_z[:, 2] = np.random.normal(0.5e-6, 0.1e-6, self._num_points) # offset self._points_z[:, 3] = 0 def on_deactivate(self): """ Deactivate properly the confocal scanner dummy. """ self.reset_hardware() def reset_hardware(self): """ Resets the hardware, so the connection is lost and other programs can access it. @return int: error code (0:OK, -1:error) """ self.log.warning('Scanning Device will be reset.') return 0 def get_position_range(self): """ Returns the physical range of the scanner. @return float [4][2]: array of 4 ranges with an array containing lower and upper limit """ return self._position_range def set_position_range(self, myrange=None): """ Sets the physical range of the scanner. @param float [4][2] myrange: array of 4 ranges with an array containing lower and upper limit @return int: error code (0:OK, -1:error) """ if myrange is None: myrange = [[0, 1e-6], [0, 1e-6], [0, 1e-6], [0, 1e-6]] if not isinstance(myrange, ( frozenset, list, set, tuple, np.ndarray, )): self.log.error('Given range is no array type.') return -1 if len(myrange) != 4: self.log.error('Given range should have dimension 4, but has ' '{0:d} instead.'.format(len(myrange))) return -1 for pos in myrange: if len(pos) != 2: self.log.error('Given range limit {1:d} should have ' 'dimension 2, but has {0:d} instead.'.format( len(pos), pos)) return -1 if pos[0] > pos[1]: self.log.error('Given range limit {0:d} has the wrong ' 'order.'.format(pos)) return -1 self._position_range = myrange return 0 def set_voltage_range(self, myrange=None): """ Sets the voltage range of the NI Card. @param float [2] myrange: array containing lower and upper limit @return int: error code (0:OK, -1:error) """ if myrange is None: myrange = [-10., 10.] if not isinstance(myrange, ( frozenset, list, set, tuple, np.ndarray, )): self.log.error('Given range is no array type.') return -1 if len(myrange) != 2: self.log.error('Given range should have dimension 2, but has ' '{0:d} instead.'.format(len(myrange))) return -1 if myrange[0] > myrange[1]: self.log.error('Given range limit {0:d} has the wrong ' 'order.'.format(myrange)) return -1 if self.module_state() == 'locked': self.log.error('A Scanner is already running, close this one ' 'first.') return -1 self._voltage_range = myrange return 0 def get_scanner_axes(self): """ Dummy scanner is always 3D cartesian. """ return ['x', 'y', 'z', 'a'] def get_scanner_count_channels(self): """ 3 counting channels in dummy confocal: normal, negative and a ramp.""" return ['Norm', 'Neg', 'Ramp'] def set_up_scanner_clock(self, clock_frequency=None, clock_channel=None): """ Configures the hardware clock of the NiDAQ card to give the timing. @param float clock_frequency: if defined, this sets the frequency of the clock @param str clock_channel: if defined, this is the physical channel of the clock @return int: error code (0:OK, -1:error) """ if clock_frequency is not None: self._clock_frequency = float(clock_frequency) self.log.debug('ConfocalScannerDummy>set_up_scanner_clock') time.sleep(0.2) return 0 def set_up_scanner(self, counter_channels=None, sources=None, clock_channel=None, scanner_ao_channels=None): """ Configures the actual scanner with a given clock. @param str counter_channel: if defined, this is the physical channel of the counter @param str photon_source: if defined, this is the physical channel where the photons are to count from @param str clock_channel: if defined, this specifies the clock for the counter @param str scanner_ao_channels: if defined, this specifies the analoque output channels @return int: error code (0:OK, -1:error) """ self.log.debug('ConfocalScannerDummy>set_up_scanner') time.sleep(0.2) return 0 def scanner_set_position(self, x=None, y=None, z=None, a=None): """Move stage to x, y, z, a (where a is the fourth voltage channel). @param float x: postion in x-direction (volts) @param float y: postion in y-direction (volts) @param float z: postion in z-direction (volts) @param float a: postion in a-direction (volts) @return int: error code (0:OK, -1:error) """ if self.module_state() == 'locked': self.log.error( 'A Scanner is already running, close this one first.') return -1 time.sleep(0.01) self._current_position = [x, y, z, a][0:len(self.get_scanner_axes())] return 0 def get_scanner_position(self): """ Get the current position of the scanner hardware. @return float[]: current position in (x, y, z, a). """ return self._current_position[0:len(self.get_scanner_axes())] def _set_up_line(self, length=100): """ Sets up the analoque output for scanning a line. @param int length: length of the line in pixel @return int: error code (0:OK, -1:error) """ self._line_length = length # self.log.debug('ConfocalScannerInterfaceDummy>set_up_line') return 0 def scan_line(self, line_path=None, pixel_clock=False): """ Scans a line and returns the counts on that line. @param float[][4] line_path: array of 4-part tuples defining the voltage points @param bool pixel_clock: whether we need to output a pixel clock for this line @return float[]: the photon counts per second """ if not isinstance(line_path, ( frozenset, list, set, tuple, np.ndarray, )): self.log.error('Given voltage list is no array type.') return np.array([[-1.]]) if np.shape(line_path)[1] != self._line_length: self._set_up_line(np.shape(line_path)[1]) count_data = np.random.uniform(0, 2e4, self._line_length) z_data = line_path[2, :] #TODO: Change the gaussian function here to the one from fitlogic and delete the local modules to calculate #the gaussian functions x_data = np.array(line_path[0, :]) y_data = np.array(line_path[1, :]) for i in range(self._num_points): count_data += self.twoD_gaussian_function( (x_data, y_data), *(self._points[i])) * self.gaussian_function( np.array(z_data), *(self._points_z[i])) time.sleep(self._line_length * 1. / self._clock_frequency) time.sleep(self._line_length * 1. / self._clock_frequency) # update the scanner position instance variable self._current_position = list(line_path[:, -1]) return np.array([ count_data, 5e5 - count_data, np.ones(count_data.shape) * line_path[1, 0] * 100 ]).transpose() def close_scanner(self): """ Closes the scanner and cleans up afterwards. @return int: error code (0:OK, -1:error) """ self.log.debug('ConfocalScannerDummy>close_scanner') return 0 def close_scanner_clock(self, power=0): """ Closes the clock and cleans up afterwards. @return int: error code (0:OK, -1:error) """ self.log.debug('ConfocalScannerDummy>close_scanner_clock') return 0 ############################################################################ # # # the following two functions are needed to fluorescence signal # # of the dummy NVs # # # ############################################################################ def twoD_gaussian_function(self, x_data_tuple=None, amplitude=None, x_zero=None, y_zero=None, sigma_x=None, sigma_y=None, theta=None, offset=None): #FIXME: x_data_tuple: dimension of arrays """ This method provides a two dimensional gaussian function. @param (k,M)-shaped array x_data_tuple: x and y values @param float or int amplitude: Amplitude of gaussian @param float or int x_zero: x value of maximum @param float or int y_zero: y value of maximum @param float or int sigma_x: standard deviation in x direction @param float or int sigma_y: standard deviation in y direction @param float or int theta: angle for eliptical gaussians @param float or int offset: offset @return callable function: returns the function """ # check if parameters make sense #FIXME: Check for 2D matrix if not isinstance(x_data_tuple, (frozenset, list, set, tuple, np.ndarray)): self.log.error('Given range of axes is no array type.') parameters = [ amplitude, x_zero, y_zero, sigma_x, sigma_y, theta, offset ] for var in parameters: if not isinstance(var, (float, int)): self.log.error('Given range of parameter is no float or int.') (x, y) = x_data_tuple x_zero = float(x_zero) y_zero = float(y_zero) a = (np.cos(theta)**2) / (2 * sigma_x**2) + (np.sin(theta)** 2) / (2 * sigma_y**2) b = -(np.sin(2 * theta)) / (4 * sigma_x**2) + (np.sin( 2 * theta)) / (4 * sigma_y**2) c = (np.sin(theta)**2) / (2 * sigma_x**2) + (np.cos(theta)** 2) / (2 * sigma_y**2) g = offset + amplitude * np.exp(-(a * ((x - x_zero)**2) + 2 * b * (x - x_zero) * (y - y_zero) + c * ((y - y_zero)**2))) return g.ravel() def gaussian_function(self, x_data=None, amplitude=None, x_zero=None, sigma=None, offset=None): """ This method provides a one dimensional gaussian function. @param array x_data: x values @param float or int amplitude: Amplitude of gaussian @param float or int x_zero: x value of maximum @param float or int sigma: standard deviation @param float or int offset: offset @return callable function: returns a 1D Gaussian function """ # check if parameters make sense if not isinstance(x_data, (frozenset, list, set, tuple, np.ndarray)): self.log.error('Given range of axis is no array type.') parameters = [amplitude, x_zero, sigma, offset] for var in parameters: if not isinstance(var, (float, int)): print('error', var) self.log.error('Given range of parameter is no float or int.') gaussian = amplitude * np.exp(-(x_data - x_zero)**2 / (2 * sigma**2)) + offset return gaussian
class FastCounterFGAPiP3(Base, FastCounterInterface): _modclass = 'FastCounterFGAPiP3' _modtype = 'hardware' # config options _fpgacounter_serial = ConfigOption('fpgacounter_serial', missing='error') _channel_apd_0 = ConfigOption('fpgacounter_channel_apd_0', 1, missing='warn') _channel_apd_1 = ConfigOption('fpgacounter_channel_apd_1', 3, missing='warn') _channel_detect = ConfigOption('fpgacounter_channel_detect', 2, missing='warn') _channel_sequence = ConfigOption('fpgacounter_channel_sequence', 6, missing='warn') def on_activate(self): """ Connect and configure the access to the FPGA. """ tt._Tagger_setSerial(self._fpgacounter_serial) thirdpartypath = os.path.join(get_main_dir(), 'thirdparty') bitfilepath = os.path.join(thirdpartypath, 'stuttgart_counter', 'TimeTaggerController.bit') tt._Tagger_setBitfilePath(bitfilepath) del bitfilepath, thirdpartypath self._number_of_gates = int(100) self._bin_width = 1 self._record_length = int(4000) self.configure(self._bin_width * 1e-9, self._record_length * 1e-9, self._number_of_gates) self.statusvar = 0 def get_constraints(self): """ Retrieve the hardware constrains from the Fast counting device. @return dict: dict with keys being the constraint names as string and items are the definition for the constaints. The keys of the returned dictionary are the str name for the constraints (which are set in this method). NO OTHER KEYS SHOULD BE INVENTED! If you are not sure about the meaning, look in other hardware files to get an impression. If still additional constraints are needed, then they have to be added to all files containing this interface. The items of the keys are again dictionaries which have the generic dictionary form: {'min': <value>, 'max': <value>, 'step': <value>, 'unit': '<value>'} Only the key 'hardware_binwidth_list' differs, since they contain the list of possible binwidths. If the constraints cannot be set in the fast counting hardware then write just zero to each key of the generic dicts. Note that there is a difference between float input (0.0) and integer input (0), because some logic modules might rely on that distinction. ALL THE PRESENT KEYS OF THE CONSTRAINTS DICT MUST BE ASSIGNED! """ constraints = dict() # the unit of those entries are seconds per bin. In order to get the # current binwidth in seonds use the get_binwidth method. constraints['hardware_binwidth_list'] = [1 / 1000e6] # TODO: think maybe about a software_binwidth_list, which will # postprocess the obtained counts. These bins must be integer # multiples of the current hardware_binwidth return constraints def on_deactivate(self): """ Deactivate the FPGA. """ if self.module_state() == 'locked': self.pulsed.stop() self.pulsed.clear() self.pulsed = None def configure(self, bin_width_s, record_length_s, number_of_gates=0): """ Configuration of the fast counter. @param float bin_width_s: Length of a single time bin in the time trace histogram in seconds. @param float record_length_s: Total length of the timetrace/each single gate in seconds. @param int number_of_gates: optional, number of gates in the pulse sequence. Ignore for not gated counter. @return tuple(binwidth_s, gate_length_s, number_of_gates): binwidth_s: float the actual set binwidth in seconds gate_length_s: the actual set gate length in seconds number_of_gates: the number of gated, which are accepted """ self._number_of_gates = number_of_gates self._bin_width = bin_width_s * 1e9 self._record_length = int(record_length_s / bin_width_s) self.statusvar = 1 self.pulsed = tt.Pulsed(self._record_length, int(np.round(self._bin_width * 1000)), self._number_of_gates, self._channel_apd_0, self._channel_detect, self._channel_sequence) return (bin_width_s, record_length_s, number_of_gates) def start_measure(self): """ Start the fast counter. """ self.module_state.lock() self.pulsed.clear() self.pulsed.start() self.statusvar = 2 return 0 def stop_measure(self): """ Stop the fast counter. """ if self.module_state() == 'locked': self.pulsed.stop() self.module_state.unlock() self.statusvar = 1 return 0 def pause_measure(self): """ Pauses the current measurement. Fast counter must be initially in the run state to make it pause. """ if self.module_state() == 'locked': self.pulsed.stop() self.statusvar = 3 return 0 def continue_measure(self): """ Continues the current measurement. If fast counter is in pause state, then fast counter will be continued. """ if self.module_state() == 'locked': self.pulsed.start() self.statusvar = 2 return 0 def is_gated(self): """ Check the gated counting possibility. Boolean return value indicates if the fast counter is a gated counter (TRUE) or not (FALSE). """ return True def get_data_trace(self): """ Polls the current timetrace data from the fast counter. @return numpy.array: 2 dimensional array of dtype = int64. This counter is gated the the return array has the following shape: returnarray[gate_index, timebin_index] The binning, specified by calling configure() in forehand, must be taken care of in this hardware class. A possible overflow of the histogram bins must be caught here and taken care of. """ return np.array(self.pulsed.getData(), dtype='int64') def get_status(self): """ Receives the current status of the Fast Counter and outputs it as return value. 0 = unconfigured 1 = idle 2 = running 3 = paused -1 = error state """ return self.statusvar def get_binwidth(self): """ Returns the width of a single timebin in the timetrace in seconds. """ width_in_seconds = self._bin_width * 1e-9 return width_in_seconds
class WavemeterLoggerLogic(GenericLogic): """This logic module gathers data from wavemeter and the counter logic. """ sig_data_updated = QtCore.Signal() sig_update_histogram_next = QtCore.Signal(bool) sig_handle_timer = QtCore.Signal(bool) sig_new_data_point = QtCore.Signal(list) sig_fit_updated = QtCore.Signal() _modclass = 'laserscanninglogic' _modtype = 'logic' # declare connectors wavemeter1 = Connector(interface='WavemeterInterface') counterlogic = Connector(interface='CounterLogic') savelogic = Connector(interface='SaveLogic') fitlogic = Connector(interface='FitLogic') # config opts _logic_acquisition_timing = ConfigOption('logic_acquisition_timing', 20.0, missing='warn') _logic_update_timing = ConfigOption('logic_update_timing', 100.0, missing='warn') def __init__(self, config, **kwargs): """ Create WavemeterLoggerLogic object with connectors. @param dict config: module configuration @param dict kwargs: optional parameters """ super().__init__(config=config, **kwargs) # locking for thread safety self.threadlock = Mutex() self._acqusition_start_time = 0 self._bins = 200 self._data_index = 0 self._recent_wavelength_window = [0, 0] self.counts_with_wavelength = [] self._xmin = 650 self._xmax = 750 # internal min and max wavelength determined by the measured wavelength self.intern_xmax = -1.0 self.intern_xmin = 1.0e10 self.current_wavelength = 0 def on_activate(self): """ Initialisation performed during activation of the module. """ self._wavelength_data = [] self.stopRequested = False self._wavemeter_device = self.get_connector('wavemeter1') # print("Counting device is", self._counting_device) self._save_logic = self.get_connector('savelogic') self._counter_logic = self.get_connector('counterlogic') self._fit_logic = self.get_connector('fitlogic') self.fc = self._fit_logic.make_fit_container('Wavemeter counts', '1d') self.fc.set_units(['Hz', 'c/s']) if 'fits' in self._statusVariables and isinstance( self._statusVariables['fits'], dict): self.fc.load_from_dict(self._statusVariables['fits']) else: d1 = OrderedDict() d1['Lorentzian peak'] = { 'fit_function': 'lorentzian', 'estimator': 'peak' } d1['Two Lorentzian peaks'] = { 'fit_function': 'lorentziandouble', 'estimator': 'peak' } d1['Two Gaussian peaks'] = { 'fit_function': 'gaussiandouble', 'estimator': 'peak' } default_fits = OrderedDict() default_fits['1d'] = d1 self.fc.load_from_dict(default_fits) # create a new x axis from xmin to xmax with bins points self.histogram_axis = np.arange(self._xmin, self._xmax, (self._xmax - self._xmin) / self._bins) self.histogram = np.zeros(self.histogram_axis.shape) self.envelope_histogram = np.zeros(self.histogram_axis.shape) self.sig_update_histogram_next.connect( self._attach_counts_to_wavelength, QtCore.Qt.QueuedConnection) # fit data self.wlog_fit_x = np.linspace(self._xmin, self._xmax, self._bins * 5) self.wlog_fit_y = np.zeros(self.wlog_fit_x.shape) # create an indepentent thread for the hardware communication self.hardware_thread = QtCore.QThread() # create an object for the hardware communication and let it live on the new thread self._hardware_pull = HardwarePull(self) self._hardware_pull.moveToThread(self.hardware_thread) # connect the signals in and out of the threaded object self.sig_handle_timer.connect(self._hardware_pull.handle_timer) # start the event loop for the hardware self.hardware_thread.start() self.last_point_time = time.time() def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. """ if self.module_state() != 'idle' and self.module_state( ) != 'deactivated': self.stop_scanning() self.hardware_thread.quit() self.sig_handle_timer.disconnect() if len(self.fc.fit_list) > 0: self._statusVariables['fits'] = self.fc.save_to_dict() def get_max_wavelength(self): """ Current maximum wavelength of the scan. @return float: current maximum wavelength """ return self._xmax def get_min_wavelength(self): """ Current minimum wavelength of the scan. @return float: current minimum wavelength """ return self._xmin def get_bins(self): """ Current number of bins in the spectrum. @return int: current number of bins in the scan """ return self._bins def recalculate_histogram(self, bins=None, xmin=None, xmax=None): """ Recalculate the current spectrum from raw data. @praram int bins: new number of bins @param float xmin: new minimum wavelength @param float xmax: new maximum wavelength """ if bins is not None: self._bins = bins if xmin is not None: self._xmin = xmin if xmax is not None: self._xmax = xmax # create a new x axis from xmin to xmax with bins points self.rawhisto = np.zeros(self._bins) self.envelope_histogram = np.zeros(self._bins) self.sumhisto = np.ones(self._bins) * 1.0e-10 self.histogram_axis = np.linspace(self._xmin, self._xmax, self._bins) self.sig_update_histogram_next.emit(True) def get_fit_functions(self): """ Return the names of all ocnfigured fit functions. @return list(str): list of fit function names """ return self.fc.fit_list.keys() def do_fit(self): """ Execute the currently configured fit """ self.wlog_fit_x, self.wlog_fit_y, result = self.fc.do_fit( self.histogram_axis, self.histogram) self.sig_fit_updated.emit() self.sig_data_updated.emit() def start_scanning(self, resume=False): """ Prepare to start counting: zero variables, change state and start counting "loop" @param bool resume: whether to resume measurement """ self.module_state.run() if self._counter_logic.module_state() == 'idle': self._counter_logic.startCount() if self._counter_logic.get_saving_state(): self._counter_logic.save_data() self._wavemeter_device.start_acqusition() self._counter_logic.start_saving(resume=resume) if not resume: self._acqusition_start_time = self._counter_logic._saving_start_time self._wavelength_data = [] self.data_index = 0 self._recent_wavelength_window = [0, 0] self.counts_with_wavelength = [] self.rawhisto = np.zeros(self._bins) self.sumhisto = np.ones(self._bins) * 1.0e-10 self.intern_xmax = -1.0 self.intern_xmin = 1.0e10 self.recent_avg = [0, 0, 0] self.recent_count = 0 # start the measuring thread self.sig_handle_timer.emit(True) self._complete_histogram = True self.sig_update_histogram_next.emit(False) return 0 def stop_scanning(self): """ Set a flag to request stopping counting. """ if not self.module_state() == 'idle': # self._wavemeter_device.stop_acqusition() # stop the measurement thread self.sig_handle_timer.emit(False) # set status to idle again self.module_state.stop() if self._counter_logic.get_saving_state(): self._counter_logic.save_data(to_file=False) return 0 def _attach_counts_to_wavelength(self, complete_histogram): """ Interpolate a wavelength value for each photon count value. This process assumes that the wavelength is varying smoothly and fairly continuously, which is sensible for most measurement conditions. Recent count values are those recorded AFTER the previous stitch operation, but BEFORE the most recent wavelength value (do not extrapolate beyond the current wavelength information). """ # If there is not yet any wavelength data, then wait and signal next loop if len(self._wavelength_data) == 0: time.sleep(self._logic_update_timing * 1e-3) self.sig_data_updated.emit() return # The end of the recent_wavelength_window is the time of the latest wavelength data self._recent_wavelength_window[1] = self._wavelength_data[-1][0] # (speed-up) We only need to worry about "recent" counts, because as the count data gets # very long all the earlier points will already be attached to wavelength values. count_recentness = 100 # TODO: calculate this from count_freq and wavemeter refresh rate # TODO: Does this depend on things, or do we loop fast enough to get every wavelength value? wavelength_recentness = np.min([5, len(self._wavelength_data)]) recent_counts = np.array( self._counter_logic._data_to_save[-count_recentness:]) recent_wavelengths = np.array( self._wavelength_data[-wavelength_recentness:]) # The latest counts are those recorded during the recent_wavelength_window count_idx = [0, 0] count_idx[0] = np.searchsorted(recent_counts[:, 0], self._recent_wavelength_window[0]) count_idx[1] = np.searchsorted(recent_counts[:, 0], self._recent_wavelength_window[1]) latest_counts = recent_counts[count_idx[0]:count_idx[1]] # Interpolate to obtain wavelength values at the times of each count interpolated_wavelengths = np.interp(latest_counts[:, 0], xp=recent_wavelengths[:, 0], fp=recent_wavelengths[:, 1]) # Stitch interpolated wavelength into latest counts array latest_stitched_data = np.insert(latest_counts, 2, values=interpolated_wavelengths, axis=1) # Add this latest data to the list of counts vs wavelength self.counts_with_wavelength += latest_stitched_data.tolist() # The start of the recent data window for the next round will be the end of this one. self._recent_wavelength_window[0] = self._recent_wavelength_window[1] # Run the old update histogram method to keep duplicate data self._update_histogram(complete_histogram) # Signal that data has been updated self.sig_data_updated.emit() # Wait and repeat if measurement is ongoing time.sleep(self._logic_update_timing * 1e-3) if self.module_state() == 'running': self.sig_update_histogram_next.emit(False) def _update_histogram(self, complete_histogram): """ Calculate new points for the histogram. @param bool complete_histogram: should the complete histogram be recalculated, or just the most recent data? @return: """ # If things like num_of_bins have changed, then recalculate the complete histogram # Note: The histogram may be recalculated (bins changed, etc) from the stitched data. # There is no need to recompute the interpolation for the stitched data. if complete_histogram: count_window = len(self._counter_logic._data_to_save) self._data_index = 0 self.log.info('Recalcutating Laser Scanning Histogram for: ' '{0:d} counts and {1:d} wavelength.'.format( count_window, len(self._wavelength_data))) else: count_window = min(100, len(self._counter_logic._data_to_save)) if count_window < 2: time.sleep(self._logic_update_timing * 1e-3) self.sig_update_histogram_next.emit(False) return temp = np.array(self._counter_logic._data_to_save[-count_window:]) # only do something if there is wavelength data to work with if len(self._wavelength_data) > 0: for i in self._wavelength_data[self._data_index:]: self._data_index += 1 if i[1] < self._xmin or i[1] > self._xmax: continue # calculate the bin the new wavelength needs to go in newbin = np.digitize([i[1]], self.histogram_axis)[0] # if the bin make no sense, start from the beginning if newbin > len(self.rawhisto) - 1: continue # sum the counts in rawhisto and count the occurence of the bin in sumhisto interpolation = np.interp(i[0], xp=temp[:, 0], fp=temp[:, 1]) self.rawhisto[newbin] += interpolation self.sumhisto[newbin] += 1.0 self.envelope_histogram[newbin] = np.max( [interpolation, self.envelope_histogram[newbin]]) datapoint = [i[1], i[0], interpolation] if time.time() - self.last_point_time > 1: self.sig_new_data_point.emit(self.recent_avg) self.last_point_time = time.time() self.recent_count = 0 else: self.recent_count += 1 for j in range(3): self.recent_avg[ j] -= self.recent_avg[j] / self.recent_count self.recent_avg[j] += datapoint[j] / self.recent_count # the plot data is the summed counts divided by the occurence of the respective bins self.histogram = self.rawhisto / self.sumhisto def save_data(self, timestamp=None): """ Save the counter trace data and writes it to a file. @param datetime timestamp: timestamp passed from gui so that saved images match filenames of data. This will be removed when savelogic handles the image creation also. @return int: error code (0:OK, -1:error) """ self._saving_stop_time = time.time() filepath = self._save_logic.get_path_for_module( module_name='WavemeterLogger') filelabel = 'wavemeter_log_histogram' # Currently need to pass timestamp from gui so that the saved image matches saved data. # TODO: once the savelogic saves images, we can revert this to always getting timestamp here. if timestamp is None: timestamp = datetime.datetime.now() # prepare the data in a dict or in an OrderedDict: data = OrderedDict() data['Wavelength (nm)'] = np.array(self.histogram_axis) data['Signal (counts/s)'] = np.array(self.histogram) # write the parameters: parameters = OrderedDict() parameters['Bins (#)'] = self._bins parameters['Xmin (nm)'] = self._xmin parameters['XMax (nm)'] = self._xmax parameters['Start Time (s)'] = time.strftime( '%d.%m.%Y %Hh:%Mmin:%Ss', time.localtime(self._acqusition_start_time)) parameters['Stop Time (s)'] = time.strftime( '%d.%m.%Y %Hh:%Mmin:%Ss', time.localtime(self._saving_stop_time)) self._save_logic.save_data(data, filepath=filepath, parameters=parameters, filelabel=filelabel, timestamp=timestamp, fmt='%.12e') filelabel = 'wavemeter_log_wavelength' # prepare the data in a dict or in an OrderedDict: data = OrderedDict() data['Time (s), Wavelength (nm)'] = self._wavelength_data # write the parameters: parameters = OrderedDict() parameters['Acquisition Timing (ms)'] = self._logic_acquisition_timing parameters['Start Time (s)'] = time.strftime( '%d.%m.%Y %Hh:%Mmin:%Ss', time.localtime(self._acqusition_start_time)) parameters['Stop Time (s)'] = time.strftime( '%d.%m.%Y %Hh:%Mmin:%Ss', time.localtime(self._saving_stop_time)) self._save_logic.save_data(data, filepath=filepath, parameters=parameters, filelabel=filelabel, timestamp=timestamp, fmt='%.12e') filelabel = 'wavemeter_log_counts' # prepare the data in a dict or in an OrderedDict: data = OrderedDict() data['Time (s),Signal (counts/s)'] = self._counter_logic._data_to_save # write the parameters: parameters = OrderedDict() parameters['Start counting time (s)'] = time.strftime( '%d.%m.%Y %Hh:%Mmin:%Ss', time.localtime(self._counter_logic._saving_start_time)) parameters['Stop counting time (s)'] = time.strftime( '%d.%m.%Y %Hh:%Mmin:%Ss', time.localtime(self._saving_stop_time)) parameters[ 'Length of counter window (# of events)'] = self._counter_logic._count_length parameters[ 'Count frequency (Hz)'] = self._counter_logic._count_frequency parameters[ 'Oversampling (Samples)'] = self._counter_logic._counting_samples parameters[ 'Smooth Window Length (# of events)'] = self._counter_logic._smooth_window_length self._save_logic.save_data(data, filepath=filepath, parameters=parameters, filelabel=filelabel, timestamp=timestamp, fmt='%.12e') self.log.debug('Laser Scan saved to:\n{0}'.format(filepath)) filelabel = 'wavemeter_log_counts_with_wavelength' # prepare the data in a dict or in an OrderedDict: data = OrderedDict() data[ 'Measurement Time (s), Signal (counts/s), Interpolated Wavelength (nm)'] = np.array( self.counts_with_wavelength) fig = self.draw_figure() # write the parameters: parameters = OrderedDict() parameters['Start Time (s)'] = time.strftime( '%d.%m.%Y %Hh:%Mmin:%Ss', time.localtime(self._acqusition_start_time)) parameters['Stop Time (s)'] = time.strftime( '%d.%m.%Y %Hh:%Mmin:%Ss', time.localtime(self._saving_stop_time)) self._save_logic.save_data(data, filepath=filepath, parameters=parameters, filelabel=filelabel, timestamp=timestamp, plotfig=fig, fmt='%.12e') plt.close(fig) return 0 def draw_figure(self): """ Draw figure to save with data file. @return: fig fig: a matplotlib figure object to be saved to file. """ # TODO: Draw plot for second APD if it is connected wavelength_data = [entry[2] for entry in self.counts_with_wavelength] count_data = np.array( [entry[1] for entry in self.counts_with_wavelength]) # Index of max counts, to use to position "0" of frequency-shift axis count_max_index = count_data.argmax() # Scale count values using SI prefix prefix = ['', 'k', 'M', 'G'] prefix_index = 0 while np.max(count_data) > 1000: count_data = count_data / 1000 prefix_index = prefix_index + 1 counts_prefix = prefix[prefix_index] # Use qudi style plt.style.use(self._save_logic.mpl_qd_style) # Create figure fig, ax = plt.subplots() ax.plot(wavelength_data, count_data, linestyle=':', linewidth=0.5) ax.set_xlabel('wavelength (nm)') ax.set_ylabel('Fluorescence (' + counts_prefix + 'c/s)') x_formatter = mpl.ticker.ScalarFormatter(useOffset=False) ax.xaxis.set_major_formatter(x_formatter) ax2 = ax.twiny() nm_xlim = ax.get_xlim() ghz_at_max_counts = self.nm_to_ghz(wavelength_data[count_max_index]) ghz_min = self.nm_to_ghz(nm_xlim[0]) - ghz_at_max_counts ghz_max = self.nm_to_ghz(nm_xlim[1]) - ghz_at_max_counts ax2.set_xlim(ghz_min, ghz_max) ax2.set_xlabel('Shift (GHz)') return fig def nm_to_ghz(self, wavelength): """ Convert wavelength to frequency. @param float wavelength: vacuum wavelength @return float: freequency """ return 3e8 / wavelength
class FastCounterFPGAQO(Base, FastCounterInterface): """ unstable: Nikolas Tomek This is the hardware class for the Spartan-6 (Opal Kelly XEM6310) FPGA based fast counter. The command reference for the communicating via the OpalKelly Frontend can be looked up here: https://library.opalkelly.com/library/FrontPanelAPI/index.html The Frontpanel is basically a C++ interface, where a wrapper was used (SWIG) to access the dll library. Be aware that the wrapper is specified for a specific version of python (here python 3.4), and it is not guaranteed to be working with other versions. """ _modclass = 'FastCounterFPGAQO' _modtype = 'hardware' _serial = ConfigOption('fpgacounter_serial', missing='error') # 'No parameter "fpgacounter_serial" specified in the config! Set the ' # 'serial number for the currently used fpga counter!\n' # 'Open the Opal Kelly Frontpanel to obtain the serial number of the ' # 'connected FPGA.\nDo not forget to close the Frontpanel before starting ' # 'the Qudi program.') _fpga_type = ConfigOption('fpga_type', 'XEM6310_LX150', missing='warn') # 'No parameter "fpga_type" specified in the config!\n' # 'Possible types are "XEM6310_LX150" or "XEM6310_LX45".\n' # 'Taking the type "{0}" as default.'.format(self._fpga_type)) def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) self.threadlock = Mutex() self.log.debug('The following configuration was found.') for key in config.keys(): self.log.debug('{0}: {1}'.format(key, config[key])) self._internal_clock_hz = 950e6 # that is a fixed number, 950MHz self.statusvar = -1 # fast counter state # The following is the encoding (status flags and errors) of the FPGA status register self._status_encoding = { 0x00000001: 'initialization', 0x00000002: 'pulling_data', 0x00000004: 'idle_ready', 0x00000008: 'running', 0x80000000: 'TDC_in_reset' } self._error_encoding = { 0x00000020: 'Init/output FSM in FPGA hardware encountered an error.' ' Please reset the device to recover from this state.', 0x00000040: 'Histogram FSM in FPGA hardware encountered an error. ' 'Please reset the device to recover from this state.', 0x00000080: 'One or more histogram bins have overflown (32 bit ' 'unsigned integer). Time tagger will not continue to ' 'accumulate more events. Please save current data and ' 'start a new measurement.', 0x00000200: 'Output buffer FIFO for pipe transfer via USB has ' 'overflown. This should not happen under any ' 'circumstance. Please contact hardware manufacturer.', 0x00000400: 'Output buffer FIFO for pipe transfer via USB has ' 'underrun. This should not happen under any ' 'circumstance. Please contact hardware manufacturer.', 0x00000800: 'Power-On self calibration of DDR2 interface not ' 'successful. Please contact hardware manufacturer.', 0x00001000: 'Read buffer of init/output memory interface port has ' 'overflown. This should not happen under any ' 'circumstance. Please contact hardware manufacturer.', 0x00002000: 'Write buffer of init/output memory interface port has ' 'underrun. This should not happen under any ' 'circumstance. Please contact hardware manufacturer.', 0x00004000: 'Init/output memory interface read port has encountered' ' a fatal error. Please contact hardware manufacturer.', 0x00008000: 'Init/output memory interface write port has ' 'encountered a fatal error. Please contact hardware ' 'manufacturer.', 0x00010000: 'Read buffer of histogram memory interface port has ' 'overflown. This should not happen under any ' 'circumstance. Please contact hardware manufacturer.', 0x00020000: 'Write buffer of histogram memory interface port has ' 'underrun. This should not happen under any ' 'circumstance. Please contact hardware manufacturer.', 0x00040000: 'Histogram memory interface read port has encountered a' ' fatal error. Please contact hardware manufacturer.', 0x00080000: 'Histogram memory interface write port has encountered ' 'a fatal error. Please contact hardware manufacturer.', 0x00100000: 'Idle memory interface port encountered an error. ' 'This should not happen under any circumstance. ' 'Please contact hardware manufacturer.', 0x00200000: 'Idle memory interface port encountered an error. ' 'This should not happen under any circumstance. ' 'Please contact hardware manufacturer.', 0x00400000: 'Idle memory interface port encountered an error. ' 'This should not happen under any circumstance. ' 'Please contact hardware manufacturer.', 0x00800000: 'Idle memory interface port encountered an error. ' 'This should not happen under any circumstance. ' 'Please contact hardware manufacturer.', 0x01000000: 'Bandwidth of Timetagger buffer memory exceeded. This ' 'can happen if the rate of detector events is too high ' 'and/or the data requests are too frequent. Timetrace ' 'is not reliable.', 0x10000000: 'Timetagger event buffer has encountered an overflow. ' 'This should not happen under any circumstance. ' 'Please contact hardware manufacturer.', 0x20000000: 'Timetagger event buffer has encountered an underrun. ' 'This should not happen under any circumstance. ' 'Please contact hardware manufacturer.', 0x40000000: 'Power-On self calibration of TDC not successful. ' 'Please contact hardware manufacturer.' } def on_activate(self): """ Connect and configure the access to the FPGA. """ config = self.getConfiguration() self._switching_voltage = { 1: 0.5, 2: 0.5, 3: 0.5, 4: 0.5, 5: 0.5, 6: 0.5, 7: 0.5, 8: 0.5 } for key in config.keys(): if 'threshV_ch' in key: self._switching_voltage[int(key[-1])] = config[key] # fast counter state self.statusvar = -1 # fast counter parameters to be configured. Default values. self._binwidth = 1 # number of elementary bins to be combined into a single bin self._gate_length_bins = 8192 # number of bins in one gate (max 65536) self._number_of_gates = 1 # number of gates in the pulse sequence (max 512) self._old_data = None # histogram to be added to the current data after # continuing a measurement self.count_data = None self.saved_count_data = None # Count data stored to continue measurement # Create an instance of the Opal Kelly FrontPanel. The Frontpanel is a C dll which was # wrapped for use with python. self._fpga = ok.FrontPanel() # connect to the FPGA module self._connect() # configure DAC for threshold voltages self._reset_dac() self._activate_dac_ref() self._set_dac_voltages() return def on_deactivate(self): """ Deactivate the FPGA. """ self.stop_measure() self.statusvar = -1 del self._fpga return def _connect(self): """ Connect host PC to FPGA module with the specified serial number. """ # check if a FPGA is connected to this host PC. That method is used to determine also how # many devices are available. if not self._fpga.GetDeviceCount(): self.log.error( 'No FPGA connected to host PC or FrontPanel.exe is running.') return -1 # open a connection to the FPGA with the specified serial number self._fpga.OpenBySerial(self._serial) # upload the proper fast counter configuration bitfile to the FPGA bitfile_name = 'fastcounter_' + self._fpga_type + '.bit' # Load on the FPGA a configuration file (bit file). self._fpga.ConfigureFPGA( os.path.join(self.get_main_dir(), 'thirdparty', 'qo_fpga', bitfile_name)) # Check if the upload was successful and the Opal Kelly FrontPanel is # enabled on the FPGA if not self._fpga.IsFrontPanelEnabled(): self.log.error('Opal Kelly FrontPanel is not enabled in FPGA.\n' 'Upload of bitfile failed.') self.statusvar = -1 return -1 # Wait until all power-up initialization processes on the FPGA have finished timeout = 5 start_time = time.time() while True: if time.time() - start_time >= timeout: self.log.error( 'Power-on initialization of FPGA-timetagger timed out. ' 'Device non-functional.') self.statusvar = -1 break status_messages = self._get_status_messages() if len(status_messages) == 2 and ( 'idle_ready' in status_messages) and ('TDC_in_reset' in status_messages): self.log.info( 'Power-on initialization of FPGA-timetagger complete.') self.statusvar = 0 break time.sleep(0.2) return self.statusvar def _read_status_register(self): """ Reads the 32bit status register from the FPGA Timetagger via USB. @return: 32bit status register """ self._fpga.UpdateWireOuts() status_register = self._fpga.GetWireOutValue(0x20) return status_register def _get_status_messages(self): """ @return: """ status_register = self._read_status_register() status_messages = [] for bitmask in self._status_encoding: if (bitmask & status_register) != 0: status_messages.append(self._status_encoding[bitmask]) return status_messages def _get_error_messages(self): """ @return: """ status_register = self._read_status_register() error_messages = [] for bitmask in self._error_encoding: if (bitmask & status_register) != 0: error_messages.append(self._error_encoding[bitmask]) return error_messages def _set_dac_voltages(self): """ """ with self.threadlock: dac_sma_mapping = {1: 1, 2: 5, 3: 2, 4: 6, 5: 3, 6: 7, 7: 4, 8: 8} set_voltage_cmd = 0x03000000 for dac_chnl in range(8): sma_chnl = dac_sma_mapping[dac_chnl + 1] dac_value = int( np.rint(4096 * self._switching_voltage[sma_chnl] / (2.5 * 2))) if dac_value > 4095: dac_value = 4095 elif dac_value < 0: dac_value = 0 tmp_cmd = set_voltage_cmd + (dac_chnl << 20) + (dac_value << 8) self._fpga.SetWireInValue(0x01, tmp_cmd) self._fpga.UpdateWireIns() self._fpga.ActivateTriggerIn(0x41, 0) return def _activate_dac_ref(self): """ """ with self.threadlock: self._fpga.SetWireInValue(0x01, 0x08000001) self._fpga.UpdateWireIns() self._fpga.ActivateTriggerIn(0x41, 0) return def _reset_dac(self): """ """ with self.threadlock: self._fpga.ActivateTriggerIn(0x41, 31) return def get_constraints(self): """ Retrieve the hardware constrains from the Fast counting device. @return dict: dict with keys being the constraint names as string and items are the definition for the constaints. The keys of the returned dictionary are the str name for the constraints (which are set in this method). NO OTHER KEYS SHOULD BE INVENTED! If you are not sure about the meaning, look in other hardware files to get an impression. If still additional constraints are needed, then they have to be added to all files containing this interface. The items of the keys are again dictionaries which have the generic dictionary form: {'min': <value>, 'max': <value>, 'step': <value>, 'unit': '<value>'} Only the key 'hardware_binwidth_list' differs, since they contain the list of possible binwidths. If the constraints cannot be set in the fast counting hardware then write just zero to each key of the generic dicts. Note that there is a difference between float input (0.0) and integer input (0), because some logic modules might rely on that distinction. ALL THE PRESENT KEYS OF THE CONSTRAINTS DICT MUST BE ASSIGNED! """ constraints = dict() # the unit of those entries are seconds per bin. In order to get the # current binwidth in seonds use the get_binwidth method. constraints['hardware_binwidth_list'] = [1 / 950e6] #TODO: think maybe about a software_binwidth_list, which will postprocess the obtained # counts. These bins must be integer multiples of the current hardware_binwidth return constraints def configure(self, bin_width_s, record_length_s, number_of_gates=0): """ Configuration of the fast counter. @param float bin_width_s: Length of a single time bin in the time trace histogram in seconds. @param float record_length_s: Total length of the timetrace/each single gate in seconds. @param int number_of_gates: optional, number of gates in the pulse sequence. Ignore for not gated counter. @return tuple(binwidth_s, gate_length_s, number_of_gates): binwidth_s: float the actual set binwidth in seconds gate_length_s: the actual set gate length in seconds number_of_gates: the number of gated, which are accepted """ # Do nothing if fast counter is running if self.statusvar >= 2: binwidth_s = self._binwidth / self._internal_clock_hz gate_length_s = self._gate_length_bins * binwidth_s return binwidth_s, gate_length_s, self._number_of_gates # set class variables self._binwidth = int(np.rint(bin_width_s * self._internal_clock_hz)) # calculate the actual binwidth depending on the internal clock: binwidth_s = self._binwidth / self._internal_clock_hz self._gate_length_bins = int(np.rint(record_length_s / bin_width_s)) gate_length_s = self._gate_length_bins * binwidth_s self._number_of_gates = number_of_gates self.statusvar = 1 return binwidth_s, gate_length_s, number_of_gates def start_measure(self): """ Start the fast counter. """ self.saved_count_data = None # initialize the data array self.count_data = np.zeros( [self._number_of_gates, self._gate_length_bins]) # Start the counter. self._fpga.ActivateTriggerIn(0x40, 0) timeout = 5 start_time = time.time() while True: status_messages = self._get_status_messages() if len(status_messages) <= 2 and ('running' in status_messages): self.log.info('FPGA-timetagger measurement started.') self.statusvar = 2 break if time.time() - start_time >= timeout: self.log.error('Starting of FPGA-timetagger timed out.') break time.sleep(0.1) return self.statusvar def get_data_trace(self): """ Polls the current timetrace data from the fast counter. @return numpy.array: 2 dimensional numpy ndarray. This counter is gated. The return array has the following shape: returnarray[gate_index, timebin_index] The binning, specified by calling configure() in forehand, must be taken care of in this hardware class. A possible overflow of the histogram bins must be caught here and taken care of. """ with self.threadlock: # check for error status in FPGA timetagger error_messages = self._get_error_messages() if len(error_messages) != 0: for err_message in error_messages: self.log.error(err_message) self.stop_measure() return self.count_data # check for running status status_messages = self._get_status_messages() if len(status_messages) != 1 or ('running' not in status_messages): self.log.error( 'The FPGA is currently not running! Start the FPGA to get the data ' 'trace of the device. An empty numpy array[{0},{1}] filled with ' 'zeros will be returned.'.format(self._number_of_gates, self._gate_length_bins)) return self.count_data # initialize the read buffer for the USB transfer. # one timebin of the data to read is 32 bit wide and the data is transferred in bytes. buffersize = 128 * 1024 * 1024 # 128 MB data_buffer = bytearray(buffersize) # trigger the data read in the FPGA self._fpga.ActivateTriggerIn(0x40, 2) # Read data from FPGA read_err_code = self._fpga.ReadFromBlockPipeOut( 0xA0, 1024, data_buffer) if read_err_code != buffersize: self.log.error( 'Data transfer from FPGA via USB failed with error code {0}. ' 'Returning old count data.'.format(read_err_code)) return self.count_data # Encode bytes into 32bit unsigned integers buffer_encode = np.frombuffer(data_buffer, dtype='uint32') # Extract only the requested number of gates and gate length buffer_encode = buffer_encode.reshape( 512, 65536)[0:self._number_of_gates, 0:self._gate_length_bins] # convert into float values self.count_data = buffer_encode.astype(float, casting='safe') # Add saved count data (in case of continued measurement) if self.saved_count_data is not None: if self.saved_count_data.shape == self.count_data.shape: self.count_data = self.count_data + self.saved_count_data else: self.log.error( 'Count data before pausing measurement had different shape than ' 'after measurement. Can not properly continue measurement.' ) # bin the data according to the specified bin width #if self._binwidth != 1: # buffer_encode = buffer_encode[:(buffer_encode.size // self._binwidth) * self._binwidth].reshape(-1, self._binwidth).sum(axis=1) return self.count_data def stop_measure(self): """ Stop the fast counter. """ self.saved_count_data = None # stop FPGA timetagger self._fpga.ActivateTriggerIn(0x40, 1) # Check status and wait until stopped timeout = 5 start_time = time.time() while True: status_messages = self._get_status_messages() if len(status_messages) == 2 and ( 'idle_ready' in status_messages) and ('TDC_in_reset' in status_messages): self.log.info('FPGA-timetagger measurement stopped.') self.statusvar = 1 break if time.time() - start_time >= timeout: self.log.error('Stopping of FPGA-timetagger timed out.') break time.sleep(0.1) return self.statusvar def pause_measure(self): """ Pauses the current measurement. Fast counter must be initially in the run state to make it pause. """ # stop FPGA timetagger self.saved_count_data = self.get_data_trace() self._fpga.ActivateTriggerIn(0x40, 1) # Check status and wait until stopped timeout = 5 start_time = time.time() while True: status_messages = self._get_status_messages() if len(status_messages) == 2 and ( 'idle_ready' in status_messages) and ('TDC_in_reset' in status_messages): self.log.info('FPGA-timetagger measurement paused.') self.statusvar = 3 break if time.time() - start_time >= timeout: self.log.error('Pausing of FPGA-timetagger timed out.') break time.sleep(0.1) return self.statusvar def continue_measure(self): """ Continues the current measurement. If fast counter is in pause state, then fast counter will be continued. """ self.count_data = np.zeros( [self._number_of_gates, self._gate_length_bins]) # Check if fastcounter was in pause state if self.statusvar != 3: self.log.error( 'Can not continue fast counter since it was not in a paused state.' ) return self.statusvar # Start the counter. self._fpga.ActivateTriggerIn(0x40, 0) timeout = 5 start_time = time.time() while True: status_messages = self._get_status_messages() if len(status_messages) == 1 and ('running' in status_messages): self.log.info('FPGA-timetagger measurement started.') self.statusvar = 2 break if time.time() - start_time >= timeout: self.log.error('Starting of FPGA-timetagger timed out.') break time.sleep(0.1) return self.statusvar def is_gated(self): """ Check the gated counting possibility. @return bool: Boolean value indicates if the fast counter is a gated counter (TRUE) or not (FALSE). """ return True def get_binwidth(self): """ Returns the width of a single timebin in the timetrace in seconds. @return float: current length of a single bin in seconds (seconds/bin) """ width_in_seconds = self._binwidth / self._internal_clock_hz return width_in_seconds def get_status(self): """ Receives the current status of the Fast Counter and outputs it as return value. 0 = unconfigured 1 = idle 2 = running 3 = paused -1 = error state """ return self.statusvar
class HardwareSwitchFpga(Base, SwitchInterface): """ This is the hardware class for the Spartan-6 (Opal Kelly XEM6310) FPGA based hardware switch. The command reference for communicating via the OpalKelly Frontend can be looked up here: https://library.opalkelly.com/library/FrontPanelAPI/index.html The Frontpanel is basically a C++ interface, where a wrapper was used (SWIG) to access the dll library. Be aware that the wrapper is specified for a specific version of python (here python 3.4), and it is not guaranteed to be working with other versions. """ _modclass = 'HardwareSwitchFpga' _modtype = 'hardware' # config options _serial = ConfigOption('fpga_serial', missing='error') def on_activate(self): """ Connect and configure the access to the FPGA. """ # Create an instance of the Opal Kelly FrontPanel. The Frontpanel is a # c dll which was wrapped with SWIG for Windows type systems to be # accessed with python 3.4. You have to ensure to use the python 3.4 # version to be able to run the Frontpanel wrapper: self._fpga = ok.FrontPanel() # threading self.threadlock = Mutex() # TTL output status of the 8 channels self._switch_status = { 1: False, 2: False, 3: False, 4: False, 5: False, 6: False, 7: False, 8: False } self._connected = False # connect to the FPGA module self._connect() return def on_deactivate(self): """ Deactivate the FPGA. """ self.reset() del self._fpga self._connected = False return def _connect(self): """ Connect host PC to FPGA module with the specified serial number. """ # check if a FPGA is connected to this host PC. That method is used to # determine also how many devices are available. if not self._fpga.GetDeviceCount(): self.log.error( 'No FPGA connected to host PC or FrontPanel.exe is running.') return -1 # open a connection to the FPGA with the specified serial number self._fpga.OpenBySerial(self._serial) # upload the proper hardware switch configuration bitfile to the FPGA bitfile_name = 'switch_8chnl_withcopy_LX150.bit' # Load on the FPGA a configuration file (bit file). self._fpga.ConfigureFPGA( os.path.join(get_main_dir(), 'thirdparty', 'qo_fpga', bitfile_name)) # Check if the upload was successful and the Opal Kelly FrontPanel is enabled on the FPGA if not self._fpga.IsFrontPanelEnabled(): self.log.error('Opal Kelly FrontPanel is not enabled in FPGA') return -1 else: self._fpga.SetWireInValue(0x00, 0x00000000) self._fpga.UpdateWireIns() self._switch_status = { 0: False, 1: False, 2: False, 3: False, 4: False, 5: False, 6: False, 7: False } self._connected = True return 0 def getNumberOfSwitches(self): """ There are 8 TTL channels on the OK FPGA. Chan PIN ---------- Ch1 B14 Ch2 B16 Ch3 B12 Ch4 C7 Ch5 D15 Ch6 D10 Ch7 D9 Ch8 D11 @return int: number of switches """ return 8 def getSwitchState(self, channel): """ Gives state of switch. @param int channel: number of switch channel @return bool: True if on, False if off, None on error """ if channel not in self._switch_status: self.log.error( 'FPGA switch only accepts channel numbers 0..7. Asked for channel {0}.' ''.format(channel)) return None self._get_all_states() return self._switch_status[channel] def switchOn(self, channel): with self.threadlock: if channel not in self._switch_status: self.log.error( 'FPGA switch only accepts channel numbers 0..7. Asked for channel ' '{0}.'.format(channel)) return # determine new channels status new_state = self._switch_status.copy() new_state[channel] = True # encode channel states chnl_state = 0 for chnl in list(new_state): if new_state[chnl]: chnl_state += int(2**chnl) # apply changes in hardware self._fpga.SetWireInValue(0x00, chnl_state) self._fpga.UpdateWireIns() # get new state from hardware actual_state = self._get_all_states() if new_state != actual_state: self.log.error('Setting of channel states in hardware failed.') return def switchOff(self, channel): with self.threadlock: if channel not in self._switch_status: self.log.error( 'FPGA switch only accepts channel numbers 0..7. Asked for channel ' '{0}.'.format(channel)) return # determine new channels status new_state = self._switch_status.copy() new_state[channel] = False # encode channel states chnl_state = 0 for chnl in list(new_state): if new_state[chnl]: chnl_state += int(2**chnl) # apply changes in hardware self._fpga.SetWireInValue(0x00, chnl_state) self._fpga.UpdateWireIns() # get new state from hardware actual_state = self._get_all_states() if new_state != actual_state: self.log.error('Setting of channel states in hardware failed.') return def reset(self): """ Reset TTL outputs to zero """ with self.threadlock: if not self._connected: return self._fpga.SetWireInValue(0x00, 0) self._fpga.UpdateWireIns() self._switch_status = { 0: False, 1: False, 2: False, 3: False, 4: False, 5: False, 6: False, 7: False } return def getCalibration(self, switchNumber, state): return -1 def setCalibration(self, switchNumber, state, value): return True def getSwitchTime(self, switchNumber): """ Give switching time for switch. @param int switchNumber: number of switch @return float: time needed for switch state change """ return 100.0e-3 def _get_all_states(self): self._fpga.UpdateWireOuts() new_state = int(self._fpga.GetWireOutValue(0x20)) for chnl in list(self._switch_status): if new_state & (2**chnl) != 0: self._switch_status[chnl] = True else: self._switch_status[chnl] = False return self._switch_status
class PulseStreamer(Base, PulserInterface): """Methods to control PulseStreamer. """ _modclass = 'pulserinterface' _modtype = 'hardware' _pulsestreamer_ip = ConfigOption('pulsestreamer_ip', '192.168.1.100', missing='warn') _laser_channel = ConfigOption('laser_channel', 0, missing='warn') _uw_x_channel = ConfigOption('uw_x_channel', 2, missing='warn') def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) if 'pulsed_file_dir' in config.keys(): self.pulsed_file_dir = config['pulsed_file_dir'] if not os.path.exists(self.pulsed_file_dir): homedir = get_home_dir() self.pulsed_file_dir = os.path.join(homedir, 'pulsed_files') self.log.warning( 'The directory defined in parameter ' '"pulsed_file_dir" in the config for ' 'PulseStreamer does not exist!\n' 'The default home directory\n{0}\n will be taken ' 'instead.'.format(self.pulsed_file_dir)) else: homedir = get_home_dir() self.pulsed_file_dir = os.path.join(homedir, 'pulsed_files') self.log.warning( 'No parameter "pulsed_file_dir" was specified in the config for ' 'PulseStreamer as directory for the pulsed files!\nThe default home ' 'directory\n{0}\nwill be taken instead.'.format( self.pulsed_file_dir)) self.host_waveform_directory = self._get_dir_for_name( 'sampled_hardware_files') self.current_status = -1 self.sample_rate = 1e9 self.current_loaded_asset = None self._channel = grpc.insecure_channel(self._pulsestreamer_ip + ':50051') def on_activate(self): """ Establish connection to pulse streamer and tell it to cancel all operations """ self.pulse_streamer = pulse_streamer_pb2.PulseStreamerStub( self._channel) self.pulser_off() self.current_status = 0 def on_deactivate(self): del self.pulse_streamer def get_constraints(self): """ Retrieve the hardware constrains from the Pulsing device. @return dict: dict with constraints for the sequence generation and GUI Provides all the constraints (e.g. sample_rate, amplitude, total_length_bins, channel_config, ...) related to the pulse generator hardware to the caller. The keys of the returned dictionary are the str name for the constraints (which are set in this method). No other keys should be invented. If you are not sure about the meaning, look in other hardware files to get an impression. If still additional constraints are needed, then they have to be add to all files containing this interface. The items of the keys are again dictionaries which have the generic dictionary form: {'min': <value>, 'max': <value>, 'step': <value>, 'unit': '<value>'} Only the keys 'activation_config' and differs, since it contain the channel configuration/activation information. If the constraints cannot be set in the pulsing hardware (because it might e.g. has no sequence mode) then write just zero to each generic dict. Note that there is a difference between float input (0.0) and integer input (0). ALL THE PRESENT KEYS OF THE CONSTRAINTS DICT MUST BE ASSIGNED! """ constraints = PulserConstraints() # The file formats are hardware specific. constraints.waveform_format = ['pstream'] constraints.sequence_format = [] constraints.sample_rate.min = 1e9 constraints.sample_rate.max = 1e9 constraints.sample_rate.step = 0 constraints.sample_rate.default = 1e9 constraints.d_ch_low.min = 0.0 constraints.d_ch_low.max = 0.0 constraints.d_ch_low.step = 0.0 constraints.d_ch_low.default = 0.0 constraints.d_ch_high.min = 3.3 constraints.d_ch_high.max = 3.3 constraints.d_ch_high.step = 0.0 constraints.d_ch_high.default = 3.3 # sample file length max is not well-defined for PulseStreamer, which collates sequential identical pulses into # one. Total number of not-sequentially-identical pulses which can be stored: 1 M. constraints.sampled_file_length.min = 1 constraints.sampled_file_length.max = 134217728 constraints.sampled_file_length.step = 1 constraints.sampled_file_length.default = 1 # the name a_ch<num> and d_ch<num> are generic names, which describe UNAMBIGUOUSLY the # channels. Here all possible channel configurations are stated, where only the generic # names should be used. The names for the different configurations can be customary chosen. activation_config = OrderedDict() activation_config['all'] = [ 'd_ch1', 'd_ch2', 'd_ch3', 'd_ch4', 'd_ch5', 'd_ch6', 'd_ch7', 'd_ch8' ] constraints.activation_config = activation_config return constraints def pulser_on(self): """ Switches the pulsing device on. @return int: error code (0:OK, -1:error) """ # start the pulse sequence self.pulse_streamer.stream(self._sequence) self.log.info('Asset uploaded to PulseStreamer') self.pulse_streamer.startNow(pulse_streamer_pb2.VoidMessage()) self.current_status = 1 return 0 def pulser_off(self): """ Switches the pulsing device off. @return int: error code (0:OK, -1:error) """ # stop the pulse sequence channels = self._convert_to_bitmask( [self._laser_channel, self._uw_x_channel]) self.pulse_streamer.constant( pulse_streamer_pb2.PulseMessage(ticks=0, digi=channels, ao0=0, ao1=0)) self.current_status = 0 return 0 def upload_asset(self, asset_name=None): """ Upload an already hardware conform file to the device. Does NOT load it into channels. @param name: string, name of the ensemble/seqeunce to be uploaded @return int: error code (0:OK, -1:error) """ self.log.debug( 'PulseStreamer has no own storage capability.\n"upload_asset" call ignored.' ) return 0 def load_asset(self, asset_name, load_dict=None): """ Loads a sequence or waveform to the specified channel of the pulsing device. @param str asset_name: The name of the asset to be loaded @param dict load_dict: a dictionary with keys being one of the available channel numbers and items being the name of the already sampled waveform/sequence files. Examples: {1: rabi_Ch1, 2: rabi_Ch2} {1: rabi_Ch2, 2: rabi_Ch1} This parameter is optional. If none is given then the channel association is invoked from the sequence generation, i.e. the filename appendix (_Ch1, _Ch2 etc.) @return int: error code (0:OK, -1:error) """ # ignore if no asset_name is given if asset_name is None: self.log.warning('"load_asset" called with asset_name = None.') return 0 # check if asset exists saved_assets = self.get_saved_asset_names() if asset_name not in saved_assets: self.log.error( 'No asset with name "{0}" found for PulseStreamer.\n' '"load_asset" call ignored.'.format(asset_name)) return -1 # get samples from file filepath = os.path.join(self.host_waveform_directory, asset_name + '.pstream') pulse_sequence_raw = dill.load(open(filepath, 'rb')) pulse_sequence = [] for pulse in pulse_sequence_raw: pulse_sequence.append( pulse_streamer_pb2.PulseMessage(ticks=pulse[0], digi=pulse[1], ao0=0, ao1=1)) blank_pulse = pulse_streamer_pb2.PulseMessage(ticks=0, digi=0, ao0=0, ao1=0) laser_on = pulse_streamer_pb2.PulseMessage( ticks=0, digi=self._convert_to_bitmask([self._laser_channel]), ao0=0, ao1=0) laser_and_uw_channels = self._convert_to_bitmask( [self._laser_channel, self._uw_x_channel]) laser_and_uw_on = pulse_streamer_pb2.PulseMessage( ticks=0, digi=laser_and_uw_channels, ao0=0, ao1=0) self._sequence = pulse_streamer_pb2.SequenceMessage( pulse=pulse_sequence, n_runs=0, initial=laser_on, final=laser_and_uw_on, underflow=blank_pulse, start=1) self.current_loaded_asset = asset_name return 0 def clear_all(self): """ Clears all loaded waveforms from the pulse generators RAM. @return int: error code (0:OK, -1:error) Unused for digital pulse generators without storage capability (PulseBlaster, FPGA). """ return 0 def get_status(self): """ Retrieves the status of the pulsing hardware @return (int, dict): tuple with an interger value of the current status and a corresponding dictionary containing status description for all the possible status variables of the pulse generator hardware. """ status_dic = dict() status_dic[-1] = 'Failed Request or Failed Communication with device.' status_dic[0] = 'Device has stopped, but can receive commands.' status_dic[1] = 'Device is active and running.' return self.current_status, status_dic def get_sample_rate(self): """ Get the sample rate of the pulse generator hardware @return float: The current sample rate of the device (in Hz) Do not return a saved sample rate in a class variable, but instead retrieve the current sample rate directly from the device. """ return self.sample_rate def set_sample_rate(self, sample_rate): """ Set the sample rate of the pulse generator hardware. @param float sample_rate: The sampling rate to be set (in Hz) @return float: the sample rate returned from the device. Note: After setting the sampling rate of the device, retrieve it again for obtaining the actual set value and use that information for further processing. """ self.log.debug('PulseStreamer sample rate cannot be configured') return self.sample_rate def get_analog_level(self, amplitude=None, offset=None): """ Retrieve the analog amplitude and offset of the provided channels. @param list amplitude: optional, if a specific amplitude value (in Volt peak to peak, i.e. the full amplitude) of a channel is desired. @param list offset: optional, if a specific high value (in Volt) of a channel is desired. @return: (dict, dict): tuple of two dicts, with keys being the channel number and items being the values for those channels. Amplitude is always denoted in Volt-peak-to-peak and Offset in (absolute) Voltage. """ return {}, {} def set_analog_level(self, amplitude=None, offset=None): """ Set amplitude and/or offset value of the provided analog channel. @param dict amplitude: dictionary, with key being the channel and items being the amplitude values (in Volt peak to peak, i.e. the full amplitude) for the desired channel. @param dict offset: dictionary, with key being the channel and items being the offset values (in absolute volt) for the desired channel. @return (dict, dict): tuple of two dicts with the actual set values for amplitude and offset. If nothing is passed then the command will return two empty dicts. """ return {}, {} def get_digital_level(self, low=None, high=None): """ Retrieve the digital low and high level of the provided channels. @param list low: optional, if a specific low value (in Volt) of a channel is desired. @param list high: optional, if a specific high value (in Volt) of a channel is desired. @return: (dict, dict): tuple of two dicts, with keys being the channel number and items being the values for those channels. Both low and high value of a channel is denoted in (absolute) Voltage. Note: Do not return a saved low and/or high value but instead retrieve the current low and/or high value directly from the device. If no entries provided then the levels of all channels where simply returned. If no digital channels provided, return just an empty dict. Example of a possible input: low = [1,4] to obtain the low voltage values of digital channel 1 an 4. A possible answer might be {1: -0.5, 4: 2.0} {} since no high request was performed. The major difference to analog signals is that digital signals are either ON or OFF, whereas analog channels have a varying amplitude range. In contrast to analog output levels, digital output levels are defined by a voltage, which corresponds to the ON status and a voltage which corresponds to the OFF status (both denoted in (absolute) voltage) In general there is no bijective correspondence between (amplitude, offset) and (value high, value low)! """ if low is None: low = [] if high is None: high = [] low_dict = {} high_dict = {} if low is [] and high is []: for channel in range(8): low_dict[channel] = 0.0 high_dict[channel] = 3.3 else: for channel in low: low_dict[channel] = 0.0 for channel in high: high_dict[channel] = 3.3 return low_dict, high_dict def set_digital_level(self, low=None, high=None): """ Set low and/or high value of the provided digital channel. @param dict low: dictionary, with key being the channel and items being the low values (in volt) for the desired channel. @param dict high: dictionary, with key being the channel and items being the high values (in volt) for the desired channel. @return (dict, dict): tuple of two dicts where first dict denotes the current low value and the second dict the high value. If nothing is passed then the command will return two empty dicts. Note: After setting the high and/or low values of the device, retrieve them again for obtaining the actual set value(s) and use that information for further processing. The major difference to analog signals is that digital signals are either ON or OFF, whereas analog channels have a varying amplitude range. In contrast to analog output levels, digital output levels are defined by a voltage, which corresponds to the ON status and a voltage which corresponds to the OFF status (both denoted in (absolute) voltage) In general there is no bijective correspondence between (amplitude, offset) and (value high, value low)! """ if low is None: low = {} if high is None: high = {} self.log.warning('PulseStreamer logic level cannot be adjusted!') return 0 def get_active_channels(self, ch=None): if ch is None: ch = {} d_ch_dict = {} if len(ch) < 1: for chnl in range(1, 9): d_ch_dict['d_ch{0}'.format(chnl)] = True else: for channel in ch: d_ch_dict[channel] = True return d_ch_dict def set_active_channels(self, ch=None): if ch is None: ch = {} d_ch_dict = { 'd_ch1': True, 'd_ch2': True, 'd_ch3': True, 'd_ch4': True, 'd_ch5': True, 'd_ch6': True, 'd_ch7': True, 'd_ch8': True } return d_ch_dict def get_loaded_asset(self): """ Retrieve the currently loaded asset name of the device. @return str: Name of the current asset, that can be either a filename a waveform, a sequence ect. """ return self.current_loaded_asset def get_uploaded_asset_names(self): """ Retrieve the names of all uploaded assets on the device. @return list: List of all uploaded asset name strings in the current device directory. This is no list of the file names. Unused for digital pulse generators without sequence storage capability (PulseBlaster, FPGA). """ names = [] return names def get_saved_asset_names(self): """ Retrieve the names of all sampled and saved assets on the host PC. This is no list of the file names. @return list: List of all saved asset name strings in the current directory of the host PC. """ file_list = self._get_filenames_on_host() saved_assets = [] for filename in file_list: if filename.endswith('.pstream'): asset_name = filename.rsplit('.', 1)[0] if asset_name not in saved_assets: saved_assets.append(asset_name) return saved_assets def delete_asset(self, asset_name): """ Delete all files associated with an asset with the passed asset_name from the device memory. @param str asset_name: The name of the asset to be deleted Optionally a list of asset names can be passed. @return int: error code (0:OK, -1:error) Unused for digital pulse generators without sequence storage capability (PulseBlaster, FPGA). """ return 0 def set_asset_dir_on_device(self, dir_path): """ Change the directory where the assets are stored on the device. @param str dir_path: The target directory @return int: error code (0:OK, -1:error) Unused for digital pulse generators without changeable file structure (PulseBlaster, FPGA). """ return 0 def get_asset_dir_on_device(self): """ Ask for the directory where the hardware conform files are stored on the device. @return str: The current file directory Unused for digital pulse generators without changeable file structure (PulseBlaster, FPGA). """ return '' def get_interleave(self): """ Check whether Interleave is ON or OFF in AWG. @return bool: True: ON, False: OFF Unused for pulse generator hardware other than an AWG. """ return False def set_interleave(self, state=False): """ Turns the interleave of an AWG on or off. @param bool state: The state the interleave should be set to (True: ON, False: OFF) @return bool: actual interleave status (True: ON, False: OFF) Note: After setting the interleave of the device, retrieve the interleave again and use that information for further processing. Unused for pulse generator hardware other than an AWG. """ return False def tell(self, command): """ Sends a command string to the device. @param string command: string containing the command @return int: error code (0:OK, -1:error) """ return 0 def ask(self, question): """ Asks the device a 'question' and receive and return an answer from it. a @param string question: string containing the command @return string: the answer of the device to the 'question' in a string """ return '' def reset(self): """ Reset the device. @return int: error code (0:OK, -1:error) """ channels = self._convert_to_bitmask( [self._laser_channel, self._uw_x_channel]) self.pulse_streamer.constant( pulse_streamer_pb2.PulseMessage(ticks=0, digi=channels, ao0=0, ao1=0)) self.pulse_streamer.constant(laser_on) return 0 def has_sequence_mode(self): """ Asks the pulse generator whether sequence mode exists. @return: bool, True for yes, False for no. """ return False def _get_dir_for_name(self, name): """ Get the path to the pulsed sub-directory 'name'. @param name: string, name of the folder @return: string, absolute path to the directory with folder 'name'. """ path = os.path.join(self.pulsed_file_dir, name) if not os.path.exists(path): os.makedirs(os.path.abspath(path)) return os.path.abspath(path) def _get_filenames_on_host(self): """ Get the full filenames of all assets saved on the host PC. @return: list, The full filenames of all assets saved on the host PC. """ filename_list = [ f for f in os.listdir(self.host_waveform_directory) if f.endswith('.pstream') ] return filename_list def _convert_to_bitmask(self, active_channels): """ Convert a list of channels into a bitmask. @param numpy.array active_channels: the list of active channels like e.g. [0,4,7]. Note that the channels start from 0. @return int: The channel-list is converted into a bitmask (an sequence of 1 and 0). The returned integer corresponds to such a bitmask. Note that you can get a binary representation of an integer in python if you use the command bin(<integer-value>). All higher unneeded digits will be dropped, i.e. 0b00100 is turned into 0b100. Examples are bin(0) = 0b0 bin(1) = 0b1 bin(8) = 0b1000 Each bit value (read from right to left) corresponds to the fact that a channel is on or off. I.e. if you have 0b001011 then it would mean that only channel 0, 1 and 3 are switched to on, the others are off. Helper method for write_pulse_form. """ bits = 0 # that corresponds to: 0b0 for channel in active_channels: # go through each list element and create the digital word out of # 0 and 1 that represents the channel configuration. In order to do # that a bitwise shift to the left (<< operator) is performed and # the current channel configuration is compared with a bitwise OR # to check whether the bit was already set. E.g.: # 0b1001 | 0b0110: compare elementwise: # 1 | 0 => 1 # 0 | 1 => 1 # 0 | 1 => 1 # 1 | 1 => 1 # => 0b1111 bits = bits | (1 << channel) return bits
class MillenniaeVLaser(Base, SimpleLaserInterface): """ Spectra Physics Millennia diode pumped solid state laser """ _modclass = 'millenniaevlaser' _modtype = 'hardware' serial_interface = ConfigOption('interface', 'ASRL1::INSTR', missing='warn') maxpower = ConfigOption('maxpower', 25.0, missing='warn') def on_activate(self): """ Activate Module. """ self.connect_laser(self.serial_interface) def on_deactivate(self): """ Deactivate module """ self.disconnect_laser() def connect_laser(self, interface): """ Connect to Instrument. @param str interface: visa interface identifier @return bool: connection success """ try: self.rm = visa.ResourceManager() rate = 115200 self.inst = self.rm.open_resource( interface, baud_rate=rate, write_termination='\n', read_termination='\n', send_end=True) self.inst.timeout = 1000 idn = self.inst.query('*IDN?') (self.mfg, self.model, self.serial, self.version) = idn.split(',') except visa.VisaIOError as e: self.log.exception('Communication Failure:') return False else: return True def disconnect_laser(self): """ Close the connection to the instrument. """ self.inst.close() self.rm.close() def allowed_control_modes(self): """ Control modes for this laser @return ControlMode: available control modes """ return [ControlMode.MIXED] def get_control_mode(self): """ Get active control mode @return ControlMode: active control mode """ return ControlMode.MIXED def set_control_mode(self, mode): """ Set actve control mode @param ControlMode mode: desired control mode @return ControlMode: actual control mode """ return ControlMode.MIXED def get_power(self): """ Current laser power @return float: laser power in watts """ answer = self.inst.query('?P') return float(answer) def get_power_setpoint(self): """ Current laser power setpoint @return float: power setpoint in watts """ answer = self.inst.query('?PSET') return float(answer) def get_power_range(self): """ Laser power range @return (float, float): laser power range """ return 0, self.maxpower def set_power(self, power): """ Set laser power setpoint @param float power: desired laser power @return float: actual laser power setpoint """ self.inst.query('P:{0:f}'.format(power)) return self.get_power_setpoint() def get_current_unit(self): """ Get unit for current return str: unit for laser current """ return 'A' def get_current_range(self): """ Get range for laser current @return (float, float): range for laser current """ maxcurrent = float(self.inst.query('?DCL')) return (0, maxcurrent) def get_current(self): """ Get current laser current @return float: current laser current """ return float(self.inst.query('?C1')) def get_current_setpoint(self): """ Get laser current setpoint @return float: laser current setpoint """ return float(self.inst.query('?CS1')) def set_current(self, current_percent): """ Set laser current setpoint @param float current_percent: desired laser current setpoint @return float: actual laer current setpoint """ self.inst.query('C:{0}'.format(current_percent)) return self.get_current_setpoint() def get_shutter_state(self): """ Get laser shutter state @return ShutterState: current laser shutter state """ state = self.inst.query('?SHT') if 'OPEN' in state: return ShutterState.OPEN elif 'CLOSED' in state: return ShutterState.CLOSED else: return ShutterState.UNKNOWN def set_shutter_state(self, state): """ Set laser shutter state. @param ShuterState state: desired laser shutter state @return ShutterState: actual laser shutter state """ actstate = self.get_shutter_state() if state != actstate: if state == ShutterState.OPEN: self.inst.query('SHT:1') elif state == ShutterState.CLOSED: self.inst.query('SHT:0') return self.get_shutter_state() def get_crystal_temperature(self): """ Get SHG crystal temerpature. @return float: SHG crystal temperature in degrees Celsius """ return float(self.inst.query('?SHG')) def get_diode_temperature(self): """ Get laser diode temperature. @return float: laser diode temperature in degrees Celsius """ return float(self.inst.query('?T')) def get_tower_temperature(self): """ Get SHG tower temperature @return float: SHG tower temperature in degrees Celsius """ return float(self.inst.query('?TT')) def get_cab_temperature(self): """ Get cabinet temperature @return float: get laser cabinet temperature in degrees Celsius """ return float(self.inst.query('?CABTEMP')) def get_temperatures(self): """ Get all available temperatures @return dict: tict of temperature names and values """ return { 'crystal': self.get_crystal_temperature(), 'diode': self.get_diode_temperature(), 'tower': self.get_tower_temperature(), 'cab': self.get_cab_temperature(), } def set_temperatures(self, temps): """ Set temperatures for lasers wth tunable temperatures """ return {} def get_temperature_setpoints(self): """ Get tepmerature setpoints. @return dict: setpoint name and value """ shgset = int(self.inst.query('?SHGS')) return {'shg': shgset} def get_laser_state(self): """ Get laser state. @return LaserState: current laser state """ diode = int(self.inst.query('?D')) state = self.inst.query('?F') if state in ('SYS ILK', 'KEY ILK'): return LaserState.LOCKED elif state == 'SYSTEM OK': if diode == 1: return LaserState.ON elif diode == 0: return LaserState.OFF else: return LaserState.UNKNOWN else: return LaserState.UNKNOWN def set_laser_state(self, status): """ Set laser state @param LaserState status: desited laser state @return LaserState: actual laser state """ actstat = self.get_laser_state() if actstat != status: if status == LaserState.ON: self.inst.query('ON') elif status == LaserState.OFF: self.inst.query('OFF') return self.get_laser_state() def on(self): """ Turn laser on. @return LaserState: actual laser state """ return self.set_laser_state(LaserState.ON) def off(self): """ Turn laser off. @return LaserState: actual laser state """ return self.set_laser_state(LaserState.OFF) def dump(self): """ Dump laser information. @return str: laser information """ lines = '' lines += 'Didoe Serial: {0}\n'.format(self.inst.query('?DSN')) return lines def timers(self): """ Laser component runtimes @return str: laser component run times """ lines = '' lines += 'Diode ON: {0}\n'.format(self.inst.query('?DH')) lines += 'Head ON: {0}\n'.format(self.inst.query('?HEADHRS')) lines += 'PSU ON: {0}\n'.format(self.inst.query('?PSHRS')) return lines def get_extra_info(self): """ Formatted information about the laser. @return str: Laser information """ extra = '' extra += '{0}\n{1}\n{2}\n{3}\n'.format(self.mfg, self.model, self.serial, self.version) extra += '\n' extra += '\n {0}'.format(self.timers()) extra += '\n' return extra
class AutocorrelationTimetagger(Base, AutocorrelationInterface): """unstable: Jan Kurzhals """ _modclass = 'autocorrelationinterface' _modtype = 'hardware' _channel_apd_0 = ConfigOption('timetagger_channel_apd_0', missing='error') _channel_apd_1 = ConfigOption('timetagger_channel_apd_1', missing='error') def on_activate(self): """ Initialisation performed during activation of the module. """ self._count_length = int(10) self._bin_width = 1 # bin width in ps self._tagger = tt.createTimeTagger() self._tagger.reset() self.correlation = None self.statusvar = 0 def on_deactivate(self): """ Deactivate the FPGA. """ if self.getState() == 'locked': self.correlation.stop() self.correlation.clear() self.correlation = None def get_constraints(self): """ Get hardware limits of NI device. @return SlowCounterConstraints: constraints class for slow counter FIXME: ask hardware for limits when module is loaded """ constraints = AutocorrelationConstraints() constraints.max_channels = 2 constraints.min_channels = 2 constraints.min_count_length = 1 constraints.min_bin_width = 100 return constraints def configure(self, bin_width, count_length): """ Configuration of the fast counter. @param float bin_width: Length of a single time bin in the time trace histogram in picoseconds. @param float count_length: Total number of bins. @return tuple(bin_width, count_length): bin_width: float the actual set binwidth in picoseconds count_length: actual number of bins """ self._bin_width = bin_width self._count_length = count_length self.statusvar = 1 if self.correlation != None: self._reset_hardware() if self._tagger == None: return -1 else: try: self.correlation = tt.Correlation( tagger=self._tagger, channel_1=self._channel_apd_0, channel_2=self._channel_apd_1, binwidth=self._bin_width, n_bins=self._count_length) self.correlation.stop() return 0 except: return -1 def _reset_hardware(self): self.correlation.clear() return 0 def get_status(self): """ Receives the current status of the Fast Counter and outputs it as return value. 0 = unconfigured 1 = idle 2 = running 3 = paused -1 = error state """ return self.statusvar def start_measure(self): """ Start the fast counter. """ if self.getState() != 'locked': self.lock() self.correlation.clear() self.correlation.start() self.statusvar = 2 return 0 def stop_measure(self): """ Stop the fast counter. """ if self.getState() == 'locked': self.correlation.stop() self.unlock() self.statusvar = 1 return 0 def pause_measure(self): """ Pauses the current measurement. Fast counter must be initially in the run state to make it pause. """ if self.getState() == 'locked': self.correlation.stop() self.statusvar = 3 return 0 def continue_measure(self): """ Continues the current measurement. If fast counter is in pause state, then fast counter will be continued. """ if self.getState() == 'locked': self.correlation.start() self.statusvar = 2 return 0 def get_bin_width(self): """ Returns the width of a single timebin in the timetrace in picoseconds. @return float: current length of a single bin in seconds (seconds/bin) """ return self._bin_width def get_count_length(self): """ Returns the number of time bins. @return float: number of bins """ return (2 * self._count_length + 1) def get_data_trace(self): """ @return numpy.array: onedimensional array of dtype = int64. Size of array is determined by 2*count_length+1 """ correlation_data = np.array(self.correlation.getData(), dtype='int32') return correlation_data def get_normalized_data_trace(self): """ @return numpy.array: onedimensional array of dtype = int64 normalized according to https://www.physi.uni-heidelberg.de/~schmiedm/seminar/QIPC2002/SinglePhotonSource/SolidStateSingPhotSource_PRL85(2000).pdf Size of array is determined by 2*count_length+1 """ return np.array(self.correlation.getDataNormalized(), dtype='int32')