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
Exemplo n.º 2
0
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'
Exemplo n.º 3
0
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)
Exemplo n.º 5
0
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
Exemplo n.º 6
0
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}
Exemplo n.º 7
0
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)
Exemplo n.º 8
0
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
Exemplo n.º 9
0
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
Exemplo n.º 10
0
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
Exemplo n.º 11
0
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
Exemplo n.º 12
0
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))
Exemplo n.º 13
0
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
Exemplo n.º 15
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
Exemplo n.º 16
0
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
Exemplo n.º 17
0
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
Exemplo n.º 18
0
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
Exemplo n.º 20
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
Exemplo n.º 21
0
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
Exemplo n.º 23
0
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
Exemplo n.º 24
0
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
Exemplo n.º 25
0
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
Exemplo n.º 27
0
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
Exemplo n.º 28
0
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
Exemplo n.º 29
0
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')