Example #1
0
class LaserExtCtrlLogic(GenericLogic):

    laser = Connector(interface='ExtCtrlLaserInterface')
    option_placeholder = ConfigOption('option_placeholder', 0)

    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.info('{0}: {1}'.format(key, config[key]))
        """
        self.threadlock = Mutex()

    def on_activate(self):
        self._laser = self.laser()

    def on_deactivate(self):
        pass

    def get_power_atsource(self):
        return self._laser.get_power()

    def set_ext_ctrl_power(self, power_percentage):
        self._laser.set_power_extctrl(power_percentage)

    def get_power_atpd(self):
        pass
Example #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'

    """

    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'
Example #3
0
class MimicGui(GUIBase):

    xseries = Connector(interface='NationalInstrumentsXSeries')

    channel = ConfigOption('channel', '/Dev1/PCIe-6363', missing='warn')

    def __init__(self, config, **kwargs):
        super().__init__(config=config, **kwargs)

    def on_activate(self):
        """ Definition and initialisation of the GUI.
        """

        self.nicard = self.xseries()

        # Create main window instance
        self._mw = MimicMainWindow()

        this_dir = os.path.dirname(__file__)

        self.fibre_sw_on_pixmap = QtGui.QPixmap(
            os.path.join(this_dir, "fibre_switch_on.png"))

        self.fibre_sw_off_pixmap = QtGui.QPixmap(
            os.path.join(this_dir, "fibre_switch_off.png"))

        # Initialise module with fibre switch off
        self.fibre_switch_off()

        ###################
        # Connect UI events
        ###################

        self._mw.fibre_sw_off_btn.clicked.connect(self.fibre_switch_off)
        self._mw.fibre_sw_on_btn.clicked.connect(self.fibre_switch_on)

    def show(self):
        """Make window visible and put it above all other windows.
        """
        QtWidgets.QMainWindow.show(self._mw)
        self._mw.activateWindow()
        self._mw.raise_()

    def on_deactivate(self):
        # FIXME: !
        """ Deactivate the module
        """
        self._mw.close()

    def fibre_switch_off(self):
        self._mw.fibre_sw_mimic.setPixmap(self.fibre_sw_off_pixmap)
        self.nicard.digital_channel_switch(self.channel, False)

    def fibre_switch_on(self):
        self._mw.fibre_sw_mimic.setPixmap(self.fibre_sw_on_pixmap)
        self.nicard.digital_channel_switch(self.channel, True)
Example #4
0
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)
Example #5
0
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

    """
    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
Example #6
0
class ArduinoUnoR3(Base, ArduinoInterface):
    """
    Example config:
        voltage_generator:
            module.Class: 'power_supply.power_supply_dummy.PowerSupplyDummy'
            voltage_min: 0
            voltage_max_1: 30
            voltage_max_2: 30
            voltage_max_3: 5
            current_max: 3

    """
    _usb_address = ConfigOption('usb_address', missing='error')
    _baud_rate = ConfigOption('baud_rate', missing='error')

    def on_activate(self):
        """
        Initialisation performed during activation of the module.
        """
        self.rm = visa.ResourceManager()
        try:
            self._serialcomm = serial.Serial('COM5', 115200)
            self._serialcomm.timeout = 0.05
        except:
            self.log.error('Could not connect to port "{}". Check '
                           'whether address exists and reload '
                           'module!'.format(self._usb_address))
            raise


    def on_deactivate(self):
        """ Deinitialisation performed during deactivation of the module.
        """
        self._serialcomm.close()

    def write_and_read(self, text):
        """
        pass string to arduino and receive return
        """
        self._serialcomm.write(text.encode())
        time.sleep(0.1)
        output = self._serialcomm.readline().decode('ascii')
        return output
Example #7
0
class OptimizerLogic(GenericLogic):

    """Helper module to simplify running survey workflow scripts in the background.
        Works in cooperatively with Jupyter"""

    # Connect to everything this module has helper methods for
    # (intended as a local module although in some form this could be made configurable)
    aom = Connector(interface='aomlogic')
    hbt = Connector(interface='hbtlogic')
    poimanager = Connector(interface='poimanagerlogic')
    pulsedmaster = Connector(interface='pulsedmasterlogic')
    optimizer = Connector(interface='optimizerlogic')

    working_directory = ConfigOption('working_directory')

    _sigInternal = QtCore.Signal()


    def __init__(self, config, **kwargs):
        super().__init__(config=config, **kwargs)

        self._running = False   # Is a workflow running
        self._subdir = None

    def on_activate(self):
        """ Initialisation performed during activation of the module.

        @return int: error code (0:OK, -1:error)
        """
        return 0


    def on_deactivate(self):
        """ Reverse steps of activation

        @return int: error code (0:OK, -1:error)
        """
        return 0

    def run_in_background(self, code):
        # set up a namespace for the sub thread


    def wlog(self, msg):
        # log the workflow progress to a separate logfile
        out = _time_str() + ' ' + msg
        with open(self._log_file, 'a') as f:
            f.write(out + '\n')
        self.log.debug(msg)

    def set_laser_power(self, power):
        self.wlog("Setting power via AOM to {} mW".format(power))
        self.aom().set_power(power * 1e-3)
class SlackNotifierLogic(GenericLogic):
    """Notifier logic for Slack.

    Provides the `send_message` method that can be called
    in scripts or other modules.

    Config for copy-paste:

    slacknotifier:
        module.Class: 'slack_notifier_logic.SlackNotifierLogic'
        api_key: '<Slack bot API OAuth key>'
        channel: '#channel-to-message'
    """

    # Config options
    _api_key = ConfigOption('api_key', missing='error')
    _channel = ConfigOption('channel', missing='error')

    def on_activate(self):
        pass

    def on_deactivate(self):
        pass

    def send_message(self, message):
        """Send message to Slack

        @param str message: Message to post.
        """
        try:
            requests.post('https://slack.com/api/chat.postMessage', {
                        'token':self._api_key,
                        'channel':self._channel,
                        'text':message
                        }).json()

            self.log.info('{} posted to Slack channel {}'.format(message, self._channel))

        except Exception as err:
            self.log.error('Error posting Slack message: {}'.format(err))
class ANCController(Base):
    _modclass = 'attocube_anc350'
    _modtype = 'hardware'

    _dll_location = ConfigOption('dll_location', missing='error')
    _sample_controller_number = ConfigOption("sample_controller_number",
                                             0,
                                             missing='error')
    _tip_controller_number = ConfigOption("tip_controller_number",
                                          1,
                                          missing='error')
    tip = None
    sample = None

    def on_activate(self):
        self.tip = ANC350(self._dll_location, self._tip_controller_number)
        self.sample = ANC350(self._dll_location,
                             self._sample_controller_number)

    def on_deactivate(self):
        self.tip.close()
        self.sample.close()
Example #10
0
class PiezoDummy(Base, PiezoInterface):
    """ Dummy for piezo interface

    Example config for copy-paste:

    piezo_dummy:
        module.Class: 'piezo.piezo_dummy.PiezoDummy'
        step: 0.01 # in µm
    """

    step = ConfigOption('step', 0.01)

    def on_activate(self):
        """ Initialisation performed during activation of the module.
        """
        pass

    def on_deactivate(self):
        """ Deinitialisation performed during deactivation of the module.
        """
        pass

    def get_position(self):
        """ Retrieves the current position
        
        @returns: float pos: simulated position
        """
        pos = np.random.normal()
        return pos

    def set_step(self, step):
        """ sets the step entered on the GUI by the user
        
        @returns: None
        """
        self.step = step
class ODMRLogic(GenericLogic):
    """This is the Logic class for ODMR."""

    # declare connectors
    odmrcounter = Connector(interface='ODMRCounterInterface')
    fitlogic = Connector(interface='FitLogic')
    microwave1 = Connector(interface='MicrowaveInterface')
    savelogic = Connector(interface='SaveLogic')
    taskrunner = Connector(interface='TaskRunner')
    # Connecting to camera logic
    camera = Connector(interface='CameraLogic')

    # config option
    mw_scanmode = ConfigOption(
        'scanmode',
        'LIST',
        missing='warn',
        converter=lambda x: MicrowaveMode[x.upper()])
    # Default clock frequency is set dependant on exp. time. here f is in
    # milliseconds.
    f = 1
    exp_time = StatusVar('exp_time', f)
    clock_frequency = StatusVar('clock_frequency', 1. / ((f / 1000.) + 0.0))
    cw_mw_frequency = StatusVar('cw_mw_frequency', 2870e6)
    cw_mw_power = StatusVar('cw_mw_power', -30)
    sweep_mw_power = StatusVar('sweep_mw_power', -30)
    mw_start = StatusVar('mw_start', 2800e6)
    mw_stop = StatusVar('mw_stop', 2950e6)
    mw_step = StatusVar('mw_step', 2e6)
    run_time = StatusVar('run_time', 60)
    number_of_lines = StatusVar('number_of_lines', 50)
    fc = StatusVar('fits', None)
    lines_to_average = StatusVar('lines_to_average', 0)
    _oversampling = StatusVar('oversampling', default=10)
    _lock_in_active = StatusVar('lock_in_active', default=False)

    # Internal signals
    sigNextLine = QtCore.Signal()

    # Update signals, e.g. for GUI module
    sigParameterUpdated = QtCore.Signal(dict)
    sigOutputStateUpdated = QtCore.Signal(str, bool)
    sigOdmrPlotsUpdated = QtCore.Signal(np.ndarray, np.ndarray, np.ndarray)
    sigOdmrFitUpdated = QtCore.Signal(np.ndarray, np.ndarray, dict, str)
    sigOdmrElapsedTimeUpdated = QtCore.Signal(float, int)

    def __init__(self, config, **kwargs):
        super().__init__(config=config, **kwargs)
        self.threadlock = Mutex()

    def on_activate(self):
        """
        Initialisation performed during activation of the module.
        """
        # Get connectors
        self._mw_device = self.microwave1()
        self._fit_logic = self.fitlogic()
        self._odmr_counter = self.odmrcounter()
        self._save_logic = self.savelogic()
        self._taskrunner = self.taskrunner()
        self._camera = self.camera()

        # Get hardware constraints
        limits = self.get_hw_constraints()

        # Set/recall microwave source parameters
        self.cw_mw_frequency = limits.frequency_in_range(self.cw_mw_frequency)
        self.cw_mw_power = limits.power_in_range(self.cw_mw_power)
        self.sweep_mw_power = limits.power_in_range(self.sweep_mw_power)
        self.mw_start = limits.frequency_in_range(self.mw_start)
        self.mw_stop = limits.frequency_in_range(self.mw_stop)
        self.mw_step = limits.list_step_in_range(self.mw_step)
        # self._odmr_counter.oversampling = self._oversampling
        # self._odmr_counter.lock_in_active = self._lock_in_active

        # Set the trigger polarity (RISING/FALLING) of the mw-source input trigger
        # theoretically this can be changed, but the current counting scheme
        # will not support that
        self.mw_trigger_pol = TriggerEdge.RISING
        self.set_trigger(self.mw_trigger_pol, self.clock_frequency)

        # Elapsed measurement time and number of sweeps
        self.elapsed_time = 0.0
        self.elapsed_sweeps = 0

        # Set flags
        # for stopping a measurement
        self._stopRequested = False
        # for clearing the ODMR data during a measurement
        self._clearOdmrData = False

        # Initalize the ODMR data arrays (mean signal and sweep matrix)
        self._initialize_odmr_plots()
        # Raw data array
        self.odmr_raw_data = np.zeros(
            [self.number_of_lines,
             len(self._odmr_counter.get_odmr_channels()),
             self.odmr_plot_x.size]
        )
        # The array for images of the entire sweep is intialized.
        self.sweep_images = np.zeros(
            (self.odmr_plot_x.size, *np.flip(self._camera.get_size(), axis=0))
        )
        # Switch off microwave and set CW frequency and power
        self.mw_off()
        self.set_cw_parameters(self.cw_mw_frequency, self.cw_mw_power)

        # Connect signals
        self.sigNextLine.connect(
            self._scan_odmr_line,
            QtCore.Qt.QueuedConnection)
        return

    def on_deactivate(self):
        """ Deinitialisation performed during deactivation of the module.
        """
        # Stop measurement if it is still running
        if self.module_state() == 'locked':
            self.stop_odmr_scan()
        timeout = 30.0
        start_time = time.time()
        while self.module_state() == 'locked':
            time.sleep(0.5)
            timeout -= (time.time() - start_time)
            if timeout <= 0.0:
                self.log.error(
                    'Failed to properly deactivate odmr logic. Odmr scan is still '
                    'running but can not be stopped after 30 sec.')
                break
        # Switch off microwave source for sure (also if CW mode is active or
        # module is still locked)
        self._mw_device.off()
        # The camera's deactivate function is called as well.
        self._camera.on_deactivate()
        # Disconnect signals
        self.sigNextLine.disconnect()

    @fc.constructor
    def sv_set_fits(self, val):
        # Setup fit container
        fc = self.fitlogic().make_fit_container('ODMR sum', '1d')
        fc.set_units(['Hz', 'contrast'])
        if isinstance(val, dict) and len(val) > 0:
            fc.load_from_dict(val)
        else:
            d1 = OrderedDict()
            d1['Lorentzian dip'] = {
                'fit_function': 'lorentzian',
                'estimator': 'dip'
            }
            d1['Two Lorentzian dips'] = {
                'fit_function': 'lorentziandouble',
                'estimator': 'dip'
            }
            d1['N14'] = {
                'fit_function': 'lorentziantriple',
                'estimator': 'N14'
            }
            d1['N15'] = {
                'fit_function': 'lorentziandouble',
                'estimator': 'N15'
            }
            d1['Two Gaussian dips'] = {
                'fit_function': 'gaussiandouble',
                'estimator': 'dip'
            }
            default_fits = OrderedDict()
            default_fits['1d'] = d1
            fc.load_from_dict(default_fits)
        return fc

    @fc.representer
    def sv_get_fits(self, val):
        """ save configured fits """
        if len(val.fit_list) > 0:
            return val.save_to_dict()
        else:
            return None

    def _initialize_odmr_plots(self):
        """ Initializing the ODMR plots (line and matrix). """
        self.odmr_plot_x = np.arange(
            self.mw_start,
            self.mw_stop +
            self.mw_step,
            self.mw_step)
        self.odmr_plot_y = np.zeros(
            [len(self.get_odmr_channels()), self.odmr_plot_x.size])
        self.odmr_fit_x = np.arange(
            self.mw_start,
            self.mw_stop +
            self.mw_step,
            self.mw_step)
        self.odmr_fit_y = np.zeros(self.odmr_fit_x.size)
        self.odmr_plot_xy = np.zeros([self.number_of_lines, len(
            self.get_odmr_channels()), self.odmr_plot_x.size])
        self.sigOdmrPlotsUpdated.emit(
            self.odmr_plot_x,
            self.odmr_plot_y,
            self.odmr_plot_xy)
        current_fit = self.fc.current_fit
        self.sigOdmrFitUpdated.emit(
            self.odmr_fit_x, self.odmr_fit_y, {}, current_fit)
        return

    def set_trigger(self, trigger_pol, frequency):
        """
        Set trigger polarity of external microwave trigger (for list and sweep mode).

        @param object trigger_pol: one of [TriggerEdge.RISING, TriggerEdge.FALLING]
        @param float frequency: trigger frequency during ODMR scan

        @return object: actually set trigger polarity returned from hardware
        """
        if self._lock_in_active:
            frequency = frequency / self._oversampling

        if self.module_state() != 'locked':
            self.mw_trigger_pol, _ = self._mw_device.set_ext_trigger(
                trigger_pol, 1 / frequency)
        else:
            self.log.warning('set_trigger failed. Logic is locked.')

        update_dict = {'trigger_pol': self.mw_trigger_pol}
        self.sigParameterUpdated.emit(update_dict)
        return self.mw_trigger_pol

    def set_average_length(self, lines_to_average):
        """
        Sets the number of lines to average for the sum of the data

        @param int lines_to_average: desired number of lines to average (0 means all)

        @return int: actually set lines to average
        """
        self.lines_to_average = int(lines_to_average)

        if self.lines_to_average <= 0:
            self.odmr_plot_y = np.mean(
                self.odmr_raw_data[:max(1, self.elapsed_sweeps), :, :],
                axis=0,
                dtype=np.float64
            )
        else:
            self.odmr_plot_y = np.mean(
                self.odmr_raw_data[:max(1, min(self.lines_to_average, self.elapsed_sweeps)), :, :],
                axis=0,
                dtype=np.float64
            )

        self.sigOdmrPlotsUpdated.emit(
            self.odmr_plot_x,
            self.odmr_plot_y,
            self.odmr_plot_xy)
        self.sigParameterUpdated.emit(
            {'average_length': self.lines_to_average})
        return self.lines_to_average

    def set_clock_frequency(self, clock_frequency):
        """
        Sets the frequency of the counter clock
        ## This must be dependant on the exposure time
        @param int clock_frequency: desired frequency of the clock

        @return int: actually set clock frequency
        """
        # checks if scanner is still running
        if self.module_state() != 'locked' and isinstance(clock_frequency, (int, float)):
            ##self.clock_frequency = int(clock_frequency)
            exp_res_dict = {0: 1000., 1: 1000000.}
            self.clock_frequency = 1. / ((self.exp_time / exp_res_dict[self._camera.get_exposure_resolution()]) + 0.0)
        else:
            self.log.warning(
                'set_clock_frequency failed. Logic is either locked or input value is '
                'no integer or float.')

        update_dict = {'clock_frequency': self.clock_frequency}
        self.sigParameterUpdated.emit(update_dict)
        return self.clock_frequency

    @property
    def oversampling(self):
        return self._oversampling

    @oversampling.setter
    def oversampling(self, oversampling):
        """
        Sets the frequency of the counter clock

        @param int oversampling: desired oversampling per frequency step
        """
        # checks if scanner is still running
        if self.module_state() != 'locked' and isinstance(oversampling, (int, float)):
            self._oversampling = int(oversampling)
            # self._odmr_counter.oversampling = self._oversampling
        else:
            self.log.warning(
                'setter of oversampling failed. Logic is either locked or input value is '
                'no integer or float.')

        update_dict = {'oversampling': self._oversampling}
        self.sigParameterUpdated.emit(update_dict)

    def set_oversampling(self, oversampling):
        self.oversampling = oversampling
        return self.oversampling

    @property
    def lock_in(self):
        return self._lock_in_active

    @lock_in.setter
    def lock_in(self, active):
        """
        Sets the frequency of the counter clock

        @param bool active: specify if signal should be detected with lock in
        """
        # checks if scanner is still running
        if self.module_state() != 'locked' and isinstance(active, bool):
            self._lock_in_active = active
            # self._odmr_counter.lock_in_active = self._lock_in_active
        else:
            self.log.warning(
                'setter of lock in failed. Logic is either locked or input value is no boolean.')

        update_dict = {'lock_in': self._lock_in_active}
        self.sigParameterUpdated.emit(update_dict)

    def set_lock_in(self, active):
        self.lock_in = active
        return self.lock_in

    def set_matrix_line_number(self, number_of_lines):
        """
        Sets the number of lines in the ODMR matrix

        @param int number_of_lines: desired number of matrix lines

        @return int: actually set number of matrix lines
        """
        if isinstance(number_of_lines, int):
            self.number_of_lines = number_of_lines
        else:
            self.log.warning('set_matrix_line_number failed. '
                             'Input parameter number_of_lines is no integer.')

        update_dict = {'number_of_lines': self.number_of_lines}
        self.sigParameterUpdated.emit(update_dict)
        return self.number_of_lines

    def set_runtime(self, runtime):
        """
        Sets the runtime for ODMR measurement

        @param float runtime: desired runtime in seconds

        @return float: actually set runtime in seconds
        """
        if isinstance(runtime, (int, float)):
            self.run_time = runtime
        else:
            self.log.warning(
                'set_runtime failed. Input parameter runtime is no integer or float.')

        update_dict = {'run_time': self.run_time}
        self.sigParameterUpdated.emit(update_dict)
        return self.run_time

    def set_cw_parameters(self, frequency, power):
        """ Set the desired new cw mode parameters.

        @param float frequency: frequency to set in Hz
        @param float power: power to set in dBm

        @return (float, float): actually set frequency in Hz, actually set power in dBm
        """
        if self.module_state() != 'locked' and isinstance(
                frequency, (int, float)) and isinstance(
                power, (int, float)):
            constraints = self.get_hw_constraints()
            frequency_to_set = constraints.frequency_in_range(frequency)
            power_to_set = constraints.power_in_range(power)
            self.cw_mw_frequency, self.cw_mw_power, dummy = self._mw_device.set_cw(
                frequency_to_set, power_to_set)
        else:
            self.log.warning(
                'set_cw_frequency failed. Logic is either locked or input value is '
                'no integer or float.')

        param_dict = {
            'cw_mw_frequency': self.cw_mw_frequency,
            'cw_mw_power': self.cw_mw_power}
        self.sigParameterUpdated.emit(param_dict)
        return self.cw_mw_frequency, self.cw_mw_power

    def set_sweep_parameters(self, start, stop, step, power):
        """ Set the desired frequency parameters for list and sweep mode

        @param float start: start frequency to set in Hz
        @param float stop: stop frequency to set in Hz
        @param float step: step frequency to set in Hz
        @param float power: mw power to set in dBm

        @return float, float, float, float: current start_freq, current stop_freq,
                                            current freq_step, current power
        """
        limits = self.get_hw_constraints()
        if self.module_state() != 'locked':
            if isinstance(start, (int, float)):
                self.mw_start = limits.frequency_in_range(start)
            if isinstance(
                    stop, (int, float)) and isinstance(
                    step, (int, float)):
                if stop <= start:
                    stop = start + step
                self.mw_stop = limits.frequency_in_range(stop)
                if self.mw_scanmode == MicrowaveMode.LIST:
                    self.mw_step = limits.list_step_in_range(step)
                elif self.mw_scanmode == MicrowaveMode.SWEEP:
                    self.mw_step = limits.sweep_step_in_range(step)
            if isinstance(power, (int, float)):
                self.sweep_mw_power = limits.power_in_range(power)
        else:
            self.log.warning('set_sweep_parameters failed. Logic is locked.')

        param_dict = {
            'mw_start': self.mw_start,
            'mw_stop': self.mw_stop,
            'mw_step': self.mw_step,
            'sweep_mw_power': self.sweep_mw_power}
        self.sigParameterUpdated.emit(param_dict)
        return self.mw_start, self.mw_stop, self.mw_step, self.sweep_mw_power

    def mw_cw_on(self):
        """
        Switching on the mw source in cw mode.

        @return str, bool: active mode ['cw', 'list', 'sweep'], is_running
        """
        if self.module_state() == 'locked':
            self.log.error(
                'Can not start microwave in CW mode. ODMRLogic is already locked.')
        else:
            self.cw_mw_frequency, self.cw_mw_power, mode = self._mw_device.set_cw(
                self.cw_mw_frequency, self.cw_mw_power)
            param_dict = {
                'cw_mw_frequency': self.cw_mw_frequency,
                'cw_mw_power': self.cw_mw_power}
            self.sigParameterUpdated.emit(param_dict)
            if mode != 'cw':
                self.log.error('Switching to CW microwave output mode failed.')
            else:
                err_code = self._mw_device.cw_on()
                if err_code < 0:
                    self.log.error('Activation of microwave output failed.')

        mode, is_running = self._mw_device.get_status()
        self.sigOutputStateUpdated.emit(mode, is_running)
        return mode, is_running

    def mw_sweep_on(self):
        """
        Switching on the mw source in list/sweep mode.

        @return str, bool: active mode ['cw', 'list', 'sweep'], is_running
        """

        limits = self.get_hw_constraints()
        param_dict = {}

        if self.mw_scanmode == MicrowaveMode.LIST:
            if np.abs(self.mw_stop - self.mw_start) / \
                    self.mw_step >= limits.list_maxentries:
                self.log.warning(
                    'Number of frequency steps too large for microwave device. '
                    'Lowering resolution to fit the maximum length.')
                self.mw_step = np.abs(
                    self.mw_stop - self.mw_start) / (limits.list_maxentries - 1)
                self.sigParameterUpdated.emit({'mw_step': self.mw_step})

            # adjust the end frequency in order to have an integer multiple of step size
            # The master module (i.e. GUI) will be notified about the changed
            # end frequency
            num_steps = int(
                np.rint(
                    (self.mw_stop -
                     self.mw_start) /
                    self.mw_step))
            end_freq = self.mw_start + num_steps * self.mw_step
            freq_list = np.linspace(self.mw_start, end_freq, num_steps + 1)
            freq_list, self.sweep_mw_power, mode = self._mw_device.set_list(
                freq_list, self.sweep_mw_power)
            self.mw_start = freq_list[0]
            self.mw_stop = freq_list[-1]
            self.mw_step = (self.mw_stop - self.mw_start) / \
                (len(freq_list) - 1)

            param_dict = {
                'mw_start': self.mw_start,
                'mw_stop': self.mw_stop,
                'mw_step': self.mw_step,
                'sweep_mw_power': self.sweep_mw_power}

        elif self.mw_scanmode == MicrowaveMode.SWEEP:
            if np.abs(self.mw_stop - self.mw_start) / \
                    self.mw_step >= limits.sweep_maxentries:
                self.log.warning(
                    'Number of frequency steps too large for microwave device. '
                    'Lowering resolution to fit the maximum length.')
                self.mw_step = np.abs(
                    self.mw_stop - self.mw_start) / (limits.list_maxentries - 1)
                self.sigParameterUpdated.emit({'mw_step': self.mw_step})

            sweep_return = self._mw_device.set_sweep(
                self.mw_start, self.mw_stop, self.mw_step, self.sweep_mw_power)
            self.mw_start, self.mw_stop, self.mw_step, self.sweep_mw_power, mode = sweep_return

            param_dict = {
                'mw_start': self.mw_start,
                'mw_stop': self.mw_stop,
                'mw_step': self.mw_step,
                'sweep_mw_power': self.sweep_mw_power}

        else:
            self.log.error(
                'Scanmode not supported. Please select SWEEP or LIST.')

        self.sigParameterUpdated.emit(param_dict)

        if mode != 'list' and mode != 'sweep':
            self.log.error(
                'Switching to list/sweep microwave output mode failed.')
        elif self.mw_scanmode == MicrowaveMode.SWEEP:
            err_code = self._mw_device.sweep_on()
            if err_code < 0:
                self.log.error('Activation of microwave output failed.')
        else:
            err_code = self._mw_device.list_on()
            if err_code < 0:
                self.log.error('Activation of microwave output failed.')

        mode, is_running = self._mw_device.get_status()
        self.sigOutputStateUpdated.emit(mode, is_running)
        return mode, is_running

    def reset_sweep(self):
        """
        Resets the list/sweep mode of the microwave source to the first frequency step.
        """
        if self.mw_scanmode == MicrowaveMode.SWEEP:
            self._mw_device.reset_sweeppos()
        elif self.mw_scanmode == MicrowaveMode.LIST:
            self._mw_device.reset_listpos()
        return

    def mw_off(self):
        """ Switching off the MW source.

        @return str, bool: active mode ['cw', 'list', 'sweep'], is_running
        """
        error_code = self._mw_device.off()
        if error_code < 0:
            self.log.error('Switching off microwave source failed.')

        mode, is_running = self._mw_device.get_status()
        self.sigOutputStateUpdated.emit(mode, is_running)
        return mode, is_running

    def set_exp_time(self, exp, cur_res_index):
        '''Exp time in mseconds in set from the GUI and updated for the cam class as well as in the variable
        in the ODMR logic class. The clock frequency variable is updated as well since it depends on the exp
        time of the camera.

        @param int: exp ~ 1ms - 10000ms
        '''
        self._camera.set_exposure_resolution(cur_res_index)
        self._camera.set_exposure(exp)
        self.exp_time = exp
        exp_res_dict = {0: 1000., 1: 1000000.}
        self.clock_frequency = 1. / ((exp / exp_res_dict[cur_res_index]) + 0.0)

    def _start_odmr_counter(self):
        """
        Starting the ODMR counter and set up the clock for it.
        No counter is actually set up instead the clock is set up and the camera trigger mode is set
        to edge trigger for every image in the sequence.

        @return int: error code (0:OK, -1:error)
        """

        clock_status = self._odmr_counter.set_up_odmr_clock(
            clock_frequency=self.clock_frequency, no_x=self.odmr_plot_x.size)
        # Seting exposure mode on camera via logic "Ext Trig Internal"
        # self._camera.set_trigger_seq("Ext Trig Internal")
        self._camera.set_trigger_seq("Edge Trigger")
        if clock_status < 0:
            return -1
        # Try not running the counter since we dont need it
        # counter_status = self._odmr_counter.set_up_odmr()
        # if counter_status < 0:
        #     self._odmr_counter.close_odmr_clock()
        #     return -1

        return 0

    def _stop_odmr_counter(self):
        """
        Stopping the ODMR counter.

        @return int: error code (0:OK, -1:error)
        """
        # Added a line in DAQmx hardware to wait until task is done when closing so as to allow camera to
        # complete the acquisition
        ret_val1 = self._odmr_counter.close_odmr()
        if ret_val1 != 0:
            self.log.error('ODMR counter could not be stopped!')
        # ret_val2 = self._odmr_counter.close_odmr_clock()
        # if ret_val2 != 0:
        #     self.log.error('ODMR clock could not be stopped!')
        
        # Check with a bitwise or:
        return ret_val1

    def start_odmr_scan(self):
        """ Starting an ODMR scan.

        @return int: error code (0:OK, -1:error)
        """
        with self.threadlock:
            if self.module_state() == 'locked':
                self.log.error(
                    'Can not start ODMR scan. Logic is already locked.')
                return -1

            self.set_trigger(self.mw_trigger_pol, self.clock_frequency)

            self.module_state.lock()
            self._clearOdmrData = False
            self.stopRequested = False
            self.fc.clear_result()

            self.elapsed_sweeps = 0
            self.elapsed_time = 0.0
            self._startTime = time.time()
            self.sigOdmrElapsedTimeUpdated.emit(
                self.elapsed_time, self.elapsed_sweeps)

            odmr_status = self._start_odmr_counter()
            if odmr_status < 0:
                mode, is_running = self._mw_device.get_status()
                self.sigOutputStateUpdated.emit(mode, is_running)
                self.module_state.unlock()
                return -1

            mode, is_running = self.mw_sweep_on()
            if not is_running:
                self._stop_odmr_counter()
                self.module_state.unlock()
                return -1

            self._initialize_odmr_plots()
            # initialize raw_data array
            estimated_number_of_lines = self.run_time * \
                self.clock_frequency / self.odmr_plot_x.size
            estimated_number_of_lines = int(
                1.5 * estimated_number_of_lines)  # Safety
            if estimated_number_of_lines < self.number_of_lines:
                estimated_number_of_lines = self.number_of_lines
            self.log.debug('Estimated number of raw data lines: {0:d}'
                           ''.format(estimated_number_of_lines))
            self.odmr_raw_data = np.zeros(
                [estimated_number_of_lines,
                 len(self._odmr_counter.get_odmr_channels()),
                 self.odmr_plot_x.size]
            )
            # Sweep images are set to zero at every new scan
            self.sweep_images = np.zeros(
                (self.odmr_plot_x.size, *np.flip(self._camera.get_size(), axis=0))
            )
            self.sigNextLine.emit()
            return 0

    def continue_odmr_scan(self):
        """ Continue ODMR scan.

        @return int: error code (0:OK, -1:error)
        """
        with self.threadlock:
            if self.module_state() == 'locked':
                self.log.error(
                    'Can not start ODMR scan. Logic is already locked.')
                return -1

            self.set_trigger(self.mw_trigger_pol, self.clock_frequency)

            self.module_state.lock()
            self.stopRequested = False
            self.fc.clear_result()

            self._startTime = time.time() - self.elapsed_time
            self.sigOdmrElapsedTimeUpdated.emit(
                self.elapsed_time, self.elapsed_sweeps)

            odmr_status = self._start_odmr_counter()
            if odmr_status < 0:
                mode, is_running = self._mw_device.get_status()
                self.sigOutputStateUpdated.emit(mode, is_running)
                self.module_state.unlock()
                return -1

            mode, is_running = self.mw_sweep_on()
            if not is_running:
                self._stop_odmr_counter()
                self.module_state.unlock()
                return -1

            self.sigNextLine.emit()
            return 0

    def stop_odmr_scan(self):
        """ Stop the ODMR scan.

        @return int: error code (0:OK, -1:error)
        """
        with self.threadlock:
            if self.module_state() == 'locked':
                self.stopRequested = True
        return 0

    def clear_odmr_data(self):
        """¨Set the option to clear the curret ODMR data.

        The clear operation has to be performed within the method
        _scan_odmr_line. This method just sets the flag for that. """
        with self.threadlock:
            if self.module_state() == 'locked':
                self._clearOdmrData = True
        return

    def _scan_odmr_line(self):
        """ Scans one line in ODMR

        (from mw_start to mw_stop in steps of mw_step)
        """
        with self.threadlock:
            # If the odmr measurement is not running do nothing
            if self.module_state() != 'locked':
                return

            # Stop measurement if stop has been requested
            if self.stopRequested:
                self.stopRequested = False
                self.mw_off()
                self._stop_odmr_counter()
                self.module_state.unlock()
                self._camera.set_trigger_seq("Internal Trigger")
                return

            # if during the scan a clearing of the ODMR data is needed:
            if self._clearOdmrData:
                self.elapsed_sweeps = 0
                self.sweep_images = np.zeros(
                    (self.odmr_plot_x.size, *np.flip(self._camera.get_size(), axis=0))
                )
                self._startTime = time.time()

            # reset position so every line starts from the same frequency
            self.reset_sweep()

            # Acquire count data
            # The trigger sequcne is started below after the specified delay.
            # The camera is also set to acquire and the code only goes below that line after
            # all the images in the sequence has been acquired. The triggers
            # are all stopped after that.
            error, new_counts = self._odmr_counter.count_odmr(
                length=self.odmr_plot_x.size)
            self._camera.start_trigger_seq(self.odmr_plot_x.size * 2)
            # self._odmr_counter.stop_tasks()
            # The collected frames are then acquired by the logic here from cam logic Should consider memory issues
            # for the future.
            frames = self._camera.get_last_image().astype('float')
            # The reference images from switch off time should be subtracted as below. For dummy measurements
            # so as to not be just left with noise we can do a dummy
            # subtraction.
            new_counts = (frames[0::2] - frames[1::2]) / (frames[1::2] + frames[0::2]) * 100
            # Remove for actual mesurements
            # new_counts = frames[1::2] - np.full_like(frames[0::2], 1)
            # The sweep images are added up and the new counts are taken as the mean of the image which is what
            # ends up being plotted as odmr_plot_y
            self.sweep_images += new_counts
            new_counts = np.mean(new_counts, axis=(1, 2))

            if error==-1:
                self.stopRequested = True
                self.sigNextLine.emit()
                return

            # Add new count data to raw_data array and append if array is too
            # small
            if self._clearOdmrData:
                self.odmr_raw_data[:, :, :] = 0
                self._clearOdmrData = False
            if self.elapsed_sweeps == (self.odmr_raw_data.shape[0] - 1):
                expanded_array = np.zeros(self.odmr_raw_data.shape)
                self.odmr_raw_data = np.concatenate(
                    (self.odmr_raw_data, expanded_array), axis=0)
                self.log.warning(
                    'raw data array in ODMRLogic was not big enough for the entire '
                    'measurement. Array will be expanded.\nOld array shape was '
                    '({0:d}, {1:d}), new shape is ({2:d}, {3:d}).'
                    ''.format(
                        self.odmr_raw_data.shape[0] -
                        self.number_of_lines,
                        self.odmr_raw_data.shape[1],
                        self.odmr_raw_data.shape[0],
                        self.odmr_raw_data.shape[1]))

            # shift data in the array "up" and add new data at the "bottom"
            self.odmr_raw_data = np.roll(self.odmr_raw_data, 1, axis=0)

            self.odmr_raw_data[0] = new_counts

            # Add new count data to mean signal
            if self._clearOdmrData:
                self.odmr_plot_y[:, :] = 0

            if self.lines_to_average <= 0:
                self.odmr_plot_y = np.mean(
                    self.odmr_raw_data[:max(1, self.elapsed_sweeps), :, :],
                    axis=0,
                    dtype=np.float64
                )
            else:
                self.odmr_plot_y = np.mean(
                    self.odmr_raw_data[:max(1, min(self.lines_to_average, self.elapsed_sweeps)), :, :],
                    axis=0,
                    dtype=np.float64
                )

            # Set plot slice of matrix
            self.odmr_plot_xy = self.odmr_raw_data[:self.number_of_lines, :, :]

            # Update elapsed time/sweeps
            self.elapsed_sweeps += 1
            self.elapsed_time = time.time() - self._startTime
            if self.elapsed_time >= self.run_time:
                self.stopRequested = True
            # Fire update signals
            self.sigOdmrElapsedTimeUpdated.emit(
                self.elapsed_time, self.elapsed_sweeps)
            self.sigOdmrPlotsUpdated.emit(
                self.odmr_plot_x, self.odmr_plot_y, self.odmr_plot_xy)
            self.sigNextLine.emit()
            return

    def get_odmr_channels(self):
        return ['Prime95B']

    def get_hw_constraints(self):
        """ Return the names of all ocnfigured fit functions.
        @return object: Hardware constraints object
        """
        constraints = self._mw_device.get_limits()
        return constraints

    def get_fit_functions(self):
        """ Return the hardware constraints/limits
        @return list(str): list of fit function names
        """
        return list(self.fc.fit_list)

    def print_coords(self, event, x, y, flags, param):
        '''The coords for finding the pixel spectrum are determined here from the mouse click callback.
        '''
        if (event == cv2.EVENT_LBUTTONDOWN):
            self.coord = (y, x)
            cv2.destroyAllWindows()

    def do_pixel_spectrum(self, frames):
        '''This is called from the fit depending on which button is clicked in the GUI. It displays an image
        which is the sum of all the sweep images divided by number of images so as to make sure all the features
        are seen.
        The coords of selected point are then found by mouse callback and the spectrum made into the new odmr_plot_y
        data as seen in do_fit()
        '''
        # So as to have a smaller images. Hence the times 2 in the print_coords
        # function to get actual coords.
        frame = np.sum(frames, axis=(0)) / np.shape(frames)[0]
        frame = frame.astype(np.uint16)
        # Needed because cv2 can handle only uint8(?) images.
        frame = cv2.normalize(
            frame,
            dst=None,
            alpha=0,
            beta=65535,
            norm_type=cv2.NORM_MINMAX)
        cv2.imshow(f'Sweep Image : {np.shape(frames)[0]}', frame)
        cv2.setMouseCallback(
            f'Sweep Image : {np.shape(frames)[0]}',
            self.print_coords)
        cv2.waitKey(0)

    def do_fit(
            self,
            fit_function=None,
            x_data=None,
            y_data=None,
            channel_index=0,
            pixel_fit=False):
        """
        Execute the currently configured fit on the measurement data. Optionally on passed data
        """
        # To enable default odmr_plot_y if no pixel is clicke and imshow is
        # just closed. Good for preview.
        self.coord = None
        if pixel_fit and np.count_nonzero(self.sweep_images) != 0:
            frames = self.sweep_images / self.elapsed_sweeps
            frames1 = np.zeros((np.shape(frames)[0], 600, 600))
            frames1[:] = [
                cv2.resize(
                    cv2.flip(frame, 0), (600, 600), interpolation=cv2.INTER_AREA) for frame in frames]
            frames = frames1
            self.do_pixel_spectrum(frames)
            # If no mouse click happens the odmr_plot_y data is not updated and stays the same.
            # This ends up allowing us to have a preview of the entire sweep as
            # well.

            if self.coord is not None:
                x_data = self.odmr_plot_x
                y_data = np.zeros(
                    [len(self.get_odmr_channels()), self.odmr_plot_x.size])
                y_data[0] = frames[:, self.coord[0], self.coord[1]]
                self.sigOdmrPlotsUpdated.emit(
                    x_data, y_data, self.odmr_plot_xy)
                y_data = y_data[0]
        # This enables us to reset to actual odmr_plot_y values after looking
        # at the pixel spectrum
        if not pixel_fit:
            self.sigOdmrPlotsUpdated.emit(
                self.odmr_plot_x, self.odmr_plot_y, self.odmr_plot_xy)

        if (x_data is None) or (y_data is None):
            x_data = self.odmr_plot_x
            y_data = self.odmr_plot_y[channel_index]

        if fit_function is not None and isinstance(fit_function, str):
            if fit_function in self.get_fit_functions():
                self.fc.set_current_fit(fit_function)
            else:
                self.fc.set_current_fit('No Fit')
                if fit_function != 'No Fit':
                    self.log.warning(
                        'Fit function "{0}" not available in ODMRLogic fit container.'
                        ''.format(fit_function))

        self.odmr_fit_x, self.odmr_fit_y, result = self.fc.do_fit(
            x_data, y_data)

        if result is None:
            result_str_dict = {}
        else:
            result_str_dict = result.result_str_dict
        self.sigOdmrFitUpdated.emit(
            self.odmr_fit_x,
            self.odmr_fit_y,
            result_str_dict,
            self.fc.current_fit)
        return

    def save_odmr_data(
            self,
            tag=None,
            colorscale_range=None,
            percentile_range=None,
            save_stack=False):
        """ Saves the current ODMR data to a file."""
        timestamp = datetime.datetime.now()

        if tag is None:
            tag = ''
        for nch, channel in enumerate(self.get_odmr_channels()):
            # two paths to save the raw data and the odmr scan data.
            filepath = self._save_logic.get_path_for_module(module_name='ODMR')
            filepath2 = self._save_logic.get_path_for_module(
                module_name='ODMR')

            if len(tag) > 0:
                filelabel = '{0}_ODMR_data_ch{1}'.format(tag, nch)
                filelabel2 = '{0}_ODMR_data_ch{1}_raw'.format(tag, nch)
            else:
                filelabel = 'ODMR_data_ch{0}'.format(nch)
                filelabel2 = 'ODMR_data_ch{0}_raw'.format(nch)

            # prepare the data in a dict or in an OrderedDict:
            data = OrderedDict()
            data2 = OrderedDict()
            data['frequency (Hz)'] = self.odmr_plot_x
            data['Arb. counts'] = self.odmr_plot_y[nch]
            data2['Arb. counts'] = self.odmr_raw_data[:self.elapsed_sweeps, nch, :]

            parameters = OrderedDict()
            parameters['Microwave CW Power (dBm)'] = self.cw_mw_power
            parameters['Microwave Sweep Power (dBm)'] = self.sweep_mw_power
            parameters['Run Time (s)'] = self.run_time
            parameters['Number of frequency sweeps (#)'] = self.elapsed_sweeps
            parameters['Start Frequency (Hz)'] = self.mw_start
            parameters['Stop Frequency (Hz)'] = self.mw_stop
            parameters['Step size (Hz)'] = self.mw_step
            parameters['Clock Frequency (Hz)'] = self.clock_frequency
            # Exposure time is added as well to the save parameters.
            parameters['Exposure time (ms)'] = self.exp_time
            parameters['Channel'] = '{0}: {1}'.format(nch, channel)
            if self.fc.current_fit != 'No Fit':
                parameters['Fit function'] = self.fc.current_fit

            # add all fit parameter to the saved data:
            for name, param in self.fc.current_fit_param.items():
                parameters[name] = str(param)

            fig = self.draw_figure(
                nch,
                cbar_range=colorscale_range,
                percentile_range=percentile_range)

            self._save_logic.save_data(data,
                                       filepath=filepath,
                                       parameters=parameters,
                                       filelabel=filelabel,
                                       fmt='%.6e',
                                       delimiter='\t',
                                       timestamp=timestamp,
                                       plotfig=fig)

            self._save_logic.save_data(data2,
                                       filepath=filepath2,
                                       parameters=parameters,
                                       filelabel=filelabel2,
                                       fmt='%.6e',
                                       delimiter='\t',
                                       timestamp=timestamp)
            # The files is saved as a compressed .npz file which can be looaed by np.load('.npz')['sweep_images']
            # Provides best possible compression for array storage. Saved with almost the same timestamp
            # as used in save_logic
            if save_stack:
                loc = filepath + '/' + \
                    timestamp.strftime("%Y%m%d-%H%M-%S") + '_' + filelabel + '_sweep'
                np.savez_compressed(
                loc,
                sweep_images=(self.sweep_images /
                                self.elapsed_sweeps))
            self.log.info('ODMR data saved to:\n{0}'.format(filepath))
        return

    def draw_figure(
            self,
            channel_number,
            cbar_range=None,
            percentile_range=None):
        """ Draw the summary figure to save with the data.

        @param: list cbar_range: (optional) [color_scale_min, color_scale_max].
                                 If not supplied then a default of data_min to data_max
                                 will be used.

        @param: list percentile_range: (optional) Percentile range of the chosen cbar_range.

        @return: fig fig: a matplotlib figure object to be saved to file.
        """
        freq_data = self.odmr_plot_x
        count_data = self.odmr_plot_y[channel_number]
        fit_freq_vals = self.odmr_fit_x
        fit_count_vals = self.odmr_fit_y
        matrix_data = self.odmr_plot_xy[:, channel_number]

        # If no colorbar range was given, take full range of data
        if cbar_range is None:
            cbar_range = np.array([np.min(matrix_data), np.max(matrix_data)])
        else:
            cbar_range = np.array(cbar_range)

        prefix = ['', 'k', 'M', 'G', 'T']
        prefix_index = 0

        # Rescale counts data with SI prefix
        while np.max(count_data) > 1000:
            count_data = count_data / 1000
            fit_count_vals = fit_count_vals / 1000
            prefix_index = prefix_index + 1

        counts_prefix = prefix[prefix_index]

        # Rescale frequency data with SI prefix
        prefix_index = 0

        while np.max(freq_data) > 1000:
            freq_data = freq_data / 1000
            fit_freq_vals = fit_freq_vals / 1000
            prefix_index = prefix_index + 1

        mw_prefix = prefix[prefix_index]

        # Rescale matrix counts data with SI prefix
        prefix_index = 0

        while np.max(matrix_data) > 1000:
            matrix_data = matrix_data / 1000
            cbar_range = cbar_range / 1000
            prefix_index = prefix_index + 1

        cbar_prefix = prefix[prefix_index]

        # Use qudi style
        plt.style.use(self._save_logic.mpl_qd_style)

        # Create figure
        fig, (ax_mean, ax_matrix) = plt.subplots(nrows=2, ncols=1)

        ax_mean.plot(freq_data, count_data, linestyle=':', linewidth=0.5)

        # Do not include fit curve if there is no fit calculated.
        if max(fit_count_vals) > 0:
            ax_mean.plot(fit_freq_vals, fit_count_vals, marker='None')

        ax_mean.set_ylabel('Arb. (' + counts_prefix + 'units)')
        ax_mean.set_xlim(np.min(freq_data), np.max(freq_data))

        matrixplot = ax_matrix.imshow(
            matrix_data,
            cmap=plt.get_cmap('inferno'),  # reference the right place in qd
            origin='lower',
            vmin=cbar_range[0],
            vmax=cbar_range[1],
            extent=[np.min(freq_data),
                    np.max(freq_data),
                    0,
                    self.number_of_lines
                    ],
            aspect='auto',
            interpolation='nearest')

        ax_matrix.set_xlabel('Frequency (' + mw_prefix + 'Hz)')
        ax_matrix.set_ylabel('Scan #')

        # Adjust subplots to make room for colorbar
        fig.subplots_adjust(right=0.8)

        # Add colorbar axis to figure
        cbar_ax = fig.add_axes([0.85, 0.15, 0.02, 0.7])

        # Draw colorbar
        cbar = fig.colorbar(matrixplot, cax=cbar_ax)
        cbar.set_label('Arb. (' + cbar_prefix + 'units)')

        # remove ticks from colorbar for cleaner image
        cbar.ax.tick_params(which=u'both', length=0)

        # If we have percentile information, draw that to the figure
        if percentile_range is not None:
            cbar.ax.annotate(str(percentile_range[0]),
                             xy=(-0.3, 0.0),
                             xycoords='axes fraction',
                             horizontalalignment='right',
                             verticalalignment='center',
                             rotation=90
                             )
            cbar.ax.annotate(str(percentile_range[1]),
                             xy=(-0.3, 1.0),
                             xycoords='axes fraction',
                             horizontalalignment='right',
                             verticalalignment='center',
                             rotation=90
                             )
            cbar.ax.annotate('(percentile)',
                             xy=(-0.3, 0.5),
                             xycoords='axes fraction',
                             horizontalalignment='right',
                             verticalalignment='center',
                             rotation=90
                             )

        return fig

    def perform_odmr_measurement(
            self,
            freq_start,
            freq_step,
            freq_stop,
            power,
            channel,
            runtime,
            fit_function='No Fit',
            save_after_meas=True,
            name_tag=''):
        """ An independant method, which can be called by a task with the proper input values
            to perform an odmr measurement.

        @return
        """
        timeout = 30
        start_time = time.time()
        while self.module_state() != 'idle':
            time.sleep(0.5)
            timeout -= (time.time() - start_time)
            if timeout <= 0:
                self.log.error(
                    'perform_odmr_measurement failed. Logic module was still locked '
                    'and 30 sec timeout has been reached.')
                return tuple()

        # set all relevant parameter:
        self.set_sweep_parameters(freq_start, freq_stop, freq_step, power)
        self.set_runtime(runtime)

        # start the scan
        self.start_odmr_scan()

        # wait until the scan has started
        while self.module_state() != 'locked':
            time.sleep(1)
        # wait until the scan has finished
        while self.module_state() == 'locked':
            time.sleep(1)

        # Perform fit if requested
        if fit_function != 'No Fit':
            self.do_fit(fit_function, channel_index=channel)
            fit_params = self.fc.current_fit_param
        else:
            fit_params = None

        # Save data if requested
        if save_after_meas:
            self.save_odmr_data(tag=name_tag)

        return self.odmr_plot_x, self.odmr_plot_y, fit_params
Example #12
0
class SaveLogic(GenericLogic):
    """
    A general class which saves all kinds of data in a general sense.

    Example config for copy-paste:
    
    savelogic:
        module.Class: 'save_logic.SaveLogic'
        win_data_directory: 'C:/Data'   # DO NOT CHANGE THE DIRECTORY HERE! ONLY IN THE CUSTOM FILE!
        unix_data_directory: 'Data/'
        log_into_daily_directory: True
        save_pdf: True
        save_png: True
    """

    _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')
    save_pdf = ConfigOption('save_pdf', False)
    save_png = ConfigOption('save_png', True)

    # 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'
    }

    # Matplotlib style definition for saving plots
    # Better than the default!
    mpl_qudihira_style = {
        'axes.linewidth': 0.5,
        'axes.labelweight': 'light',
        'lines.linewidth': 0.5,
        'xtick.major.width': 0.5,
        'ytick.major.width': 0.5,
        'font.weight': 'light',
        'font.sans-serif': 'Calibri',
        'mathtext.fontset': 'stixsans',
        'mathtext.default': 'regular',
        'axes.spines.right': True,
        'axes.spines.top': True,
        'xtick.minor.visible': True,
        'ytick.minor.visible': True,
        'savefig.dpi': '200',
        'figure.figsize': '12, 6',
    }

    _additional_parameters = {}

    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.')

        # Expand environment variables in the data_dir path (e.g. $HOME)
        self.data_dir = os.path.expandvars(self.data_dir)

        # 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):
                if isinstance(self._additional_parameters, dict):
                    parameters = {**self._additional_parameters, **parameters}
                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

            if self.save_pdf:
                # 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]

            if self.save_png:
                # 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):
        """ Gets or creates daily save 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.
        """
        current_dir = os.path.join(self.data_dir, time.strftime("%Y"),
                                   time.strftime("%m"),
                                   time.strftime("%Y%m%d"))

        if not os.path.isdir(current_dir):
            self.log.info("Creating directory for today's data:\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.isdir(dir_path):
            os.makedirs(dir_path, exist_ok=True)
        return dir_path

    def get_additional_parameters(self):
        """ Method that return the additional parameters dictionary securely """
        return self._additional_parameters.copy()

    def update_additional_parameters(self, *args, **kwargs):
        """
        Method to update one or multiple additional parameters

        @param dict args: Optional single positional argument holding parameters in a dict to
                          update additional parameters from.
        @param kwargs: Optional keyword arguments to be added to additional parameters
        """
        if len(args) == 0:
            param_dict = kwargs
        elif len(args) == 1 and isinstance(args[0], dict):
            param_dict = args[0]
            param_dict.update(kwargs)
        else:
            raise TypeError(
                '"update_additional_parameters" takes exactly 0 or 1 positional '
                'argument of type dict.')

        for key in param_dict.keys():
            param_dict[key] = netobtain(param_dict[key])
        self._additional_parameters.update(param_dict)
        return

    def remove_additional_parameter(self, key):
        """
        remove parameter from additional parameters

        @param str key: The additional parameters key/name to delete
        """
        self._additional_parameters.pop(key, None)
        return
Example #13
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')
    _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
Example #14
0
class LaserAomInterfuse(GenericLogic, SimpleLaserInterface):
    """ This interfuse can be used to control the laser power after an AOM driven by an analog ouput on a confocal
    scanner hardware (the 4th analog output 'a')

    The hardware module should be configured accordingly (range 0 to 1, voltage 0 to 1V for example)

    This module needs a calibration file for the AOM. This is a 2D array with the first column the relative power
    (power over maximum power) and the second column the associated voltage.
    This data is interpolated to define the power/voltage function
    """

    # connector to the confocal scanner hardware that has analog output feature
    scanner = Connector(interface='ConfocalScannerInterface')

    # max power the AOM can deliver (in Watt)
    _max_power = ConfigOption('max_power', missing='error')

    # calibration file which can be read by numpy loadtxt with two columns :
    # relative power (0.0 to 1.0), voltage (V)
    _calibration_file = ConfigOption('calibration_file', missing='error')
    _power_to_voltage = None
    _power = 0
    _laser_on = LaserState.OFF

    def on_activate(self):
        """ Activate module.
        """
        self._scanner = self.scanner()
        if 'a' not in self._scanner.get_scanner_axes():
            self.log.error(
                'Scanner does not have an "a" axe configured. Can not use it to control an AOM.'
            )

        calibration_data = np.loadtxt(self._calibration_file)
        power_rel_to_voltage = interp1d(calibration_data[:, 0],
                                        calibration_data[:, 1])
        self._power_to_voltage = lambda power: power_rel_to_voltage(
            power / self._max_power)

    def on_deactivate(self):
        """ Deactivate module.
        """
        pass

    def get_power_range(self):
        """ Return optical power range

            @return (float, float): power range
        """
        return 0, self._max_power

    def get_power(self):
        """ Return laser power

            @return float: Laser power in watts
        """
        return self._power

    def get_power_setpoint(self):
        """ Return optical power setpoint.

            @return float: power setpoint in watts
        """
        return self._power

    def set_power(self, power):
        """ Set power setpoint.

            @param float power: power setpoint

            @return float: actual new power setpoint
        """
        mini, maxi = self.get_power_range()
        if mini <= power <= maxi:
            self._power = power
            if self._laser_on == LaserState.ON:
                voltage = self._power_to_voltage(power)
            else:
                voltage = self._power_to_voltage(0)
            if self._scanner.module_state() == 'locked':
                self.log.error(
                    'Output device of the voltage for the AOM is locked, cannot set voltage.'
                )
            else:
                if self._scanner.scanner_set_position(a=voltage) < 0:
                    self.log.error(
                        'Could not set the voltage for the AOM because the scanner failed.'
                    )
        return self._power

    def get_current_unit(self):
        """ Get unit for laser current.

            @return str: unit
        """
        return '%'

    def get_current_range(self):
        """ Get laser current range.

            @return (float, float): laser current range
        """
        return 0, 100

    def get_current(self):
        """ Get current laser current

            @return float: laser current in current curent units
        """
        return 0

    def get_current_setpoint(self):
        """ Get laser curent setpoint

            @return float: laser current setpoint
        """
        return 0

    def set_current(self, current):
        """ Set laser current setpoint

            @prarm float current: desired laser current setpoint

            @return float: actual laser current setpoint
        """
        return 0

    def allowed_control_modes(self):
        """ Get supported control modes

            @return list(): list of supported ControlMode
        """
        return [ControlMode.POWER]

    def get_control_mode(self):
        """ Get the currently active control mode

            @return ControlMode: active control mode
        """
        return ControlMode.POWER

    def set_control_mode(self, control_mode):
        """ Set the active control mode

            @param ControlMode control_mode: desired control mode

            @return ControlMode: actual active ControlMode
        """
        return ControlMode.POWER

    def on(self):
        """ Turn on laser.

            @return LaserState: actual laser state
        """
        return self.set_laser_state(LaserState.ON)

    def off(self):
        """ Turn off laser.

            @return LaserState: actual laser state
        """
        return self.set_laser_state(LaserState.OFF)

    def get_laser_state(self):
        """ Get laser state

            @return LaserState: actual laser state
        """
        return self._laser_on

    def set_laser_state(self, state):
        """ Set laser state.

            @param LaserState state: desired laser state

            @return LaserState: actual laser state
        """
        self._laser_on = state
        self.set_power(self._power)

    def get_shutter_state(self):
        """ Get laser shutter state

            @return ShutterState: actual laser shutter state
        """
        return ShutterState.NOSHUTTER

    def set_shutter_state(self, state):
        """ Set laser shutter state.

            @param ShutterState state: desired laser shutter state

            @return ShutterState: actual laser shutter state
        """
        return ShutterState.NOSHUTTER

    def get_temperatures(self):
        """ Get all available temperatures.

            @return dict: dict of temperature namce and value in degrees Celsius
        """
        return {}

    def set_temperatures(self, temps):
        """ Set temperatures for lasers with tunable temperatures.

            @return {}: empty dict, dummy not a tunable laser
        """
        return {}

    def get_temperature_setpoints(self):
        """ Get temperature setpoints.

            @return dict: temperature setpoints for temperature tunable lasers
        """
        return {}

    def get_extra_info(self):
        """ Multiple lines of dignostic information

            @return str: much laser, very useful
        """
        return ""

    def set_max_power(self, maxi):
        """ Function to redefine the max power if the value has changed """
        self._max_power = maxi
Example #15
0
class TimeTaggerAutocorrelation(Base, AutocorrelationInterface):
    """ UNSTABLE
    Using the TimeTagger for autocorrelation measurement.

    Example config for copy-paste:

    timetagger_slowcounter:
        module.Class: 'timetagger_autocorrelation.TimeTaggerAutocorrelation'
        timetagger_channel_apd_0: 0
        timetagger_channel_apd_1: 1
    """

    _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._tagger = tt.createTimeTagger()

        self._count_length = int(10)
        self._bin_width = 1  # bin width in ps
        self._tagger.reset()
        self.correlation = None

        self.statusvar = 0

    def on_deactivate(self):
        """ Deactivate the FPGA.
        """
        if self.module_state() == 'locked':
            self.correlation.stop()
        self.correlation.clear()
        self.correlation = None
        self._tagger.reset()
        return 0

    def get_constraints(self):
        """ Get hardware limits of TimeTagger autocorrelation device.

        @return AutocorrelationConstraints: constraints class for autocorrelation
        """
        constraints = AutocorrelationConstraints()
        constraints.max_channels = 2
        constraints.min_channels = 2
        constraints.min_count_length = 1
        constraints.min_bin_width = 0

        return constraints

    def set_up_correlation(self, count_length=None, bin_width=None):
        """ 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

        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

    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.module_state() != 'locked':
            self.lock()
            self.correlation.clear()
            self.correlation.start()
            self.statusvar = 2
        return 0

    def stop_measure(self):
        """ Stop the fast counter. """
        if self.module_state() == 'locked':
            self.correlation.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.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.module_state() == '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
        """
        return self.correlation.getData()

    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 self.correlation.getDataNormalized()

    def get_bin_times(self):
        return self.correlation.getIndex()

    def close_correlation(self):
        """ Closes the counter and cleans up afterwards.

        @return int: error code (0:OK, -1:error)
        """
        self._tagger.reset()
        return 0
class ProcessControlModifier(GenericLogic, ProcessControlInterface):
    """ This interfuse can be used to modify a process control on the fly. It needs a 2D array to interpolate
    General form : [[x_0, y_0], [x_1, y_1], ... , [x_n, y_n]]
    Example : [[0,0], [1,10]]
    With this example, the value 0.5 sent from the logic would be transformed to 5 sent to the hardware.



    This calibration is stored and remembered as a status variable. If this variable is None, the calibration
    can be read from a simple file with two columns :
    # X Y
    0   0
    1   10
    """

    hardware = Connector(interface='ProcessControlInterface')

    _calibration = StatusVar(default=None)
    _calibration_file = ConfigOption('calibration_file', None)
    _force_calibration_from_file = ConfigOption('force_calibration_from_file', False)
    _interpolated_function = None
    _interpolated_function_reversed = None

    _new_unit = ConfigOption('new_unit', None)

    _last_control_value = None

    def on_activate(self):
        """ Activate module.
        """
        self._hardware = self.hardware()

        if self._force_calibration_from_file and self._calibration_file is None:
            self.log.error('Loading from calibration is enforced but no calibration file has been'
                           'given.')
        if self._force_calibration_from_file or (self._calibration is None and self._calibration_file is not None):
            self.log.info('Loading from calibration file.')
            calibration = np.loadtxt(self._calibration_file)
            self.update_calibration(calibration)
        else:
            self.update_calibration()

    def on_deactivate(self):
        """ Deactivate module.
        """
        pass

    def update_calibration(self, calibration=None):
        """ Construct the interpolated function from the calibration data

        calibration (optional) 2d array : A new calibration to set

        """
        if calibration is not None:
            self._calibration = calibration
        if self._calibration is None:
            self._interpolated_function = lambda x: x
            self._interpolated_function_reversed = lambda x: x
        else:
            self._interpolated_function = interp1d(self._calibration[:, 0], self._calibration[:, 1])
            self._interpolated_function_reversed = interp1d(self._calibration[:, 1], self._calibration[:, 0])
        if self._last_control_value is not None:
            self.set_control_value(self._last_control_value)

    def reset_to_identity(self):
        """ Reset the calibration data to use identity """
        self._calibration = None
        self.update_calibration()

    def get_control_value(self):
        """ Return the original control value
        """
        if self._interpolated_function_reversed is not None:
            return self._interpolated_function_reversed(self._hardware.get_control_value())
        else:
            self.log.error('No calibration was found, please set the control value modifier data first.')

    def set_control_value(self, value):
        """ Set the control value modified
        """
        if self._interpolated_function is not None:
            self._hardware.set_control_value(self._interpolated_function(value))
        else:
            self.log.error('No calibration was found, please set the control value modifier data first.')

    def get_control_unit(self):
        """ Return the process unit
        """
        if self._new_unit is not None:
            return self._new_unit
        else:
            return self._hardware.get_control_unit()

    def get_control_limit(self):
        """ Return limits within which the controlled value can be set as a tuple of (low limit, high limit)
        """
        mini, maxi = self._hardware.get_control_limit()
        mini = float(self._interpolated_function_reversed(mini))
        maxi = float(self._interpolated_function_reversed(maxi))
        return mini, maxi
Example #17
0
class Main(Base, ScienceCameraInterface):
    """ Hardware class for Andor CCD spectroscopy cameras

    Tested with :
     - Newton 940

    Example config for copy-paste:

    andor_camera:
        module.Class: 'camera.andor_camera.Main'
        dll_location: 'C:\\camera\\andor.dll' # path to library file
        default_trigger_mode: 'INTERNAL'
        shutter_TTL: 1
        shutter_switching_time: 100e-3
    """
    _dll_location = ConfigOption('dll_location', missing='error')

    _start_cooler_on_activate = ConfigOption('start_cooler_on_activate', True)
    _default_temperature_degree = ConfigOption('default_temperature', -80)  # Temperature in °C (not Kelvin !)
    _default_trigger_mode = ConfigOption('default_trigger_mode', 'INTERNAL')
    _has_external_shutter = ConfigOption('has_external_shutter', False)

    # The typ parameter allows the user to control the TTL signal output to an external shutter.
    # 0 Output TTL low signal to open shutter
    # 1 Output TTL high signal to open shutter
    _shutter_TTL = ConfigOption('shutter_TTL', 1)

    # The opening and closing time specify the time required to open and close the shutter
    # (this information is required for calculating acquisition timings)
    _shutter_switching_time = ConfigOption('shutter_switching_time', 100e-3)

    # Declarations of attributes to make Pycharm happy
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._constraints = None
        self._dll = None
        self._active_tracks = None
        self._image_advanced_parameters = None
        self._readout_speed = None
        self._preamp_gain = None
        self._read_mode = None
        self._trigger_mode = None
        self._shutter_status = None
        self._cooler_status = None
        self._temperature_setpoint = None
        self._acquisition_mode = None

    ##############################################################################
    #                            Basic module activation/deactivation
    ##############################################################################
    def on_activate(self):
        """ Initialization performed during activation of the module. """
        try:
            self._dll = ct.cdll.LoadLibrary(self._dll_location)
        except OSError:
            self.log.error('Error during dll loading of the Andor camera, check the dll path.')
        # This module handle only one camera. DLL support up to 8 cameras.
        status_code = self._dll.Initialize()
        if status_code != OK_CODE:
            self.log.error('Problem during camera initialization')
            return

        self._constraints = self._build_constraints()

        if self._constraints.has_cooler and self._start_cooler_on_activate:
            self.set_cooler_on(True)

        self.set_read_mode(ReadMode.FVB)  # Good default value, the logic will handle it from here
        self.set_trigger_mode(self._default_trigger_mode)
        self.set_temperature_setpoint(self._default_temperature_degree + 273.15)

        self.set_readout_speed(self._constraints.readout_speeds[0])
        self.set_gain(self._constraints.internal_gains[0])
        self._set_acquisition_mode(AcquisitionMode.SINGLE_SCAN)
        self.set_trigger_mode(self._constraints.trigger_modes[0])

        if self._constraints.has_shutter:
            self.set_shutter_state(ShutterState.AUTO)

        if self._constraints.has_cooler:
            self.set_cooler_on(True)

        self._active_tracks = np.array([0, self._constraints.height-1])
        self._image_advanced_parameters = ImageAdvancedParameters()
        self._image_advanced_parameters.horizontal_end = self._constraints.width-1
        self._image_advanced_parameters.vertical_end = self._constraints.height-1

    def on_deactivate(self):
        """ De-initialisation performed during deactivation of the module. """
        if self.module_state() == 'locked':
            self.stop_acquisition()
        # if self._close_shutter_on_deactivate:
        #     self.set_shutter_open_state(False)
        try:
            self._dll.ShutDown()
        except:
            self.log.warning('Error while shutting down Andor camera via dll.')

    ##############################################################################
    #                                     Error management
    ##############################################################################
    def _check(self, func_val):
        """ Check routine for the received error codes.

        @param (int) func_val: Status code returned by the DLL

        @return: The DLL function error code
        """
        if not func_val == OK_CODE:
            self.log.error('Error in Andor camera with error_code {}:{}'.format(func_val, ERROR_DICT[func_val]))
        return func_val

    ##############################################################################
    #                                     Constraints functions
    ##############################################################################
    def _build_constraints(self):
        """ Internal method that build the constraints once at initialisation

         This makes multiple call to the DLL, so it will be called only once by on_activate
         """
        constraints = Constraints()
        constraints.name = self._get_name()
        constraints.width, constraints.height = self._get_image_size()
        constraints.pixel_size_width, constraints.pixel_size_height = self._get_pixel_size()
        constraints.internal_gains = self._get_available_gains()
        constraints.readout_speeds = self._get_available_speeds()
        constraints.trigger_modes = self._get_available_trigger_modes()
        constraints.has_shutter = self._has_shutter()
        constraints.read_modes = [ReadMode.FVB]
        if constraints.height > 1:
            constraints.read_modes.extend([ReadMode.MULTIPLE_TRACKS, ReadMode.IMAGE, ReadMode.IMAGE_ADVANCED])
        constraints.has_cooler = True  # All Andor camera have one
        constraints.temperature.min, constraints.temperature.max = self._get_temperature_range()
        constraints.temperature.step = 1  # Andor cameras use integer for control

        return constraints

    def get_constraints(self):
        """ Returns all the fixed parameters of the hardware which can be used by the logic.

        @return (Constraints): An object of class Constraints containing all fixed parameters of the hardware
        """
        return self._constraints

    ##############################################################################
    #                                     Basic functions
    ##############################################################################
    def start_acquisition(self):
        """ Starts the acquisition """
        self._check(self._dll.StartAcquisition())

    def _wait_for_acquisition(self):
        """ Internal function, can be used to wait till acquisition is finished """
        self._dll.WaitForAcquisition()

    def abort_acquisition(self):
        """ Aborts the acquisition """
        self._check(self._dll.AbortAcquisition())

    def get_ready_state(self):
        """ Get the status of the camera, to know if the acquisition is finished or still ongoing.

        @return (bool): True if the camera is ready, False if an acquisition is ongoing

        As there is no synchronous acquisition in the interface, the logic needs a way to check the acquisition state.
        """
        code = ct.c_int()
        self._dll.GetStatus(ct.byref(code))
        if ERROR_DICT[code.value] == 'DRV_IDLE':
            return True
        elif ERROR_DICT[code.value] == 'DRV_ACQUIRING':
            return False
        else:
            self._check(code.value)

    def get_acquired_data(self):
        """ Return an array of last acquired data.

               @return: Data in the format depending on the read mode.

               Depending on the read mode, the format is :
               'FVB' : 1d array
               'MULTIPLE_TRACKS' : list of 1d arrays
               'IMAGE' 2d array of shape (width, height)
               'IMAGE_ADVANCED' 2d array of shape (width, height)

               Each value might be a float or an integer.
               """
        width = self.get_constraints().width
        if self.get_read_mode() == ReadMode.FVB:
            height = 1
        elif self.get_read_mode() == ReadMode.MULTIPLE_TRACKS:
            height = int(len(self.get_active_tracks())/2)
        elif self.get_read_mode() == ReadMode.IMAGE:
            height = self.get_constraints().height
        elif self.get_read_mode() == ReadMode.IMAGE_ADVANCED:
            params = self.get_image_advanced_parameters()
            height = int((params.vertical_end - params.vertical_start+1)/params.vertical_binning)
            width = int((params.horizontal_end - params.horizontal_start+1)/params.horizontal_binning)

        dimension = int(width * height)
        c_image_array = ct.c_int32 * dimension
        c_image = c_image_array()
        status_code = self._dll.GetAcquiredData(ct.pointer(c_image), dimension)
        if status_code != OK_CODE:
            self.log.error('Could not retrieve data from camera. {0}'.format(ERROR_DICT[status_code]))

        if self.get_read_mode() == ReadMode.FVB:
            return np.array(c_image)
        if self.get_read_mode() == ReadMode.MULTIPLE_TRACKS:
            sorted_data = np.array([int(i) for i in self._sorted_tracks[::2]/2])
            return  np.reshape(np.array(c_image), (height, width))[sorted_data]
        else:
            return np.reshape(np.array(c_image), (height, width))

    ##############################################################################
    #                           Read mode functions
    ##############################################################################
    def get_read_mode(self):
        """ Getter method returning the current read mode used by the camera.

        @return (ReadMode): Current read mode
        """
        return self._read_mode

    def set_read_mode(self, value):
        """ Setter method setting the read mode used by the camera.

         @param (ReadMode) value: read mode to set
         """

        if value not in self.get_constraints().read_modes:
            self.log.error('read_mode not supported')
            return

        conversion_dict = {ReadMode.FVB: 0,
                           ReadMode.MULTIPLE_TRACKS: 2,
                           ReadMode.IMAGE: 4,
                           ReadMode.IMAGE_ADVANCED: 4}

        n_mode = conversion_dict[value]
        status_code = self._check(self._dll.SetReadMode(n_mode))
        if status_code == OK_CODE:
            self._read_mode = value

        if value == ReadMode.IMAGE or value == ReadMode.IMAGE_ADVANCED:
            self._update_image()
        elif value == ReadMode.MULTIPLE_TRACKS:
            self._update_active_tracks()

    def get_readout_speed(self):
        """  Get the current readout speed (in Hz)

        @return (float): the readout_speed (Horizontal shift) in Hz
        """
        return self._readout_speed  # No getter in the DLL

    def set_readout_speed(self, value):
        """ Set the readout speed (in Hz)

        @param (float) value: horizontal readout speed in Hz
        """
        if value in self.get_constraints().readout_speeds:
            readout_speed_index = self.get_constraints().readout_speeds.index(value)
            self._check(self._dll.SetHSSpeed(0, readout_speed_index))
            self._readout_speed = value
        else:
            self.log.error('Readout_speed value error, value {} is not in correct.'.format(value))

    def get_active_tracks(self):
        """ Getter method returning the read mode tracks parameters of the camera.

        @return (list):  active tracks positions [(start_1, end_1), (start_2, end_2), ... ]
        """
        return self._active_tracks  # No getter in the DLL

    def set_active_tracks(self, value):
        """ Setter method for the active tracks of the camera.

        @param (ndarray) value: active tracks positions  as [start_1, end_1, start_2, end_2, ... ]
        """

        self._active_tracks = value
        self._update_active_tracks()

    def _update_active_tracks(self):
        """ Internal function that send the current active tracks to the DLL """
        if self.get_read_mode() == ReadMode.MULTIPLE_TRACKS:
            self._sorted_tracks = np.argsort(self._active_tracks)
            tracks = self._active_tracks[self._sorted_tracks]+1
            self._dll.SetRandomTracks.argtypes = [ct.c_int32, ct.c_void_p]
            status_code = self._check(self._dll.SetRandomTracks(int(len(tracks)/2), tracks.ctypes.data))
            self._check(status_code)
            if status_code != OK_CODE:  # Clear tracks if an error has occurred
                self._active_tracks = []

    def get_image_advanced_parameters(self):
        """ Getter method returning the image parameters of the camera.

        @return (ImageAdvancedParameters): Current image advanced parameters

        Can be used in any mode
        """
        return self._image_advanced_parameters  # No getter in the DLL

    def set_image_advanced_parameters(self, value):
        """ Setter method setting the read mode image parameters of the camera.

        @param (ImageAdvancedParameters) value: Parameters to set

        Can be used in any mode
        """
        if not isinstance(value, ImageAdvancedParameters):
            self.log.error('ImageAdvancedParameters value error. Value {} is not correct.'.format(value))
        self._image_advanced_parameters = value
        self._update_image()

    def _update_image(self):
        """ Internal method that send the current appropriate image settings to the DLL"""

        if self.get_read_mode() == ReadMode.IMAGE:
            status_code = self._dll.SetImage(1, 1, 1, self.get_constraints().width, 1, self.get_constraints().height)
            self._check(status_code)

        elif self.get_read_mode() == ReadMode.IMAGE_ADVANCED:
            params = self._image_advanced_parameters
            status_code = self._dll.SetImage(int(params.horizontal_binning),  int(params.vertical_binning),
                                             int(params.horizontal_start+1), int(params.horizontal_end+1),
                                             int(params.vertical_start+1), int(params.vertical_end+1))
            self._check(status_code)

    ##############################################################################
    #                           Acquisition mode functions
    ##############################################################################
    def _get_acquisition_mode(self):
        """ Getter method returning the current acquisition mode used by the camera.

        @return (str): acquisition mode
        """
        return self._acquisition_mode  # No getter in the DLL

    def _set_acquisition_mode(self, value):
        """ Setter method setting the acquisition mode used by the camera.

        @param (str|AcquisitionMode): Acquisition mode as a string or an object

        This method is not part of the interface, so we might need to use it from a script directly. Hence, here
        it is worth it to accept a string.
        """
        if isinstance(value, str) and value in AcquisitionMode.__members__:
            value = AcquisitionMode[value]
        if not isinstance(value, AcquisitionMode):
            self.log.error('{} acquisition mode is not supported'.format(value))
            return
        n_mode = ct.c_int(value.value)
        self._check(self._dll.SetAcquisitionMode(n_mode))

    def get_exposure_time(self):
        """ Get the exposure time in seconds

        @return (float) : exposure time in s
        """
        return self._get_acquisition_timings()['exposure']

    def _get_acquisition_timings(self):
        """ Get the acquisitions timings from the dll

        @return (dict): dict containing keys 'exposure', 'accumulate', 'kinetic' and their values in seconds """
        exposure, accumulate, kinetic = ct.c_float(), ct.c_float(), ct.c_float()
        self._check(self._dll.GetAcquisitionTimings(ct.byref(exposure), ct.byref(accumulate), ct.byref(kinetic)))
        return {'exposure': exposure.value, 'accumulate': accumulate.value, 'kinetic': kinetic.value}

    def set_exposure_time(self, value):
        """ Set the exposure time in seconds

        @param (float) value: desired new exposure time
        """
        if value < 0:
            self.log.error('Exposure_time ({} s) can not be negative.'.format(value))
            return
        self._check(self._dll.SetExposureTime(ct.c_float(value)))

    def get_gain(self):
        """ Get the gain

        @return (float): exposure gain
        """
        return self._preamp_gain  # No getter in the DLL

    def set_gain(self, value):
        """ Set the gain

        @param (float) value: New gain, value should be one in the constraints internal_gains list.
        """
        if value not in self.get_constraints().internal_gains:
            self.log.error('gain value {} is not available.'.format(value))
            return
        gain_index = self.get_constraints().internal_gains.index(value)
        self._check(self._dll.SetPreAmpGain(gain_index))
        self._preamp_gain = value

    ##############################################################################
    #                           Trigger mode functions
    ##############################################################################
    def get_trigger_mode(self):
        """ Getter method returning the current trigger mode used by the camera.

        @return (str): current trigger mode
        """
        return self._trigger_mode  # No getter in the DLL

    def set_trigger_mode(self, value):
        """ Setter method for the trigger mode used by the camera.

        @param (str) value: trigger mode (must be compared to a dict)
        """
        if value not in self.get_constraints().trigger_modes:
            self.log.error('Trigger mode {} is not declared by hardware.'.format(value))
            return
        n_mode = TriggerMode[value].value
        status_code = self._check(self._dll.SetTriggerMode(n_mode))
        if status_code == OK_CODE:
            self._trigger_mode = value

    ##############################################################################
    #                           Shutter mode functions
    ##############################################################################
    def get_shutter_state(self):
        """ Getter method returning the shutter state.

        @return (ShutterState): The current shutter state
        """
        if not self.get_constraints().has_shutter:
            self.log.error('Can not get state of the shutter, camera does not have a shutter')
            return
        return self._shutter_status.name  # No getter in the DLL

    def set_shutter_state(self, value):
        """ Setter method setting the shutter state.

        @param (ShutterState) value: the shutter state to set
        """
        if not self.get_constraints().has_shutter:
            self.log.error('Can not set state of the shutter, camera does not have a shutter')

        conversion_dict = {ShutterState.AUTO: 0, ShutterState.OPEN: 1, ShutterState.CLOSED: 2}
        mode = conversion_dict[value]
        shutter_TTL = int(self._shutter_TTL)
        shutter_time = int(round(self._shutter_switching_time*1e3))  # DLL use ms
        status_code = self._check(self._dll.SetShutter(shutter_TTL, mode, shutter_time, shutter_time))
        if status_code == OK_CODE:
            self._shutter_status = value

    ##############################################################################
    #                           Temperature functions
    ##############################################################################
    def get_cooler_on(self):
        """ Getter method returning the cooler status

        @return (bool): True if the cooler is on
        """
        return self._cooler_status  # No getter in the DLL

    def set_cooler_on(self, value):
        """ Setter method for the the cooler status

        @param (bool) value: True to turn it on, False to turn it off
        """
        if value:
            status_code = self._dll.CoolerON()
        else:
            status_code = self._dll.CoolerOFF()
        self._check(status_code)
        if status_code == OK_CODE:
            self._cooler_status = value

    def get_temperature(self):
        """ Getter method returning the temperature of the camera.

        @return (float): temperature (in Kelvin)
        """
        temperature = ct.c_float()
        self._dll.GetTemperatureF(ct.byref(temperature))
        return temperature.value + 273.15

    def get_temperature_setpoint(self):
        """ Getter method for the temperature setpoint of the camera.

        @return (float): Current setpoint in Kelvin
        """
        return self._temperature_setpoint  # Not present in Andor DLL

    def set_temperature_setpoint(self, value):
        """ Setter method for the the temperature setpoint of the camera.

        @param (float) value: New setpoint in Kelvin
        """
        constraints = self.get_constraints().temperature
        if not(constraints.min < value < constraints.max):
            self.log.error('Temperature {} K is not in the validity range.'.format(value))
            return
        temperature = int(round(value - 273.15))
        status_code = self._check(self._dll.SetTemperature(temperature))
        if status_code == OK_CODE:
            self._temperature_setpoint = temperature + 273.15

    ##############################################################################
    #               Internal functions, for constraints preparation
    ##############################################################################
    def _get_serial_number(self):
        """ Get the serial number of the camera as a string

        @return (str): serial number of the camera
        """
        serial = ct.c_int()
        self._check(self._dll.GetCameraSerialNumber(ct.byref(serial)))
        return serial.value

    def _get_name(self):
        """ Get a name for the camera

        @return (str): local camera name with serial number
        """
        return "Camera SN: {}".format(self._get_serial_number())

    def _get_image_size(self):
        """ Returns the sensor size in pixels (width, height)

        @return tuple(int, int): number of pixel in width and height
        """
        nx_px = ct.c_int()
        ny_px = ct.c_int()
        self._check(self._dll.GetDetector(ct.byref(nx_px), ct.byref(ny_px)))
        return nx_px.value, ny_px.value

    def _get_pixel_size(self):
        """ Get the physical pixel size (width, height) in meter

        @return tuple(float, float): physical pixel size in meter
        """
        x_px = ct.c_float()
        y_px = ct.c_float()
        self._check(self._dll.GetPixelSize(ct.byref(x_px), ct.byref(y_px)))
        return y_px.value * 1e-6, x_px.value * 1e-6

    def _get_temperature_range(self):
        """ Get the temperature minimum and maximum of the camera, in K

        @return tuple(float, float): The minimum minimum and maximum allowed for the setpoint in K """
        mini = ct.c_int()
        maxi = ct.c_int()
        self._check(self._dll.GetTemperatureRange(ct.byref(mini), ct.byref(maxi)))
        return mini.value+273.15, maxi.value+273.15

    def _get_available_gains(self):
        """ Return a list of the possible preamplifier gains

        @return (list(float)): A list of the gains supported by the camera
        """
        number = ct.c_int()
        self._dll.GetNumberPreAmpGains(ct.byref(number))
        gains = []
        for i in range(number.value):
            gain = ct.c_float()
            self._check(self._dll.GetPreAmpGain(i, ct.byref(gain)))
            gains.append(gain.value)
        return gains

    def _get_available_speeds(self):
        """ Return a list of the possible readout speeds

        @return (list(float)): A list of the readout speeds supported by the camera
        """
        number = ct.c_int()
        self._dll.GetNumberHSSpeeds(0, 0, ct.byref(number))  # Amplification: 0 = electron multiplication, 1 = conventional
        speeds = []
        for i in range(number.value):
            speed = ct.c_float()
            self._check(self._dll.GetHSSpeed(0, 0, i, ct.byref(speed)))  # AD Channel index, Amplification
            speeds.append(speed.value * 1e6)  # DLL talks in MHz
        return speeds

    def _get_available_trigger_modes(self):
        """ Return a list of the trigger mode available to the camera

        @return list(str): A list of the trigger mode available to the dll """
        modes = []
        for mode in TriggerMode:
            status_code = self._dll.IsTriggerModeAvailable(mode.value)  # The answer is encoded in the status code
            if status_code == OK_CODE:
                modes.append(mode.name)
        return modes

    def _has_shutter(self):
        """ Return if the camera have a mechanical shutter installed

        @return (bool): True if the camera have a shutter
        """
        if self._has_external_shutter:
            return True
        result = ct.c_int()
        self._check(self._dll.IsInternalMechanicalShutter(ct.byref(result)))
        return bool(result.value)  # 0: Mechanical shutter not installed, 1: Mechanical shutter installed.

    def _get_current_config(self):
        """ Internal helper method to get the camera parameters in a printable dict.

        @return (dict): dictionary with camera current configuration.
        """
        config = {
            'camera ID..................................': self._get_name(),
            'sensor size (pixels).......................': self._get_image_size(),
            'pixel size (m)............................': self._get_pixel_size(),
            'acquisition mode...........................': self._get_acquisition_mode(),
            'read mode..................................': self.get_read_mode().name,
            'readout speed (Hz).........................': self.get_readout_speed(),
            'gain (x)...................................': self.get_gain(),
            'trigger_mode...............................': self.get_trigger_mode(),
            'exposure_time..............................': self.get_exposure_time(),
            'tracks definition (readmode = RANDOM TRACK)': self.get_active_tracks(),
            'temperature (K)............................': self.get_temperature(),
            'shutter_status.............................': self.get_shutter_state().name,
        }
        return config
class TimeTaggerODMRCounter(Base, ODMRCounterInterface):
    """ Using the TimeTagger as a slow counter.

    # TODO: Write a proper config

    Example config for copy-paste:

    tt_odmr:
        module.Class: 'timetagger__odmr_counter.TimeTaggerODMRCounter'
        timetagger_channel_apd_0: 0
        timetagger_channel_apd_1: 1
        timetagger_channel_trigger: 6

    """

    _modtype = 'TTCounter'
    _modclass = 'hardware'

    _channel_apd_0 = ConfigOption('timetagger_channel_apd_0', missing='error')
    _channel_apd_1 = ConfigOption('timetagger_channel_apd_1', None, missing='warn')
    _cw_channel_trigger = ConfigOption('cw_timetagger_channel_trigger', missing='error')
    _pulsed_channel_trigger = ConfigOption('pulsed_timetagger_channel_trigger', missing='error')
    sweep_length = 100

    def on_activate(self):
        """ Start up TimeTagger interface
        """
        self._tagger = tt.createTimeTagger()

        # Configuring if we are working with one APD or two
        if self._channel_apd_1 is None:
            self._mode = 1
        else:
            self._mode = 2

    def on_deactivate(self):
        """ Shut down the TimeTagger.
        """
        self._tagger.reset()
        return 0

    def set_up_odmr(self, counter_channel=None, photon_source=None,
                    clock_channel=None, odmr_trigger_channel=None):
        """
        Setting up the counters in the time tagger, depending on the mode set to it.
        """

        """
        This is the is the original implementation, that was changed due to difficulty using the combiner channel. It is
        a more elegant solution if these issues are resolved.         
        """

        # Default trigger levels are 0.5V. The channel with the splitter needs a higher trigger to avoid ghost counts.
        # self._tagger.setTriggerLevel(self._channel_apd_0, 1.0)
        # self._tagger.setTriggerLevel(self._channel_apd_1, 1.0)
        # self._tagger.setTriggerLevel(self._channel_trigger, 1.0)

        # Combine channel internally for both continuous and pulsed measurements
        self.trigger_combined = tt.Combiner(tagger=self._tagger,
                                    channels=[self._cw_channel_trigger, self._pulsed_channel_trigger])
        self._channel_trigger = self.trigger_combined.getChannel()

        if self._mode == 1:
            self._channel_clicks = self._channel_apd_0
        if self._mode == 2:
            self.combined = tt.Combiner(tagger=self._tagger,
                                        channels=[self._channel_apd_0, self._channel_apd_1])
            self._channel_clicks = self.combined.getChannel()

        self.triggered_counter = tt.CountBetweenMarkers(tagger=self._tagger,
                                                        click_channel=self._channel_clicks,
                                                        begin_channel=self._channel_trigger,
                                                        n_values=self.sweep_length)
        self.log.info('set up counter with {0} channels and sweep length {1}'.format(self._mode, self.sweep_length))
        return 0

    def get_counter_channels(self):
        return self._channel_clicks

    def get_constraints(self):
        """ Get hardware limits the device

        @return SlowCounterConstraints: constraints class for slow counter

        FIXME: ask hardware for limits when module is loaded
        """
        # TODO: See if someone needs this method, at the moment it is not being used.

        constraints = SlowCounterConstraints()
        constraints.max_detectors = 2
        constraints.min_count_frequency = 1e-3
        constraints.max_count_frequency = 10e9
        constraints.counting_mode = [CountingMode.CONTINUOUS]
        return constraints

    def count_odmr(self, length=100, pulsed=False):
        """ Obtains the counts from the count between markers method of the Time Tagger. Each bin belongs to a different
        frequency in the sweep.
        Returns the counts per second for each bin.
        The length argument is required by the interface, but not used here because it was already defined when the
        counter was initialized.
        """

        t0 = time.time()
        t = 0
        while (not self.triggered_counter.ready()) and (t < 5):
            time.sleep(0.1)
            t = time.time()-t0

        if t >= 5:
            self.log.error('ODMR measurement timed out after {:03.2f} seconds'.format(t))
            err = True
            count_rates = []
        else:
            err = False
            count_array = self.triggered_counter.getData()
            bin_widths = self.triggered_counter.getBinWidths()
            count_rates = np.divide(count_array, (bin_widths * 10 ** -12))

            # The count array intentionally discards the last bin because it isn't terminated by a pulse (at the end of
            # the average_factor number of repetitions. To allow for later reshaping of the array we add another cell
            # with zero. This should have very little effect on the counts.
            if pulsed:
                count_rates = np.append(count_rates, [0])

        return err, count_rates

    def clear_odmr(self):
        """Clear the current measurement in the Time Tagger"""
        self.triggered_counter.clear()
        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 numpy.array(uint32): the photon counts per second
        """

        # TODO: Implement this using counter, which requires setting up a clock and and a Counter

        return 0

    # From here these are methods from the ODMR counter interface that need to be implemented for the Time Tagger

    def close_odmr(self):
        """ Close the odmr and clean up afterwards.

        @return int: error code (0:OK, -1:error)
        """
        self._tagger.reset()
        return 0

    def set_odmr_length(self, length=100):
        """
        Set length of ODMR in pixels. It should be called before set_up_odmr to ensure array is created correctly.

        @param length: Length of ODMR in pixels
        @return: int: error code (0:OK, -1:error)
        """
        self.sweep_length = length
        return 0

    # These are useless methods, but the interface requires them

    def get_odmr_channels(self):
        # This sets the channels in the droplist next to the ODMR plot, but I don't think it is useful.
        ch = [1]
        return ch

    def set_up_odmr_clock(self, clock_frequency=None, clock_channel=None):
        return 0

    def close_odmr_clock(self):
        return 0

    def oversampling(self):
        pass

    def lock_in_active(self):
        pass
Example #19
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

    """

    _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
Example #20
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.

    Example config for copy-paste:

    fpga_switch:
        module.Class: 'switches.ok_fpga.ok_s6_switch.HardwareSwitchFpga'
        fpga_serial: '143400058N'
        fpga_type: 'XEM6310_LX45'  # optional
        path_to_bitfile: <file path>  # optional
        name: 'OpalKelly FPGA Switch'  # optional
        remember_states: True  # optional
        switches:               # optional
            B14: ['Off', 'On']
            B16: ['Off', 'On']
            B12: ['Off', 'On']
             C7: ['Off', 'On']
            D15: ['Off', 'On']
            D10: ['Off', 'On']
             D9: ['Off', 'On']
            D11: ['Off', 'On']
    """

    # config options
    # serial number of the FPGA
    _serial = ConfigOption('fpga_serial', missing='error')
    # Type of the FGPA, possible type options: XEM6310_LX150, XEM6310_LX45
    _fpga_type = ConfigOption('fpga_type', default='XEM6310_LX45', missing='warn')
    # specify the path to the bitfile, if it is not in qudi_main_dir/thirdparty/qo_fpga
    _path_to_bitfile = ConfigOption('path_to_bitfile', default=None, missing='nothing')
    # customize available switches in config. Each switch needs a tuple of 2 state names.
    _switches = ConfigOption(
        name='switches',
        default={s: ('Off', 'On') for s in ('B14', 'B16', 'B12', 'C7', 'D15', 'D10', 'D9', 'D11')},
        missing='nothing'
    )
    # optional name of the hardware
    _hardware_name = ConfigOption(name='name', default='OpalKelly FPGA Switch', missing='nothing')
    # if remember_states is True the last state will be restored at reloading of the module
    _remember_states = ConfigOption(name='remember_states', default=False, missing='nothing')

    # StatusVariable for remembering the last state of the hardware
    _states = StatusVar(name='states', default=None)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self._fpga = None
        self._lock = RecursiveMutex()
        self._connected = False

    def on_activate(self):
        """ Connect and configure the access to the FPGA.
        """
        self._switches = self._chk_refine_available_switches(self._switches)

        # Create an instance of the Opal Kelly FrontPanel
        self._fpga = ok.FrontPanel()
        # Sanity check for fpga_type ConfigOption
        self._fpga_type = self._fpga_type.upper()
        if self._fpga_type not in ('XEM6310_LX45', 'XEM6310_LX150'):
            raise NameError('Unsupported FPGA type "{0}" specified in config. Valid options are '
                            '"XEM6310_LX45" and "XEM6310_LX150".\nAborting module activation.'
                            ''.format(self._fpga_type))
        # connect to the FPGA module
        self._connect()

        # reset states if requested, otherwise use the saved states
        if self._remember_states and isinstance(self._states, dict) and \
                set(self._states) == set(self._switches):
            self._states = {switch: self._states[switch] for switch in self._switches}
            self.states = self._states
        else:
            self._states = dict()
            self.states = {switch: states[0] for switch, states in self._switches.items()}

    def on_deactivate(self):
        """ Deactivate the FPGA.
        """
        del self._fpga
        self._connected = False

    def _connect(self):
        """ Connect host PC to FPGA module with the specified serial number.
        The serial number is defined by the mandatory ConfigOption fpga_serial.
        """
        # 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)

        if not self._path_to_bitfile:
            # upload the proper hardware switch configuration bitfile to the FPGA
            if self._fpga_type == 'XEM6310_LX45':
                bitfile_name = 'switch_8chnl_withcopy_LX45.bit'
            elif self._fpga_type == 'XEM6310_LX150':
                bitfile_name = 'switch_8chnl_withcopy_LX150.bit'
            else:
                self.log.error('Unsupported FPGA type "{0}" specified in config. Valid options are '
                               '"XEM6310_LX45" and "XEM6310_LX150".\nConnection to FPGA module failed.'
                               ''.format(self._fpga_type))
                return -1
            self._path_to_bitfile = os.path.join(get_main_dir(), 'thirdparty', 'qo_fpga', bitfile_name)

        # Load on the FPGA a configuration file (bit file).
        self.log.debug(f'Using bitfile: {self._path_to_bitfile}')
        self._fpga.ConfigureFPGA(self._path_to_bitfile)

        # 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

        self._connected = True
        return 0

    @property
    def name(self):
        """ Name of the hardware as string.

        @return str: The name of the hardware
        """
        return self._hardware_name

    @property
    def available_states(self):
        """ Names of the states as a dict of tuples.

        The keys contain the names for each of the switches. The values are tuples of strings
        representing the ordered names of available states for each switch.

        @return dict: Available states per switch in the form {"switch": ("state1", "state2")}
        """
        return self._switches.copy()

    @property
    def states(self):
        """ The current states the hardware is in as state dictionary with switch names as keys and
        state names as values.

        @return dict: All the current states of the switches in the form {"switch": "state"}
        """
        with self._lock:
            self._fpga.UpdateWireOuts()
            new_state = int(self._fpga.GetWireOutValue(0x20))
            self._states = dict()
            for channel_index, (switch, valid_states) in enumerate(self.available_states):
                if new_state & (1 << channel_index):
                    self._states[switch] = valid_states[1]
                else:
                    self._states[switch] = valid_states[0]
            return self._states.copy()

    @states.setter
    def states(self, state_dict):
        """ The setter for the states of the hardware.

        The states of the system can be set by specifying a dict that has the switch names as keys
        and the names of the states as values.

        @param dict state_dict: state dict of the form {"switch": "state"}
        """
        assert isinstance(state_dict, dict), \
            f'Property "state" must be dict type. Received: {type(state_dict)}'
        assert all(switch in self.available_states for switch in state_dict), \
            f'Invalid switch name(s) encountered: {tuple(state_dict)}'
        assert all(isinstance(state, str) for state in state_dict.values()), \
            f'Invalid switch state(s) encountered: {tuple(state_dict.values())}'

        with self._lock:
            # determine desired state of ALL switches
            new_states = self._states.copy()
            new_states.update(state_dict)
            # encode states into a single int
            new_channel_state = 0
            for channel_index, (switch, state) in enumerate(new_states.items()):
                if state == self.available_states[switch][1]:
                    new_channel_state |= 1 << channel_index

            # apply changes in hardware
            self._fpga.SetWireInValue(0x00, new_channel_state)
            self._fpga.UpdateWireIns()
            # Check for success
            assert self.states == new_states, 'Setting of channel states failed'

    def get_state(self, switch):
        """ Query state of single switch by name

        @param str switch: name of the switch to query the state for
        @return str: The current switch state
        """
        assert switch in self.available_states, 'Invalid switch name "{0}"'.format(switch)
        return self.states[switch]

    def set_state(self, switch, state):
        """ Query state of single switch by name

        @param str switch: name of the switch to change
        @param str state: name of the state to set
        """
        self.states = {switch: state}

    @staticmethod
    def _chk_refine_available_switches(switch_dict):
        """ See SwitchInterface class for details

        @param dict switch_dict:
        @return dict:
        """
        refined = super()._chk_refine_available_switches(switch_dict)
        assert len(refined) == 8, 'Exactly 8 switches or None must be specified in config'
        assert all(len(s) == 2 for s in refined.values()), 'Switches can only take exactly 2 states'
        return refined
Example #21
0
class MicrowaveAnritsu(Base, MicrowaveInterface):
    """ Hardware control file for Anritsu Devices.

    Tested for the model MG3691C with OPTION 2.
    cw and list modes are tested.
    Important: Trigger for frequency sweep is totally independent with most trigger syntax.
            In addition, it has to be Aux I/O pin connection.

    Example config for copy-paste:

    mw_source_anritsu_mg3691c:
        module.Class: 'microwave.mw_source_anritsu_mg3691.MicrowaveAnritsu'
        gpib_address: 'GPIB0::12::INSTR'
        gpib_timeout: 10 # in seconds

    """

    _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.009  # Frequency switching speed in s (acc. to specs)

    def on_activate(self):
        """ Initialisation performed during activation of the module.
        """
        # 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 * 1000)
        except:
            self.log.error('This is MWanritsu: could not connect to the GPIB '
                           'address >>{}<<.'.format(self._gpib_address))
            raise
        self._gpib_connection.write('SYST:LANG "SCPI"')
        self.model = self._gpib_connection.query('*IDN?').split(',')[1]
        self.log.info('MicrowaveAnritsu initialised and connected to '
                      'hardware.')

    def on_deactivate(self):
        """ Deinitialisation performed during deactivation of the module.
        """
        self._gpib_connection.close()
        self.rm.close()

    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 + '*WAI')
        return

    def get_limits(self):
        """ Right now, this is for Anritsu MG3691C with Option 2 only."""
        limits = MicrowaveLimits()
        limits.supported_modes = (MicrowaveMode.CW, MicrowaveMode.LIST)

        limits.min_frequency = 10e6
        limits.max_frequency = 20e9

        limits.min_power = -130
        limits.max_power = 30

        limits.list_minstep = 0.001
        limits.list_maxstep = 10e9
        limits.list_maxentries = 10001

        limits.sweep_minstep = 0.001
        limits.sweep_maxstep = 10e9
        limits.sweep_maxentries = 10001
        if self.model == 'MG3961C':
            limits.max_frequency = 10e9
            limits.min_frequency = 10e6
            limits.min_power = -130
            limits.max_power = 30
        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)
        """
        self._gpib_connection.write('OUTP:STAT OFF')
        while int(
                self._gpib_connection.query('OUTP:STAT?').strip('\r\n')) != 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(self._gpib_connection.query('OUTP:STAT?').strip('\r\n')))
        mode = self._gpib_connection.query(':FREQ:MODE?').strip('\r\n')
        if mode == 'CW':
            mode = 'cw'
        if 'SWE' in mode:
            mode = 'sweep'
        if 'LIST' in mode:
            mode = 'list'
        return mode, is_running

    def get_power(self):
        """
        Gets the microwave output power.

        @return float: the power set at the device in dBm
        """
        if self.get_status()[0] == 'cw':
            power = float(self._gpib_connection.query(':POW?').strip('\r\n'))
        if self.get_status()[0] == 'list':
            self._command_wait(':LIST:IND 0')
            power = self._gpib_connection.query(':LIST:POW?').strip('\r\n')
        if self.get_status()[0] == 'sweep':
            power = self._gpib_connection.query(':POW?').strip('\r\n')
        return power

    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?').strip('\r\n'))
        elif 'sweep' in mode:
            start = float(
                self._gpib_connection.query(':FREQ:STAR?').strip('\r\n'))
            stop = float(
                self._gpib_connection.query(':FREQ:STOP?').strip('\r\n'))
            step = float(
                self._gpib_connection.query(':SWE:FREQ:STEP?').strip('\r\n'))
            return_val = [start, stop, step]
        elif 'list' in mode:
            stop_index = int(
                self._gpib_connection.query(':LIST:STOP?').strip('\r\n'))
            self._gpib_connection.write(':LIST:IND {0}'.format(stop_index))
            stop = float(
                self._gpib_connection.query(':LIST:FREQ?').strip('\r\n'))
            self._gpib_connection.write(':LIST:IND 0')
            start = float(
                self._gpib_connection.query(':LIST:FREQ?').strip('\r\n'))
            step = (stop - start) / stop_index
            return_val = np.arange(start, stop + step, step)
        return return_val

    def cw_on(self):
        """ Switches on any preconfigured microwave output.

        @return int: error code (0:OK, -1:error)
        """
        mode, is_running = self.get_status()
        if is_running:
            if mode == 'cw':
                return 0
            else:
                self.off()

        if mode != 'cw':
            self._command_wait(':FREQ:MODE CW')

        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_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
        @param bool useinterleave: If this mode exists you can choose it.

        @return float, float, str: current frequency in Hz, current power in dBm, current mode

        Interleave option is used for arbitrary waveform generator devices.
        """
        mode, is_running = self.get_status()
        if is_running:
            self.off()

        if mode != 'cw':
            self._command_wait(':FREQ:MODE CW')

        if frequency is not None:
            self._command_wait(':FREQ {0:f}'.format(frequency))

        if power is not None:
            self._command_wait(':POW {0:.2f}'.format(power))

        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)
        """
        mode, is_running = self.get_status()
        if is_running:
            if mode == 'list':
                return 0
            else:
                self.off()

        if mode != 'list':
            self._command_wait(':FREQ:MODE LIST')

        self._gpib_connection.write(':LIST:MODE MNT')

        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_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 list, float, str: current frequencies in Hz, current power in dBm, current mode
        """
        mode, is_running = self.get_status()

        if is_running:
            self.off()

        if mode != 'list':
            self._command_wait(':FREQ:MODE LIST')
        self._command_wait(':LIST:IND 0')

        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._command_wait(':LIST:FREQ' + s)
            self._command_wait(':LIST:STAR 0')
            self._command_wait(':LIST:STOP {0}'.format(len(frequency) - 1))
        self._gpib_connection.write(':LIST:MODE MAN')

        if power is not None:
            self._command_wait(':LIST:IND 0')
            self._command_wait(
                ':LIST:POW {0}'.format((len(frequency) - 1) *
                                       (str(power) + ', ') + str(power)))

        self._command_wait(':LIST:IND 0')

        actual_power = self.get_power()
        actual_freq = self.get_frequency()
        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(':LIST:IND 0')
        return 0

    def sweep_on(self):
        """ Switches on the sweep mode.

        @return int: error code (0:OK, -1:error)
        """

        return -1

    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
        """
        actual_power = self.get_power()
        mode, dummy = self.get_status()
        return -1, -1, -1, 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)
        """

        return -1

    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
        """
        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(':TRIG:SEQ3:SLOP {0}'.format(edge))

        polarity = self._gpib_connection.query(':TRIG:SEQ3:SLOP?').strip(
            '\r\n')
        if polarity == 'NEG':
            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._gpib_connection.write('*TRG')
        time.sleep(self._FREQ_SWITCH_SPEED)  # that is the switching speed
        return 0
Example #22
0
class FastComtec(Base, FastCounterInterface):
    """ Hardware Class for the FastComtec Card.

    This module is also compatible with model 7889 by specifying the model config option.

    Example config for copy-paste:

    fastcomtec_p7887:
        module.Class: 'fastcomtec.fastcomtecp7887.FastComtec'
        gated: False
        trigger_safety: 200e-9
        aom_delay: 400e-9
        minimal_binwidth: 0.25e-9

    """

    gated = ConfigOption('gated', False, missing='warn')
    trigger_safety = ConfigOption('trigger_safety', 200e-9, missing='warn')
    aom_delay = ConfigOption('aom_delay', 400e-9, missing='warn')
    minimal_binwidth = ConfigOption('minimal_binwidth',
                                    0.25e-9,
                                    missing='warn')
    model = ConfigOption('model', '7887')
    use_dma = ConfigOption('use_dma', False)

    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.info('{0}: {1}'.format(key, config[key]))
        #this variable has to be added because there is no difference
        #in the fastcomtec it can be on "stopped" or "halt"
        self.stopped_or_halt = "stopped"
        self.timetrace_tmp = []

    def on_activate(self):
        """ Initialisation performed during activation of the module.
        """
        dll_name = 'dp{}.dll'.format(self.model)
        self.dll = ctypes.windll.LoadLibrary(dll_name)
        if self.gated:
            self.change_sweep_mode(gated=True)
        else:
            self.change_sweep_mode(gated=False)
        return

    def on_deactivate(self):
        """ Deinitialisation performed during deactivation of the module.
        """
        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 seconds use the get_binwidth method.
        constraints['hardware_binwidth_list'] = list(
            self.minimal_binwidth * (2**np.array(np.linspace(0, 24, 25))))
        constraints['max_sweep_len'] = 6.8
        return constraints

    def configure(self,
                  bin_width_s,
                  record_length_s,
                  number_of_gates=0,
                  filename=None):
        """ 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, record_length_s, number_of_gates):
                    binwidth_s: float the actual set binwidth in seconds
                    gate_length_s: the actual record length in seconds
                    number_of_gates: the number of gated, which are accepted,
                    None if not-gated
        """

        # when not gated, record length = total sequence length, when gated, record length = laser length.
        # subtract 200 ns to make sure no sequence trigger is missed
        record_length_FastComTech_s = record_length_s
        if self.gated:
            # add time to account for AOM delay
            no_of_bins = int((record_length_FastComTech_s + self.aom_delay) /
                             self.set_binwidth(bin_width_s))
        else:
            # subtract time to make sure no sequence trigger is missed
            no_of_bins = int(
                (record_length_FastComTech_s - self.trigger_safety) /
                self.set_binwidth(bin_width_s))

        self.set_length(no_of_bins, preset=1, cycles=number_of_gates)

        if filename is not None:
            self._change_filename(filename)

        return self.get_binwidth(
        ), record_length_FastComTech_s, number_of_gates

    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
        """
        status = AcqStatus()
        self.dll.GetStatusData(ctypes.byref(status), 0)
        if status.started == 1:
            return 2
        elif status.started == 0:
            if self.stopped_or_halt == "stopped":
                return 1
            elif self.stopped_or_halt == "halt":
                return 3
            else:
                self.log.error('FastComTec neither stopped nor halt')

                return -1
        else:
            self.log.error(
                'There is an unknown status from FastComtec. The status message was %s'
                % (str(status.started)))
            return -1

    def get_current_runtime(self):
        """
        Returns the current runtime.
        @return float runtime: in s
        """
        status = AcqStatus()
        self.dll.GetStatusData(ctypes.byref(status), 0)
        return status.runtime

    def get_current_sweeps(self):
        """
        Returns the current sweeps.
        @return int sweeps: in sweeps

        The fastcomtec has "start" and "sweep" parameters that are generally equal but might differ depending on the
        configuration. Here the number of trigger events is called "start". This is what is meant by the "sweep"
        parameter of the fast_counter interface.
        """
        status = AcqStatus()
        self.dll.GetStatusData(ctypes.byref(status), 0)
        return status.stevents  # the number of trigger is named "stevents".

    def start_measure(self):
        """Start the measurement. """
        status = self.dll.Start(0)
        while self.get_status() != 2:
            time.sleep(0.05)
        return status

    def pause_measure(self):
        """Make a pause in the measurement, which can be continued. """
        self.stopped_or_halt = "halt"
        status = self.dll.Halt(0)
        while self.get_status() != 3:
            time.sleep(0.05)

        if self.gated:
            self.timetrace_tmp = self.get_data_trace()
        return status

    def stop_measure(self):
        """Stop the measurement. """
        self.stopped_or_halt = "stopped"
        status = self.dll.Halt(0)
        while self.get_status() != 1:
            time.sleep(0.05)

        if self.gated:
            self.timetrace_tmp = []
        return status

    def continue_measure(self):
        """Continue a paused measurement. """
        if self.gated:
            status = self.start_measure()
        else:
            status = self.dll.Continue(0)
            while self.get_status() != 2:
                time.sleep(0.05)
        return status

    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)

        The red out bitshift will be converted to binwidth. The binwidth is
        defined as 2**bitshift*minimal_binwidth.
        """
        return self.minimal_binwidth * (2**int(self.get_bitshift()))

    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 self.gated

    def get_data_trace(self):
        """
        Polls the current timetrace data from the fast counter and returns it as a numpy array (dtype = int64).
        The binning specified by calling configure() must be taken care of in this hardware class.
        A possible overflow of the histogram bins must be caught here and taken care of.
        If the counter is UNgated it will return a 1D-numpy-array with returnarray[timebin_index]
        If the counter is gated it will return a 2D-numpy-array with returnarray[gate_index, timebin_index]

          @return arrray: Time trace.
        """
        setting = AcqSettings()
        self.dll.GetSettingData(ctypes.byref(setting), 0)
        N = setting.range

        if self.gated:
            bsetting = AcqSettings()
            self.dll.GetSettingData(ctypes.byref(bsetting), 0)
            H = bsetting.cycles
            data = np.empty((H, int(N / H)), dtype=np.uint32)

        else:
            data = np.empty((N, ), dtype=np.uint32)

        p_type_ulong = ctypes.POINTER(ctypes.c_uint32)
        ptr = data.ctypes.data_as(p_type_ulong)
        self.dll.LVGetDat(ptr, 0)
        time_trace = np.int64(data)

        if self.gated and self.timetrace_tmp != []:
            time_trace = time_trace + self.timetrace_tmp

        info_dict = {
            'elapsed_sweeps': self.get_current_sweeps(),
            'elapsed_time': None
        }
        return time_trace, info_dict

    def get_data_testfile(self):
        """ Load data test file """
        data = np.loadtxt(
            os.path.join(get_main_dir(), 'tools',
                         'FastComTec_demo_timetrace.asc'))
        time.sleep(0.5)
        return data

    # =========================================================================
    #                           Non Interface methods
    # =========================================================================

    def get_bitshift(self):
        """Get bitshift from Fastcomtec.

        @return int settings.bitshift: the red out bitshift
        """

        settings = AcqSettings()
        self.dll.GetSettingData(ctypes.byref(settings), 0)
        return int(settings.bitshift)

    def set_bitshift(self, bitshift):
        """ Sets the bitshift properly for this card.

        @param int bitshift:

        @return int: asks the actual bitshift and returns the red out value
        """

        cmd = 'BITSHIFT={0}'.format(bitshift)
        self.dll.RunCmd(0, bytes(cmd, 'ascii'))
        return self.get_bitshift()

    def set_binwidth(self, binwidth):
        """ Set defined binwidth in Card.

        @param float binwidth: the current binwidth in seconds

        @return float: Red out bitshift converted to binwidth

        The binwidth is converted into to an appropiate bitshift defined as
        2**bitshift*minimal_binwidth.
        """
        bitshift = int(np.log2(binwidth / self.minimal_binwidth))
        new_bitshift = self.set_bitshift(bitshift)

        return self.minimal_binwidth * (2**new_bitshift)

    #TODO: Check such that only possible lengths are set.
    def set_length(self,
                   length_bins,
                   preset=None,
                   cycles=None,
                   sequences=None):
        """ Sets the length of the length of the actual measurement.

        @param int length_bins: Length of the measurement in bins

        @return float: Red out length of measurement
        """
        constraints = self.get_constraints()
        if length_bins * self.get_binwidth() < constraints['max_sweep_len']:
            cmd = 'RANGE={0}'.format(int(length_bins))
            self.dll.RunCmd(0, bytes(cmd, 'ascii'))
            cmd = 'roimax={0}'.format(int(length_bins))
            self.dll.RunCmd(0, bytes(cmd, 'ascii'))
            if preset is not None:
                cmd = 'swpreset={0}'.format(preset)
                self.dll.RunCmd(0, bytes(cmd, 'ascii'))
            if cycles:
                cmd = 'cycles={0}'.format(cycles)
                self.dll.RunCmd(0, bytes(cmd, 'ascii'))
            if sequences:
                cmd = 'sequences={0}'.format(sequences)
                self.dll.RunCmd(0, bytes(cmd, 'ascii'))
            return self.get_length()
        else:
            self.log.error('Length of sequence is too high: %s' %
                           (str(length_bins * self.get_binwidth())))
            return -1

    def set_preset(self, preset):
        cmd = 'swpreset={0}'.format(preset)
        self.dll.RunCmd(0, bytes(cmd, 'ascii'))
        return preset

    def set_cycles(self, cycles):
        cmd = 'cycles={0}'.format(cycles)
        self.dll.RunCmd(0, bytes(cmd, 'ascii'))
        return cycles

    def get_length(self):
        """ Get the length of the current measurement.

          @return int: length of the current measurement
        """
        setting = AcqSettings()
        self.dll.GetSettingData(ctypes.byref(setting), 0)
        return int(setting.range)

    def _change_filename(self, name):
        """ Changed the name in FCT"""
        cmd = 'datname=%s' % name
        self.dll.RunCmd(0, bytes(cmd, 'ascii'))
        return name

    def change_sweep_mode(self, gated):
        if gated:
            number = 1978500
            number = number + 32 if self.use_dma else number
            cmd = 'sweepmode={0}'.format(hex(number))
            self.dll.RunCmd(0, bytes(cmd, 'ascii'))
            cmd = 'prena={0}'.format(hex(16))  #To select starts preset
            # cmd = 'prena={0}'.format(hex(4)) #To select sweeps preset
            self.dll.RunCmd(0, bytes(cmd, 'ascii'))
            self.gated = True
        else:
            # fastcomtch standard settings for ungated acquisition (check manual)
            number = 1978496
            number = number + 32 if self.use_dma else number
            cmd = 'sweepmode={0}'.format(hex(number))
            self.dll.RunCmd(0, bytes(cmd, 'ascii'))
            cmd = 'prena={0}'.format(hex(0))
            self.dll.RunCmd(0, bytes(cmd, 'ascii'))
            self.gated = False
        return gated

    def change_save_mode(self, mode):
        """ Changes the save mode of p7887

        @param int mode: Specifies the save mode (0: No Save at Halt, 1: Save at Halt,
                        2: Write list file, No Save at Halt, 3: Write list file, Save at Halt

        @return int mode: specified save mode
        """
        cmd = 'savedata={0}'.format(mode)
        self.dll.RunCmd(0, bytes(cmd, 'ascii'))
        return mode

    def set_delay_start(self, delay_s):
        """ Sets the record delay length

        @param int delay_s: Record delay after receiving a start trigger

        @return int mode: specified save mode
        """

        # A delay can only be adjusted in steps of 6.4ns
        delay_bins = np.rint(delay_s / 6.4e-9 / 2.5)
        cmd = 'fstchan={0}'.format(int(delay_bins))
        self.dll.RunCmd(0, bytes(cmd, 'ascii'))
        return delay_bins

    def get_delay_start(self):
        """ Returns the current record delay length

        @return float delay_s: current record delay length in seconds
        """
        bsetting = AcqSettings()
        self.dll.GetSettingData(ctypes.byref(bsetting), 0)
        delay_s = bsetting.fstchan * 6.4e-9 * 2.5
        #prena = bsetting.prena
        return delay_s

    # =========================================================================
    #   The following methods have to be carefully reviewed and integrated as
    #   internal methods/function, because they might be important one day.
    # =========================================================================

    def SetDelay(self, t):
        #~ setting = AcqSettings()
        #~ self.dll.GetSettingData(ctypes.byref(setting), 0)
        #~ setting.fstchan = t/6.4
        #~ self.dll.StoreSettingData(ctypes.byref(setting), 0)
        #~ self.dll.NewSetting(0)
        self.dll.RunCmd(0, 'DELAY={0:f}'.format(t))
        return self.GetDelay()

    def GetDelay(self):
        setting = AcqSettings()
        self.dll.GetSettingData(ctypes.byref(setting), 0)
        return setting.fstchan * 6.4

    #former SaveData_fast
    def SaveData_locally(self, filename, laser_index):
        # os.chdir(r'D:\data\FastComTec')
        data = self.get_data()
        fil = open(filename + '.asc', 'w')
        for i in laser_index:
            for n in data[i:i + int(
                    round(3000 /
                          (self.minimal_binwidth * 2**self.GetBitshift()))) +
                          int(
                              round(1000 / (self.minimal_binwidth *
                                            2**self.GetBitshift())))]:
                fil.write('{0!s}\n'.format(n))
        fil.close()

    def SetLevel(self, start, stop):
        setting = AcqSettings()
        self.dll.GetSettingData(ctypes.byref(setting), 0)

        def FloatToWord(r):
            return int((r + 2.048) / 4.096 * int('ffff', 16))

        setting.dac0 = (setting.dac0
                        & int('ffff0000', 16)) | FloatToWord(start)
        setting.dac1 = (setting.dac1 & int('ffff0000', 16)) | FloatToWord(stop)
        self.dll.StoreSettingData(ctypes.byref(setting), 0)
        self.dll.NewSetting(0)
        return self.GetLevel()

    def GetLevel(self):
        setting = AcqSettings()
        self.dll.GetSettingData(ctypes.byref(setting), 0)

        def WordToFloat(word):
            return (word & int('ffff', 16)) * 4.096 / int('ffff', 16) - 2.048

        return WordToFloat(setting.dac0), WordToFloat(setting.dac1)

    #used in one script for SSR
    #Todo: Remove
    def Running(self):
        s = self.GetStatus()
        return s.started

    def GetStatus(self):
        status = AcqStatus()
        self.dll.GetStatusData(ctypes.byref(status), 0)
        return status

    def load_setup(self, configname):
        cmd = 'loadcnf={0}'.format(configname)
        self.dll.RunCmd(0, bytes(cmd, 'ascii'))
Example #23
0
class HbtLogic(GenericLogic):
    """
    This is the logic for running HBT experiments
    """
    _modclass = 'hbtlogic'
    _modtype = 'logic'

    _channel_apd_0 = ConfigOption('timetagger_channel_apd_0', missing='error')
    _channel_apd_1 = ConfigOption('timetagger_channel_apd_1', missing='error')
    _bin_width = ConfigOption('bin_width', 500, missing='info')
    _n_bins = ConfigOption('bins', 2000, missing='info')
    savelogic = Connector(interface='SaveLogic')

    hbt_updated = QtCore.Signal()
    hbt_fit_updated = QtCore.Signal()
    hbt_saved = QtCore.Signal()
    sigStart = QtCore.Signal()
    sigStop = QtCore.Signal()

    def __init__(self, config, **kwargs):
        super().__init__(config=config, **kwargs)
        self.fit_times = []
        self.bin_times = []
        self.fit_g2 = []
        self.g2_data = []
        self.g2_data_normalised = []
        self.hbt_available = False
        self._setup_measurement()
        self._close_measurement()

    def on_activate(self):
        """ Connect and configure the access to the FPGA.
        """
        self._save_logic = self.get_connector('savelogic')
        self._number_of_gates = int(100)
        self.g2_data = np.zeros_like(self.bin_times)
        self.g2_data_normalised = np.zeros_like(self.bin_times)
        self.fit_times = self.bin_times
        self.fit_g2 = np.zeros_like(self.fit_times)

        self.timer = QtCore.QTimer()
        self.timer.timeout.connect(self.update)

        self.sigStart.connect(self._start_hbt)
        self.sigStop.connect(self._stop_hbt)

    def _setup_measurement(self):
        self._tagger = tt.createTimeTagger()
        self.coin = tt.Correlation(self._tagger,
                                   self._channel_apd_0,
                                   self._channel_apd_1,
                                   binwidth=self._bin_width,
                                   n_bins=self._n_bins)
        self.bin_times = self.coin.getIndex()

    def _close_measurement(self):
        self.coin.stop()
        self.coin = None
        self._tagger = None

    def start_hbt(self):
        self.sigStart.emit()

    def stop_hbt(self):
        self.sigStop.emit()

    def _start_hbt(self):
        self._setup_measurement()
        self.coin.clear()
        self.coin.start()
        self.timer.start(500)  # 0.5s

    def update(self):
        self.bin_times = self.coin.getIndex()
        self.g2_data = self.coin.getData()
        self.hbt_available = True
        lvl = np.mean(self.g2_data[0:100])
        if lvl > 0:
            self.g2_data_normalised = self.g2_data / lvl
        else:
            self.g2_data_normalised = np.zeros_like(self.g2_data)
        self.hbt_updated.emit()

    def pause_hbt(self):
        if self.coin is not None:
            self.coin.stop()

    def continue_hbt(self):
        if self.coin is not None:
            self.coin.start()

    def _stop_hbt(self):
        if self.coin is not None:
            self._close_measurement()

        self.timer.stop()

    def fit_data(self):
        pass
        # model, param = self.fitlogic.make_hyperbolicsaturation_model()
        # param['I_sat'].min = 0
        # param['I_sat'].max = 1e7
        # param['I_sat'].value = max(self.psat_data) * .7
        # param['P_sat'].max = 100.0
        # param['P_sat'].min = 0.0
        # param['P_sat'].value = 1.0
        # param['slope'].min = 0.0
        # param['slope'].value = 1e3
        # param['offset'].min = 0.0
        # fit = self.fitlogic.make_hyperbolicsaturation_fit(x_axis=self.psat_powers, data=self.psat_data,
        #                                                   estimator=self.fitlogic.estimate_hyperbolicsaturation,
        #                                                   add_params=param)
        # self.fit = fit
        # self.fitted_Psat = fit.best_values['P_sat']
        # self.fitted_Isat = fit.best_values['I_sat']

    def save_hbt(self):
        # File path and name
        filepath = self._save_logic.get_path_for_module(module_name='HBT')

        # We will fill the data OrderedDict to send to savelogic
        data = OrderedDict()
        data['Time (ns)'] = np.array(self.bin_times)
        data['g2(t)'] = np.array(self.g2_data)
        data['g2(t) normalised'] = np.array(self.g2_data_normalised)

        self._save_logic.save_data(data,
                                   filepath=filepath,
                                   filelabel='g2data',
                                   fmt=['%.6e', '%.6e', '%.6e'])
        self.log.debug('HBT data saved to:\n{0}'.format(filepath))

        self.hbt_saved.emit()
        return 0

    def on_deactivate(self):
        """ Reverse steps of activation

        @return int: error code (0:OK, -1:error)
        """
        return 0
Example #24
0
class ATC204(Base, ProcessInterface, ProcessControlInterface):
    """ Methods to control Arios ATC-204(東邦電子 TTM-204).
    https://dia-pe-titech.esa.io/posts/158

    Example config for copy-paste:

    APC:
        module.Class: 'ATC204_pressure.ATC204'
        port: 'COM4'
        baudrate: 9600
        timeout: 100

        limit_min: 0    # Pa
        limit_max: 40e3 # Pa
    """

    _port = ConfigOption('port')
    _baudrate = ConfigOption('baudrate', 9600, missing='warn')
    _timeout = ConfigOption('timeout', 10, missing='warn')

    _limit_min = ConfigOption('limit_min', 0, missing='warn')
    _limit_max = ConfigOption('limit_max', 40e3, missing='warn')

    CMD_RETRY_NUM = 10
    CMD_ZERO_NUM = 10

    STX = b'\x02'
    ETX = b'\x03'

    def on_activate(self):
        """ Activate module.
        """
        self._serial_connection = serial.Serial(
            port=self._port,
            baudrate=self._baudrate,
            bytesize=serial.EIGHTBITS,
            parity=serial.PARITY_NONE,
            stopbits=serial.STOPBITS_TWO,
            timeout=self._timeout)

        self._openflag = self._serial_connection.is_open
        if not(self._openflag):
            self._serial_connection.open()

        # なぜか初期値が0.2になってしまうのでリセット
        if self.get_control_value() == .2e3:
            self.set_control_value(0.0)

    def on_deactivate(self):
        """ Deactivate module.
        """
        if not(self._openflag):
            self._serial_connection.close()

    def get_process_unit(self, channel=None):
        """ Process unit, here Pa.

            @return float: process unit
        """
        return 'Pa', 'pascal'

    def get_control_unit(self, channel=None):
        """ Get unit of control value.

            @return tuple(str): short and text unit of control value
        """
        return 'Pa', 'pascal'

    def get_control_limit(self, channel=None):
        """ Get minimum and maximum of control value.

            @return tuple(float, float): minimum and maximum of control value
        """
        return self._limit_min, self._limit_max

    def get_minimal_step(self):
        return 100.0

    def get_process_value(self, channel=None):
        """ Process value, here temperature.

            @return float: process value
        """
        return self._send_cmd('01RPV1') * 1e3

    def get_control_value(self, channel=None):
        """ Get current control value, here heating power

            @return float: current control value
        """
        return self._send_cmd('01RSV1') * 1e3

    def set_control_value(self, value, channel=None):
        """ Set control value, here heating power.

            @param flaot value: control value
        """

        set_value = round(value / 1e3 * 10)
        cmd = '01WSV1' + str(set_value).zfill(5)
        # '01WSV1' + '{:0=5}'.format( set_value ) でも同じ結果になる

        return self._send_cmd(cmd) * 1e3
        # print(cmd)
        # return True

    def _send_cmd(self, cmd):
        """ Send command
        """

        cmd = self.STX + cmd.encode() + self.ETX

        bcc = 0
        for data in cmd:
            bcc = bcc ^ data

        final_cmd = cmd + bytes([bcc])

        for i in range(self.CMD_RETRY_NUM):
            # if self._serial_connection.out_waiting > 0:
            #    self._serial_connection.reset_output_buffer()

            self._serial_connection.write(final_cmd)
            res = self._read_data()

            if not res:
                return False
            elif res == b'\x0201\x06\x03\x06':
                return True
            else:
                try:
                    return float(res[7:12].decode()) / 10
                except ValueError:
                    # clear receive buffer
                    self._serial_connection.reset_input_buffer()
                    self.log.warning('clear receive buffer')

        self.log.warning('cmd res error:', res)
        return False

    def _read_data(self):
        in_reading = True
        in_resiving_data = False

        _num_of_zero = 0
        data = b''

        while in_reading:
            res = self._serial_connection.read(1)

            if res == b'\x00' or res == b'':
                _num_of_zero += 1
                if _num_of_zero >= self.CMD_ZERO_NUM:
                    self.log.error(
                        self.__class__.__name__ +
                        '> Communication Error: This hardware might be off!')
                    return False
            elif res == self.STX:
                in_resiving_data = True
                data = res
            elif in_resiving_data:
                if res == self.ETX:
                    in_resiving_data = False
                    in_reading = False
                data += res

        # BCC
        res = self._serial_connection.read(1)
        data += res

        return data
Example #25
0
class PM100D(Base, SimpleDataInterface, ProcessInterface):
    """ 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 having activated qudi environment
    """

    _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

    def get_process_value(self):
        """ Return a measured value """
        return self.get_power()

    def get_process_unit(self):
        """ Return the unit that hte value is measured in as a tuple of ('abreviation', 'full unit name') """
        return ('W', 'watt')

    def get_wavelength(self):
        """ Return the current wavelength in nanometers """
        return self._power_meter.sense.correction.wavelength

    def set_wavelength(self, value=None):
        """ Set the new wavelength in nanometers """
        mini, maxi = self.get_wavelength_range()
        if value is not None:
            if mini <= value <= maxi:
                self._power_meter.sense.correction.wavelength = value
            else:
                self.log.error(
                    'Wavelength {} is out of the range [{}, {}].'.format(
                        value, mini, maxi))
        return self.get_wavelength()

    def get_wavelength_range(self):
        """ Return the wavelength range of the power meter in nanometers """
        return self._power_meter.sense.correction.minimum_beamdiameter,\
               self._power_meter.sense.correction.maximum_wavelength

    def set_autorange(self, value=1):
        """ Set the powermeter power range in automatic"""
        if value == 1:
            self._power_meter.sense.power.dc.range.auto = 1
        elif value == 0:
            self._power_meter.sense.power.dc.range.auto = 0
        else:
            print("not a valid command, autorange is {}".format(
                self._power_meter.sense.power.dc.range.auto))
        return self._power_meter.sense.power.dc.range.auto
Example #26
0
class AutofocusLogic(GenericLogic):
    """ This logic connect to the instruments necessary for the autofocus method based on the FPGA + QPD. This logic
    can be accessed from the focus_logic controlling the piezo position.

    autofocus_logic:
        module.Class: 'autofocus_logic.AutofocusLogic'
        autofocus_ref_axis : 'X' # 'Y'
        proportional_gain : 0.1 # in %%
        integration_gain : 1 # in %%
        exposure = 0.001
        connect:
            camera : 'thorlabs_camera'
            fpga: 'nifpga'
            stage: 'ms2000'
    """

    # declare connectors
    fpga = Connector(interface='FPGAInterface')  # to check _ a new interface was defined for FPGA connection
    stage = Connector(interface='MotorInterface')
    camera = Connector(interface='CameraInterface')

    # camera attributes
    _exposure = ConfigOption('exposure', 0.001, missing='warn')
    _camera_acquiring = False
    _threshold = None  # for compatibility with focus logic, not used

    # autofocus attributes
    _focus_offset = ConfigOption('focus_offset', 0, missing='warn')
    _ref_axis = ConfigOption('autofocus_ref_axis', 'X', missing='warn')
    _autofocus_stable = False
    _autofocus_iterations = 0

    # pid attributes
    _pid_frequency = 0.2  # in s, frequency for the autofocus PID update
    _P_gain = ConfigOption('proportional_gain', 0, missing='warn')
    _I_gain = ConfigOption('integration_gain', 0, missing='warn')
    _setpoint = None

    _last_pid_output_values = np.zeros((10,))

    # signals
    sigOffsetDefined = QtCore.Signal()
    sigStageMoved = QtCore.Signal()

    def __init__(self, config, **kwargs):
        super().__init__(config=config, **kwargs)
        # self.threadpool = QtCore.QThreadPool()

    def on_activate(self):
        """ Initialisation performed during activation of the module.
        """
        # hardware connections
        self._fpga = self.fpga()
        self._stage = self.stage()
        self._camera = self.camera()
        self._camera.set_exposure(self._exposure)

    def on_deactivate(self):
        """ Required deactivation.
        """
        self.stop_camera_live()

# =======================================================================================
# Public method for the autofocus, used by all the methods (camera or FPGA/QPD based)
# =======================================================================================
        
    def read_detector_signal(self):
        """ General function returning the reference signal for the autofocus correction. In the case of the
        method using a FPGA, it returns the QPD signal measured along the reference axis.
        """
        return self.qpd_read_position()

    # fpga only
    def read_detector_intensity(self):
        """ Function used for the focus search. Measured the intensity of the reflection instead of reading its
        position
        """
        return self.qpd_read_sum()

    def autofocus_check_signal(self):
        """ Check that the intensity detected by the QPD is above a specific threshold (300). If the signal is too low,
        the function returns False to indicate that the autofocus signal is lost.
        :return bool: True: signal ok, False: signal too low
        """
        qpd_sum = self.qpd_read_sum()
        print(qpd_sum)
        if qpd_sum < 300:
            return False
        else:
            return True

    def define_pid_setpoint(self):
        """ Initialize the pid setpoint
        """
        self.qpd_reset()
        self._setpoint = self.read_detector_signal()
        return self._setpoint

    def init_pid(self):
        """ Initialize the pid for the autofocus
        """
        self.qpd_reset()
        self._fpga.init_pid(self._P_gain, self._I_gain, self._setpoint, self._ref_axis)
        self.set_worker_frequency()

        self._autofocus_stable = False
        self._autofocus_iterations = 0

    def read_pid_output(self, check_stabilization):
        """ Read the pid output signal in order to adjust the position of the objective
        """
        pid_output = self._fpga.read_pid()

        if check_stabilization:
            self._autofocus_iterations += 1
            self._last_pid_output_values = np.concatenate((self._last_pid_output_values[1:10], [pid_output]))
            return pid_output, self.check_stabilization()
        else:
            return pid_output

    def check_stabilization(self):
        """ Check for the stabilization of the focus
        """
        if self._autofocus_iterations > 10:
            p = Poly.fit(np.linspace(0, 9, num=10), self._last_pid_output_values, deg=1)
            slope = p(9) - p(0)
            if np.absolute(slope) < 10:
                self._autofocus_stable = True
            else:
                self._autofocus_stable = False

        return self._autofocus_stable

    def start_camera_live(self):
        """ Launch live acquisition of the camera
        """
        self._camera.start_live_acquisition()
        self._camera_acquiring = True

    def stop_camera_live(self):
        """ Stop live acquisition of the camera
        """
        self._camera.stop_acquisition()
        self._camera_acquiring = False

    def get_latest_image(self):
        """ Get the latest acquired image from the camera. This function returns the raw image as well as the
        threshold image
        """
        im = self._camera.get_acquired_data()
        return im

    def calibrate_offset(self):
        """ Calibrate the offset between the sample position and a reference on the bottom of the coverslip. This method
        is inspired from the LSM-Zeiss microscope and is used when the sample (such as embryos) is interfering too much
        with the IR signal and makes the regular focus stabilization unstable.
        """
        # Read the stage position
        z_up = self._stage.get_pos()['z']
        offset = self._focus_offset

        # Move the stage by the default offset value along the z-axis
        self.stage_move_z(offset)

        # rescue autofocus when no signal detected
        if not self.autofocus_check_signal():
            self.rescue_autofocus()

        # Look for the position with the maximum intensity - for the QPD the SUM signal is used.
        max_sum = 0
        z_range = 5  # in µm
        z_step = 0.1  # in µm

        self.stage_move_z(-z_range/2)

        for n in range(int(z_range/z_step)):

            self.stage_move_z(z_step)

            sum = self.read_detector_intensity()
            print(sum)
            if sum > max_sum:
                max_sum = sum
            elif sum < max_sum and max_sum > 500:
                break

        # Calculate the offset for the stage and move back to the initial position
        offset = self._stage.get_pos()['z'] - z_up
        offset = np.round(offset, decimals=1)

        # avoid moving stage while QPD signal is read
        sleep(0.1)

        self.stage_move_z(-(offset))

        # send signal to focus logic that will be linked to define_autofocus_setpoint
        self.sigOffsetDefined.emit()

        self._focus_offset = offset

        return offset

    def rescue_autofocus(self):
        """ When the autofocus signal is lost, launch a rescuing procedure by using the MS2000 translation stage. The
        z position of the stage is moved until the piezo signal is found again.
        """
        success = False
        z_range = 20
        while not self.autofocus_check_signal() and z_range <= 40:

            self.stage_move_z(-z_range/2)

            for z in range(z_range):
                step = 1
                self.stage_move_z(step)

                if self.autofocus_check_signal():
                    success = True
                    print("autofocus signal found!")
                    return success

            if not self.autofocus_check_signal():
                self.stage_move_z(-z_range/2)
                z_range += 10

        return success

    def stage_move_z(self, step):
        self._stage.move_rel({'z': step})

    def do_position_correction(self, step):
        self._stage.set_velocity({'z': 0.01})
        sleep(1)
        self.stage_move_z(step)
        self._stage.wait_for_idle()
        self._stage.set_velocity({'z': 1.9})
        self.sigStageMoved.emit()

# ----------------to be completed----------------------------------
#     def start_piezo_position_correction(self, direction):
#         """ When the piezo position gets too close to the limits, the MS2000 stage is used to move the piezo back
#         to a standard position. If the piezo close to the lower limit (<5µm) it is moved to 25µm. If the piezo is too
#         close to the upper limit (>70µm), it is moved back to 50µm.
#         """
#
#         if direction == "up":
#             step = 1
#         elif direction == "down":
#             step = -1
#         else:
#             self.log.warning('no valid direction specified')
#             return
#
#         self._pos_dict = {'z': step}
#
#         # this needs to be modified :
#         if not self._run_autofocus:
#             self.start_autofocus()
#
#         stage_worker = StageAutofocusWorker()
#         stage_worker.signals.sigFinished.connect(self.run_piezo_position_correction)
#         self.threadpool.start(stage_worker)
#
#     def run_piezo_position_correction(self):
#         """ Correct the piezo position by moving the MS2000 stage while the autofocus ON
#         """
#         z = self.get_position()  # z posiiton of the piezo -- to be modified, this needs to be passed in by the focus_logic
#         # if not self._autofocus_lost and (z < 25 or z > 50):
#         if not self.autofocus_check_signal and (z < 25 or z > 50):
#             self._stage.move_rel(self._pos_dict)
#
#             stage_worker = StageAutofocusWorker()
#             stage_worker.signals.sigFinished.connect(self.run_piezo_position_correction)
#             self.threadpool.start(stage_worker)
# ---------------------------------------------------------------

# =================================================================
# private methods for QPD-based autofocus
# =================================================================

    def qpd_read_position(self):
        """ Read the QPD signal from the FPGA. The signal is read from X/Y positions. In order to make sure we are
        always reading from the latest piezo position, the method is waiting for a new count.
        """
        qpd = self._fpga.read_qpd()
        last_count = qpd[3]
        while last_count == qpd[3]:
            qpd = self._fpga.read_qpd()
            sleep(0.01)

        if self._ref_axis == 'X':
            return qpd[0]
        elif self._ref_axis == 'Y':
            return qpd[1]

    def qpd_read_sum(self):
        """ Read the SUM signal from the QPD. Returns an indication whether there is a detected signal or not
        """
        qpd = self._fpga.read_qpd()
        return qpd[2]

    def set_worker_frequency(self):
        """ Update the worker frequency according to the iteration time of the fpga
        """
        qpd = self._fpga.read_qpd()
        self._pid_frequency = qpd[4] / 1000 + 0.01

    def qpd_reset(self):
        """ Reset the QPD counter
        """
        self._fpga.reset_qpd_counter()
Example #27
0
class ODMRLogic(GenericLogic):
    """This is the Logic class for ODMR."""

    # declare connectors
    odmrcounter = Connector(interface='ODMRCounterInterface')
    fitlogic = Connector(interface='FitLogic')
    microwave1 = Connector(interface='MicrowaveInterface')
    savelogic = Connector(interface='SaveLogic')
    taskrunner = Connector(interface='TaskRunner')

    # config option
    mw_scanmode = ConfigOption(
                    'scanmode',
                    'LIST',
                    missing='warn',
                    converter=lambda x: MicrowaveMode[x.upper()])

    clock_frequency = StatusVar('clock_frequency', 200)
    cw_mw_frequency = StatusVar('cw_mw_frequency', 2870e6)
    cw_mw_power = StatusVar('cw_mw_power', -30)
    sweep_mw_power = StatusVar('sweep_mw_power', -30)
    mw_start = StatusVar('mw_start', 2800e6)
    mw_stop = StatusVar('mw_stop', 2950e6)
    mw_step = StatusVar('mw_step', 2e6)
    run_time = StatusVar('run_time', 60)
    number_of_lines = StatusVar('number_of_lines', 50)
    fc = StatusVar('fits', None)
    lines_to_average = StatusVar('lines_to_average', 0)
    _oversampling = StatusVar('oversampling', default=10)
    _lock_in_active = StatusVar('lock_in_active', default=False)

    # Internal signals
    sigNextLine = QtCore.Signal()

    # Update signals, e.g. for GUI module
    sigParameterUpdated = QtCore.Signal(dict)
    sigOutputStateUpdated = QtCore.Signal(str, bool)
    sigOdmrPlotsUpdated = QtCore.Signal(np.ndarray, np.ndarray, np.ndarray)
    sigOdmrFitUpdated = QtCore.Signal(np.ndarray, np.ndarray, dict, str)
    sigOdmrElapsedTimeUpdated = QtCore.Signal(float, int)

    def __init__(self, config, **kwargs):
        super().__init__(config=config, **kwargs)
        self.threadlock = Mutex()

    def on_activate(self):
        """
        Initialisation performed during activation of the module.
        """
        # Get connectors
        self._mw_device = self.microwave1()
        self._fit_logic = self.fitlogic()
        self._odmr_counter = self.odmrcounter()
        self._save_logic = self.savelogic()
        self._taskrunner = self.taskrunner()

        # Get hardware constraints
        limits = self.get_hw_constraints()

        # Set/recall microwave source parameters
        self.cw_mw_frequency = limits.frequency_in_range(self.cw_mw_frequency)
        self.cw_mw_power = limits.power_in_range(self.cw_mw_power)
        self.sweep_mw_power = limits.power_in_range(self.sweep_mw_power)
        self.mw_start = limits.frequency_in_range(self.mw_start)
        self.mw_stop = limits.frequency_in_range(self.mw_stop)
        self.mw_step = limits.list_step_in_range(self.mw_step)
        self._odmr_counter.oversampling = self._oversampling
        self._odmr_counter.lock_in_active = self._lock_in_active

        # Set the trigger polarity (RISING/FALLING) of the mw-source input trigger
        # theoretically this can be changed, but the current counting scheme will not support that
        self.mw_trigger_pol = TriggerEdge.RISING
        self.set_trigger(self.mw_trigger_pol, self.clock_frequency)

        # Elapsed measurement time and number of sweeps
        self.elapsed_time = 0.0
        self.elapsed_sweeps = 0
        

        # Set flags
        # for stopping a measurement
        self._stopRequested = False
        # in case of sweep parameters being updated, so the data arrays need
        # to be resized
        self._sweep_params_updated = False

        # Initalize the ODMR data arrays (mean signal and sweep matrix)
        self._initialize_odmr_plots()

        # Switch off microwave and set CW frequency and power
        self.mw_off()
        self.set_cw_parameters(self.cw_mw_frequency, self.cw_mw_power)

        # Connect signals
        self.sigNextLine.connect(self._scan_odmr_line, QtCore.Qt.QueuedConnection)
        return

    def on_deactivate(self):
        """ Deinitialisation performed during deactivation of the module.
        """
        # Stop measurement if it is still running
        if self.module_state() == 'locked':
            self.stop_odmr_scan()
        timeout = 30.0
        start_time = time.time()
        while self.module_state() == 'locked':
            time.sleep(0.5)
            timeout -= (time.time() - start_time)
            if timeout <= 0.0:
                self.log.error('Failed to properly deactivate odmr logic. Odmr scan is still '
                               'running but can not be stopped after 30 sec.')
                break
        # Switch off microwave source for sure (also if CW mode is active or module is still locked)
        self._mw_device.off()
        # Disconnect signals
        self.sigNextLine.disconnect()

    @fc.constructor
    def sv_set_fits(self, val):
        # Setup fit container
        fc = self.fitlogic().make_fit_container('ODMR sum', '1d')
        fc.set_units(['Hz', 'c/s'])
        if isinstance(val, dict) and len(val) > 0:
            fc.load_from_dict(val)
        else:
            d1 = OrderedDict()
            d1['Lorentzian dip'] = {
                'fit_function': 'lorentzian',
                'estimator': 'dip'
                }
            d1['Two Lorentzian dips'] = {
                'fit_function': 'lorentziandouble',
                'estimator': 'dip'
                }
            d1['N14'] = {
                'fit_function': 'lorentziantriple',
                'estimator': 'N14'
                }
            d1['N15'] = {
                'fit_function': 'lorentziandouble',
                'estimator': 'N15'
                }
            d1['Two Gaussian dips'] = {
                'fit_function': 'gaussiandouble',
                'estimator': 'dip'
                }
            default_fits = OrderedDict()
            default_fits['1d'] = d1
            fc.load_from_dict(default_fits)
        return fc

    @fc.representer
    def sv_get_fits(self, val):
        """ save configured fits """
        if len(val.fit_list) > 0:
            return val.save_to_dict()
        else:
            return None

    def _initialize_odmr_plots(self):
        """ Initializing the ODMR plots (line and matrix). """

        self.elapsed_sweeps = 0
        self.elapsed_time = 0.0
        self.sigOdmrElapsedTimeUpdated.emit(self.elapsed_time, self.elapsed_sweeps)
        
        self.odmr_plot_x = np.arange(self.mw_start, self.mw_stop + self.mw_step, self.mw_step)
        self.odmr_plot_y = np.zeros([len(self.get_odmr_channels()), self.odmr_plot_x.size])
        self.odmr_fit_x = np.arange(self.mw_start, self.mw_stop + self.mw_step, self.mw_step)
        self.odmr_fit_y = np.zeros(self.odmr_fit_x.size)
        self.odmr_plot_xy = np.zeros(
            [self.number_of_lines, len(self.get_odmr_channels()), self.odmr_plot_x.size])
        self.sigOdmrPlotsUpdated.emit(self.odmr_plot_x, self.odmr_plot_y, self.odmr_plot_xy)
        self.odmr_raw_data = np.zeros(
                    [1,
                    len(self._odmr_counter.get_odmr_channels()),
                    self.odmr_plot_x.size]
                )
        current_fit = self.fc.current_fit
        self.sigOdmrFitUpdated.emit(self.odmr_fit_x, self.odmr_fit_y, {}, current_fit)
        return

    def set_trigger(self, trigger_pol, frequency):
        """
        Set trigger polarity of external microwave trigger (for list and sweep mode).

        @param object trigger_pol: one of [TriggerEdge.RISING, TriggerEdge.FALLING]
        @param float frequency: trigger frequency during ODMR scan

        @return object: actually set trigger polarity returned from hardware
        """
        if self._lock_in_active:
            frequency = frequency / self._oversampling

        if self.module_state() != 'locked':
            self.mw_trigger_pol, triggertime = self._mw_device.set_ext_trigger(trigger_pol, 1/frequency)
        else:
            self.log.warning('set_trigger failed. Logic is locked.')

        update_dict = {'trigger_pol': self.mw_trigger_pol}
        self.sigParameterUpdated.emit(update_dict)
        return self.mw_trigger_pol

    def set_average_length(self, lines_to_average):
        """
        Sets the number of lines to average for the sum of the data

        @param int lines_to_average: desired number of lines to average (0 means all)

        @return int: actually set lines to average
        """
        self.lines_to_average = int(lines_to_average)

        if self.lines_to_average <= 0:
            self.odmr_plot_y = np.mean(
                self.odmr_raw_data[:max(1, self.elapsed_sweeps), :, :],
                axis=0,
                dtype=np.float64
            )
        else:
            self.odmr_plot_y = np.mean(
                self.odmr_raw_data[:max(1, min(self.lines_to_average, self.elapsed_sweeps)), :, :],
                axis=0,
                dtype=np.float64
            )

        self.sigOdmrPlotsUpdated.emit(self.odmr_plot_x, self.odmr_plot_y, self.odmr_plot_xy)
        self.sigParameterUpdated.emit({'average_length': self.lines_to_average})
        return self.lines_to_average

    def set_clock_frequency(self, clock_frequency):
        """
        Sets the frequency of the counter clock

        @param int clock_frequency: desired frequency of the clock

        @return int: actually set clock frequency
        """
        # checks if scanner is still running
        if self.module_state() != 'locked' and isinstance(clock_frequency, (int, float)):
            self.clock_frequency = int(clock_frequency)
        else:
            self.log.warning('set_clock_frequency failed. Logic is either locked or input value is '
                             'no integer or float.')

        update_dict = {'clock_frequency': self.clock_frequency}
        self.sigParameterUpdated.emit(update_dict)
        return self.clock_frequency

    @property
    def oversampling(self):
        return self._oversampling

    @oversampling.setter
    def oversampling(self, oversampling):
        """
        Sets the frequency of the counter clock

        @param int oversampling: desired oversampling per frequency step
        """
        # checks if scanner is still running
        if self.module_state() != 'locked' and isinstance(oversampling, (int, float)):
            self._oversampling = int(oversampling)
            self._odmr_counter.oversampling = self._oversampling
        else:
            self.log.warning('setter of oversampling failed. Logic is either locked or input value is '
                             'no integer or float.')

        update_dict = {'oversampling': self._oversampling}
        self.sigParameterUpdated.emit(update_dict)

    def set_oversampling(self, oversampling):
        self.oversampling = oversampling
        return self.oversampling

    @property
    def lock_in(self):
        return self._lock_in_active

    @lock_in.setter
    def lock_in(self, active):
        """
        Sets the frequency of the counter clock

        @param bool active: specify if signal should be detected with lock in
        """
        # checks if scanner is still running
        if self.module_state() != 'locked' and isinstance(active, bool):
            self._lock_in_active = active
            self._odmr_counter.lock_in_active = self._lock_in_active
        else:
            self.log.warning('setter of lock in failed. Logic is either locked or input value is no boolean.')

        update_dict = {'lock_in': self._lock_in_active}
        self.sigParameterUpdated.emit(update_dict)

    def set_lock_in(self, active):
        self.lock_in = active
        return self.lock_in

    def set_matrix_line_number(self, number_of_lines):
        """
        Sets the number of lines in the ODMR matrix

        @param int number_of_lines: desired number of matrix lines

        @return int: actually set number of matrix lines
        """
        if isinstance(number_of_lines, int):
            self.number_of_lines = number_of_lines
        else:
            self.log.warning('set_matrix_line_number failed. '
                             'Input parameter number_of_lines is no integer.')

        update_dict = {'number_of_lines': self.number_of_lines}
        self.sigParameterUpdated.emit(update_dict)
        return self.number_of_lines

    def set_runtime(self, runtime):
        """
        Sets the runtime for ODMR measurement

        @param float runtime: desired runtime in seconds

        @return float: actually set runtime in seconds
        """
        if isinstance(runtime, (int, float)):
            self.run_time = runtime
        else:
            self.log.warning('set_runtime failed. Input parameter runtime is no integer or float.')

        update_dict = {'run_time': self.run_time}
        self.sigParameterUpdated.emit(update_dict)
        return self.run_time

    def set_cw_parameters(self, frequency, power):
        """ Set the desired new cw mode parameters.

        @param float frequency: frequency to set in Hz
        @param float power: power to set in dBm

        @return (float, float): actually set frequency in Hz, actually set power in dBm
        """
        if self.module_state() != 'locked' and isinstance(frequency, (int, float)) and isinstance(power, (int, float)):
            constraints = self.get_hw_constraints()
            frequency_to_set = constraints.frequency_in_range(frequency)
            power_to_set = constraints.power_in_range(power)
            self.cw_mw_frequency, self.cw_mw_power, dummy = self._mw_device.set_cw(frequency_to_set,
                                                                                   power_to_set)
        else:
            self.log.warning('set_cw_frequency failed. Logic is either locked or input value is '
                             'no integer or float.')

        param_dict = {'cw_mw_frequency': self.cw_mw_frequency, 'cw_mw_power': self.cw_mw_power}
        self.sigParameterUpdated.emit(param_dict)
        return self.cw_mw_frequency, self.cw_mw_power

    def set_sweep_parameters(self, start, stop, step, power):
        """ Set the desired frequency parameters for list and sweep mode

        @param float start: start frequency to set in Hz
        @param float stop: stop frequency to set in Hz
        @param float step: step frequency to set in Hz
        @param float power: mw power to set in dBm

        @return float, float, float, float: current start_freq, current stop_freq,
                                            current freq_step, current power
        """
        limits = self.get_hw_constraints()
        if self.module_state() != 'locked':
            if isinstance(start, (int, float)):
                self.mw_start = limits.frequency_in_range(start)
            if isinstance(stop, (int, float)) and isinstance(step, (int, float)):
                if stop <= start:
                    stop = start + step
                self.mw_stop = limits.frequency_in_range(stop)
                if self.mw_scanmode == MicrowaveMode.LIST:
                    self.mw_step = limits.list_step_in_range(step)
                elif self.mw_scanmode == MicrowaveMode.SWEEP:
                    self.mw_step = limits.sweep_step_in_range(step)
            if isinstance(power, (int, float)):
                self.sweep_mw_power = limits.power_in_range(power)
        else:
            self.log.warning('set_sweep_parameters failed. Logic is locked.')

        param_dict = {'mw_start': self.mw_start, 'mw_stop': self.mw_stop, 'mw_step': self.mw_step,
                      'sweep_mw_power': self.sweep_mw_power}
        self.sigParameterUpdated.emit(param_dict)
        self._sweep_params_updated = True
        return self.mw_start, self.mw_stop, self.mw_step, self.sweep_mw_power

    def mw_cw_on(self):
        """
        Switching on the mw source in cw mode.

        @return str, bool: active mode ['cw', 'list', 'sweep'], is_running
        """
        if self.module_state() == 'locked':
            self.log.error('Can not start microwave in CW mode. ODMRLogic is already locked.')
        else:
            self.cw_mw_frequency, \
            self.cw_mw_power, \
            mode = self._mw_device.set_cw(self.cw_mw_frequency, self.cw_mw_power)
            param_dict = {'cw_mw_frequency': self.cw_mw_frequency, 'cw_mw_power': self.cw_mw_power}
            self.sigParameterUpdated.emit(param_dict)
            if mode != 'cw':
                self.log.error('Switching to CW microwave output mode failed.')
            else:
                err_code = self._mw_device.cw_on()
                if err_code < 0:
                    self.log.error('Activation of microwave output failed.')

        mode, is_running = self._mw_device.get_status()
        self.sigOutputStateUpdated.emit(mode, is_running)
        return mode, is_running

    def mw_sweep_on(self):
        """
        Switching on the mw source in list/sweep mode.

        @return str, bool: active mode ['cw', 'list', 'sweep'], is_running
        """

        limits = self.get_hw_constraints()
        param_dict = {}

        if self.mw_scanmode == MicrowaveMode.LIST:
            if np.abs(self.mw_stop - self.mw_start) / self.mw_step >= limits.list_maxentries:
                self.log.warning('Number of frequency steps too large for microwave device. '
                                 'Lowering resolution to fit the maximum length.')
                self.mw_step = np.abs(self.mw_stop - self.mw_start) / (limits.list_maxentries - 1)
                self.sigParameterUpdated.emit({'mw_step': self.mw_step})

            # adjust the end frequency in order to have an integer multiple of step size
            # The master module (i.e. GUI) will be notified about the changed end frequency
            num_steps = int(np.rint((self.mw_stop - self.mw_start) / self.mw_step))
            end_freq = self.mw_start + num_steps * self.mw_step
            freq_list = np.linspace(self.mw_start, end_freq, num_steps + 1)
            freq_list, self.sweep_mw_power, mode = self._mw_device.set_list(freq_list,
                                                                            self.sweep_mw_power)
            self.mw_start = freq_list[0]
            self.mw_stop = freq_list[-1]
            self.mw_step = (self.mw_stop - self.mw_start) / (len(freq_list) - 1)

            param_dict = {'mw_start': self.mw_start, 'mw_stop': self.mw_stop,
                          'mw_step': self.mw_step, 'sweep_mw_power': self.sweep_mw_power}

        elif self.mw_scanmode == MicrowaveMode.SWEEP:
            if np.abs(self.mw_stop - self.mw_start) / self.mw_step >= limits.sweep_maxentries:
                self.log.warning('Number of frequency steps too large for microwave device. '
                                 'Lowering resolution to fit the maximum length.')
                self.mw_step = np.abs(self.mw_stop - self.mw_start) / (limits.list_maxentries - 1)
                self.sigParameterUpdated.emit({'mw_step': self.mw_step})

            sweep_return = self._mw_device.set_sweep(
                self.mw_start, self.mw_stop, self.mw_step, self.sweep_mw_power)
            self.mw_start, self.mw_stop, self.mw_step, self.sweep_mw_power, mode = sweep_return

            param_dict = {'mw_start': self.mw_start, 'mw_stop': self.mw_stop,
                          'mw_step': self.mw_step, 'sweep_mw_power': self.sweep_mw_power}

        else:
            self.log.error('Scanmode not supported. Please select SWEEP or LIST.')

        self.sigParameterUpdated.emit(param_dict)

        if mode != 'list' and mode != 'sweep':
            self.log.error('Switching to list/sweep microwave output mode failed.')
        elif self.mw_scanmode == MicrowaveMode.SWEEP:
            err_code = self._mw_device.sweep_on()
            if err_code < 0:
                self.log.error('Activation of microwave output failed.')
        else:
            err_code = self._mw_device.list_on()
            if err_code < 0:
                self.log.error('Activation of microwave output failed.')

        mode, is_running = self._mw_device.get_status()
        self.sigOutputStateUpdated.emit(mode, is_running)
        return mode, is_running

    def reset_sweep(self):
        """
        Resets the list/sweep mode of the microwave source to the first frequency step.
        """
        if self.mw_scanmode == MicrowaveMode.SWEEP:
            self._mw_device.reset_sweeppos()
        elif self.mw_scanmode == MicrowaveMode.LIST:
            self._mw_device.reset_listpos()
        return

    def mw_off(self):
        """ Switching off the MW source.

        @return str, bool: active mode ['cw', 'list', 'sweep'], is_running
        """
        error_code = self._mw_device.off()
        if error_code < 0:
            self.log.error('Switching off microwave source failed.')

        mode, is_running = self._mw_device.get_status()
        self.sigOutputStateUpdated.emit(mode, is_running)
        return mode, is_running

    def _start_odmr_counter(self):
        """
        Starting the ODMR counter and set up the clock for it.

        @return int: error code (0:OK, -1:error)
        """

        clock_status = self._odmr_counter.set_up_odmr_clock(clock_frequency=self.clock_frequency)
        if clock_status < 0:
            return -1

        counter_status = self._odmr_counter.set_up_odmr()
        if counter_status < 0:
            self._odmr_counter.close_odmr_clock()
            return -1

        return 0

    def _stop_odmr_counter(self):
        """
        Stopping the ODMR counter.

        @return int: error code (0:OK, -1:error)
        """

        ret_val1 = self._odmr_counter.close_odmr()
        if ret_val1 != 0:
            self.log.error('ODMR counter could not be stopped!')
        ret_val2 = self._odmr_counter.close_odmr_clock()
        if ret_val2 != 0:
            self.log.error('ODMR clock could not be stopped!')

        # Check with a bitwise or:
        return ret_val1 | ret_val2

    def start_odmr_scan(self):
        """ Starting an ODMR scan.

        @return int: error code (0:OK, -1:error)
        """
        with self.threadlock:
            if self.module_state() == 'locked':
                self.log.error('Can not start ODMR scan. Logic is already locked.')
                return -1

            self.set_trigger(self.mw_trigger_pol, self.clock_frequency)

            self.module_state.lock()

            self.stopRequested = False
            self.fc.clear_result()

            # If sweep parameters have been updated since last call,
            # need to clear the data and re-initialise the buffers
            if self._sweep_params_updated:
                self._initialize_odmr_plots()
                self._sweep_params_updated = False

            self._startTime = time.time() - self.elapsed_time
            self.sigOdmrElapsedTimeUpdated.emit(self.elapsed_time, self.elapsed_sweeps)

            odmr_status = self._start_odmr_counter()
            if odmr_status < 0:
                mode, is_running = self._mw_device.get_status()
                self.sigOutputStateUpdated.emit(mode, is_running)
                self.module_state.unlock()
                return -1

            mode, is_running = self.mw_sweep_on()
            if not is_running:
                self._stop_odmr_counter()
                self.module_state.unlock()
                return -1

            self.sigNextLine.emit()
            return 0

    def stop_odmr_scan(self):
        """ Stop the ODMR scan.

        @return int: error code (0:OK, -1:error)
        """
        with self.threadlock:
            if self.module_state() == 'locked':
                self.stopRequested = True
        return 0

    def clear_odmr_data(self):
        """¨Clear ODMR data and elapsed time

        Only works when a scan is not currently running
        """
        with self.threadlock:
            if self.module_state() != 'locked':
                self._initialize_odmr_plots()
        return

    def _scan_odmr_line(self):
        """ Scans one line in ODMR

        (from mw_start to mw_stop in steps of mw_step)
        """
        with self.threadlock:
            # If the odmr measurement is not running do nothing
            if self.module_state() != 'locked':
                return

            # Stop measurement if stop has been requested
            if self.stopRequested:
                self.stopRequested = False
                self.mw_off()
                self._stop_odmr_counter()
                self.module_state.unlock()
                return

            # reset position so every line starts from the same frequency
            self.reset_sweep()

            # Acquire count data
            error, new_counts = self._odmr_counter.count_odmr(length=self.odmr_plot_x.size)

            if error:
                self.stopRequested = True
                self.sigNextLine.emit()
                return

            # Add new count data to raw_data array
            self.odmr_raw_data = np.insert(self.odmr_raw_data, 0, new_counts, 0)

            if self.lines_to_average <= 0:
                self.odmr_plot_y = np.mean(
                    self.odmr_raw_data[:max(1, self.elapsed_sweeps), :, :],
                    axis=0,
                    dtype=np.float64
                )
            else:
                self.odmr_plot_y = np.mean(
                    self.odmr_raw_data[:max(1, min(self.lines_to_average, self.elapsed_sweeps)), :, :],
                    axis=0,
                    dtype=np.float64
                )

            # Get xy plot data
            pad_amount = self.number_of_lines - self.odmr_raw_data.shape[0]
            if pad_amount > 0:
                # Pad out data if needed to fill the requested size of plot
                self.odmr_plot_xy = np.concatenate((self.odmr_raw_data, 
                                                   np.zeros((pad_amount, 
                                                   *self.odmr_raw_data.shape[1:]))))
            else:
                self.odmr_plot_xy = self.odmr_raw_data[:self.number_of_lines, :, :]

            # Update elapsed time/sweeps
            self.elapsed_sweeps += 1
            self.elapsed_time = time.time() - self._startTime
            if self.elapsed_time >= self.run_time:
                self.stopRequested = True
            # Fire update signals
            self.sigOdmrElapsedTimeUpdated.emit(self.elapsed_time, self.elapsed_sweeps)
            self.sigOdmrPlotsUpdated.emit(self.odmr_plot_x, self.odmr_plot_y, self.odmr_plot_xy)
            self.sigNextLine.emit()
            return

    def get_odmr_channels(self):
        return self._odmr_counter.get_odmr_channels()

    def get_hw_constraints(self):
        """ Return the names of all ocnfigured fit functions.
        @return object: Hardware constraints object
        """
        constraints = self._mw_device.get_limits()
        return constraints

    def get_fit_functions(self):
        """ Return the hardware constraints/limits
        @return list(str): list of fit function names
        """
        return list(self.fc.fit_list)

    def do_fit(self, fit_function=None, x_data=None, y_data=None, channel_index=0):
        """
        Execute the currently configured fit on the measurement data. Optionally on passed data
        """
        if (x_data is None) or (y_data is None):
            x_data = self.odmr_plot_x
            y_data = self.odmr_plot_y[channel_index]

        if fit_function is not None and isinstance(fit_function, str):
            if fit_function in self.get_fit_functions():
                self.fc.set_current_fit(fit_function)
            else:
                self.fc.set_current_fit('No Fit')
                if fit_function != 'No Fit':
                    self.log.warning('Fit function "{0}" not available in ODMRLogic fit container.'
                                     ''.format(fit_function))

        self.odmr_fit_x, self.odmr_fit_y, result = self.fc.do_fit(x_data, y_data)

        if result is None:
            result_str_dict = {}
        else:
            result_str_dict = result.result_str_dict
        self.sigOdmrFitUpdated.emit(
            self.odmr_fit_x, self.odmr_fit_y, result_str_dict, self.fc.current_fit)
        return

    def save_odmr_data(self, colorscale_range=None, percentile_range=None):
        """ Saves the current ODMR data to a file."""
        timestamp = datetime.datetime.now()

        for nch, channel in enumerate(self.get_odmr_channels()):
            # two paths to save the raw data and the odmr scan data.
            filepath = self._save_logic.get_path_for_module(module_name='ODMR')
            filepath2 = self._save_logic.get_path_for_module(module_name='ODMR')

            # Label file with alphanumeric characters in channel name
            filelabel = 'ODMR_{0}'.format("".join(x for x in channel if x.isalnum()))
            filelabel2 = 'ODMR_{0}_raw'.format("".join(x for x in channel if x.isalnum()))

            # prepare the data in a dict or in an OrderedDict:
            data = OrderedDict()
            data2 = OrderedDict()
            data['frequency (Hz)'] = self.odmr_plot_x
            data['count data (counts/s)'] = self.odmr_plot_y[nch]
            data2['count data (counts/s)'] = self.odmr_raw_data[:self.elapsed_sweeps, nch, :]

            parameters = OrderedDict()
            parameters['Microwave CW Power (dBm)'] = self.cw_mw_power
            parameters['Microwave Sweep Power (dBm)'] = self.sweep_mw_power
            parameters['Acquisiton Time (s)'] = self.elapsed_time
            parameters['Number of frequency sweeps (#)'] = self.elapsed_sweeps
            parameters['Start Frequency (Hz)'] = self.mw_start
            parameters['Stop Frequency (Hz)'] = self.mw_stop
            parameters['Step size (Hz)'] = self.mw_step
            parameters['Clock Frequency (Hz)'] = self.clock_frequency
            parameters['Lock-in'] = self._lock_in_active
            parameters['Oversampling'] = self._oversampling
            parameters['Channel'] = '{0}: {1}'.format(nch, channel)
            if self.fc.current_fit != 'No Fit':
                parameters['Fit function'] = self.fc.current_fit

            # add all fit parameter to the saved data:
            for name, param in self.fc.current_fit_param.items():
                parameters[name] = str(param)

            fig = self.draw_figure(
                nch,
                cbar_range=colorscale_range,
                percentile_range=percentile_range)

            self._save_logic.save_data(data,
                                       filepath=filepath,
                                       parameters=parameters,
                                       filelabel=filelabel,
                                       fmt='%.6e',
                                       delimiter='\t',
                                       timestamp=timestamp,
                                       plotfig=fig)

            self._save_logic.save_data(data2,
                                       filepath=filepath2,
                                       parameters=parameters,
                                       filelabel=filelabel2,
                                       fmt='%.6e',
                                       delimiter='\t',
                                       timestamp=timestamp)

            self.log.info('ODMR data saved to:\n{0}'.format(filepath))
        
        mode, is_running = self._mw_device.get_status()
        self.sigOutputStateUpdated.emit(mode, is_running)
        return

    def draw_figure(self, channel_number, cbar_range=None, percentile_range=None):
        """ Draw the summary figure to save with the data.

        @param: list cbar_range: (optional) [color_scale_min, color_scale_max].
                                 If not supplied then a default of data_min to data_max
                                 will be used.

        @param: list percentile_range: (optional) Percentile range of the chosen cbar_range.

        @return: fig fig: a matplotlib figure object to be saved to file.
        """
        freq_data = self.odmr_plot_x
        count_data = self.odmr_plot_y[channel_number]
        fit_freq_vals = self.odmr_fit_x
        fit_count_vals = self.odmr_fit_y
        matrix_data = self.odmr_plot_xy[:, channel_number]

        # If no colorbar range was given, take full range of data
        if cbar_range is None:
            cbar_range = np.array([np.min(matrix_data), np.max(matrix_data)])
        else:
            cbar_range = np.array(cbar_range)

        prefix = ['', 'k', 'M', 'G', 'T']
        prefix_index = 0

        # Rescale counts data with SI prefix
        while np.max(count_data) > 1000:
            count_data = count_data / 1000
            fit_count_vals = fit_count_vals / 1000
            prefix_index = prefix_index + 1

        counts_prefix = prefix[prefix_index]

        # Rescale frequency data with SI prefix
        prefix_index = 0

        while np.max(freq_data) > 1000:
            freq_data = freq_data / 1000
            fit_freq_vals = fit_freq_vals / 1000
            prefix_index = prefix_index + 1

        mw_prefix = prefix[prefix_index]

        # Rescale matrix counts data with SI prefix
        prefix_index = 0

        while np.max(matrix_data) > 1000:
            matrix_data = matrix_data / 1000
            cbar_range = cbar_range / 1000
            prefix_index = prefix_index + 1

        cbar_prefix = prefix[prefix_index]

        # Use qudi style
        plt.style.use(self._save_logic.mpl_qd_style)

        # Create figure
        fig, (ax_mean, ax_matrix) = plt.subplots(nrows=2, ncols=1)

        ax_mean.plot(freq_data, count_data, linestyle=':', linewidth=0.5)

        # Do not include fit curve if there is no fit calculated.
        if max(fit_count_vals) > 0:
            ax_mean.plot(fit_freq_vals, fit_count_vals, marker='None')

        ax_mean.set_ylabel('Fluorescence (' + counts_prefix + 'c/s)')
        ax_mean.set_xlim(np.min(freq_data), np.max(freq_data))

        matrixplot = ax_matrix.imshow(
            matrix_data,
            cmap=plt.get_cmap('inferno'),  # reference the right place in qd
            origin='lower',
            vmin=cbar_range[0],
            vmax=cbar_range[1],
            extent=[np.min(freq_data),
                np.max(freq_data),
                0,
                self.number_of_lines
                ],
            aspect='auto',
            interpolation='nearest')

        ax_matrix.set_xlabel('Frequency (' + mw_prefix + 'Hz)')
        ax_matrix.set_ylabel('Scan #')

        # Adjust subplots to make room for colorbar
        fig.subplots_adjust(right=0.8)

        # Add colorbar axis to figure
        cbar_ax = fig.add_axes([0.85, 0.15, 0.02, 0.7])

        # Draw colorbar
        cbar = fig.colorbar(matrixplot, cax=cbar_ax)
        cbar.set_label('Fluorescence (' + cbar_prefix + 'c/s)')

        # remove ticks from colorbar for cleaner image
        cbar.ax.tick_params(which=u'both', length=0)

        # If we have percentile information, draw that to the figure
        if percentile_range is not None:
            cbar.ax.annotate(str(percentile_range[0]),
                             xy=(-0.3, 0.0),
                             xycoords='axes fraction',
                             horizontalalignment='right',
                             verticalalignment='center',
                             rotation=90
                             )
            cbar.ax.annotate(str(percentile_range[1]),
                             xy=(-0.3, 1.0),
                             xycoords='axes fraction',
                             horizontalalignment='right',
                             verticalalignment='center',
                             rotation=90
                             )
            cbar.ax.annotate('(percentile)',
                             xy=(-0.3, 0.5),
                             xycoords='axes fraction',
                             horizontalalignment='right',
                             verticalalignment='center',
                             rotation=90
                             )

        return fig

    def perform_odmr_measurement(self, freq_start, freq_step, freq_stop, power, channel, runtime,
                                 fit_function='No Fit', save_after_meas=True, name_tag=''):
        """ An independant method, which can be called by a task with the proper input values
            to perform an odmr measurement.

        @return
        """
        timeout = 30
        start_time = time.time()
        while self.module_state() != 'idle':
            time.sleep(0.5)
            timeout -= (time.time() - start_time)
            if timeout <= 0:
                self.log.error('perform_odmr_measurement failed. Logic module was still locked '
                               'and 30 sec timeout has been reached.')
                return tuple()

        # set all relevant parameter:
        self.set_sweep_parameters(freq_start, freq_stop, freq_step, power)
        self.set_runtime(runtime)

        # start the scan
        self.start_odmr_scan()

        # wait until the scan has started
        while self.module_state() != 'locked':
            time.sleep(1)
        # wait until the scan has finished
        while self.module_state() == 'locked':
            time.sleep(1)

        # Perform fit if requested
        if fit_function != 'No Fit':
            self.do_fit(fit_function, channel_index=channel)
            fit_params = self.fc.current_fit_param
        else:
            fit_params = None

        # Save data if requested
        if save_after_meas:
            self.save_odmr_data(tag=name_tag)

        return self.odmr_plot_x, self.odmr_plot_y, fit_params
Example #28
0
class InjectionsLogic(GenericLogic):
    """
    Class containing the logic to configure and save an injection sequence

    Example config for copy-paste:

    injections_logic:
        module.Class: 'injections_logic.InjectionsLogic'
        probe_valve_number: 7
        number_of_valve_positions: 8
        number_of_probes: 100
    """
    probe_valve_number = ConfigOption('probe_valve_number', missing='warn')
    num_valve_positions = ConfigOption('number_of_valve_positions',
                                       missing='warn')
    num_probes = ConfigOption('number_of_probes', missing='warn')

    # signals
    sigBufferListChanged = QtCore.Signal()
    sigProbeListChanged = QtCore.Signal()
    sigHybridizationListChanged = QtCore.Signal()
    sigPhotobleachingListChanged = QtCore.Signal()
    sigIncompleteLoad = QtCore.Signal()

    # attributes
    procedures = ['Hybridization', 'Photobleaching']
    products = ['Probe']

    buffer_dict = {
    }  # key: value = valve_number: buffer_name (to make sure that each valve is only used once)
    probe_dict = {}  # key: value = position_number: probe_name (same comment)

    def __init__(self, config, **kwargs):
        super().__init__(config=config, **kwargs)

    def on_activate(self):
        """ Initialisation performed during activation of the module.
        """
        self.buffer_list_model = BufferListModel()
        self.probe_position_model = ProbePositionModel()
        self.hybridization_injection_sequence_model = InjectionSequenceModel()
        self.photobleaching_injection_sequence_model = InjectionSequenceModel()

        # add the probe entry as default into the bufferlist
        self.add_buffer('Probe', self.probe_valve_number)

    def on_deactivate(self):
        """ Perform required deactivation. """
        pass

# ----------------------------------------------------------------------------------------------------------------------
# Methods to insert / delete items into the listview models
# ----------------------------------------------------------------------------------------------------------------------

    def add_buffer(self, buffername, valve_number):
        """ This method adds an entry to the buffer dict and to the buffer list model and informs the GUI that the
        data was changed.
        :param: str buffername
        :param: int valve_number
        """
        self.buffer_dict[valve_number] = buffername
        # synchronize buffer_dict and list_model, this approach also ensures that each valve number is only set once
        self.buffer_list_model.items = [
            (self.buffer_dict[key], key)
            for key in sorted(self.buffer_dict.keys())
        ]
        self.sigBufferListChanged.emit()

    def delete_buffer(self, index):
        """ This method deletes a single entry from the buffer list model and from the buffer dict. The signal
        informs the GUI that the data was changed.
        :param: QtCore.QModelIndex index
        """
        key = self.buffer_list_model.items[index.row()][1]  # the valve number
        if key == self.probe_valve_number:
            pass
        else:
            del self.buffer_dict[key]
            del self.buffer_list_model.items[index.row()]
            self.sigBufferListChanged.emit()

    def delete_all_buffer(self):
        """ This method resets the buffer dict and deletes all items from the buffer list model. The GUI is informed
        that the data has changed.
        """
        self.buffer_dict = {}
        self.buffer_list_model.items = []
        # add the default entry Probe
        self.add_buffer('Probe', self.probe_valve_number)
        self.sigBufferListChanged.emit()

    def add_probe(self, probename, probe_position):
        """ This method adds an entry to the probe dict and to the probe position model and informs the GUI that the
        data was changed.
        :param: str probename
        :param: int probe_position
        """
        self.probe_dict[probe_position] = probename
        # synchronize buffer_dict and list_model, this approach also ensures that each valve number is only set once
        self.probe_position_model.items = [
            (self.probe_dict[key], key)
            for key in sorted(self.probe_dict.keys())
        ]
        self.sigProbeListChanged.emit()

    def delete_probe(self, index):
        """ This method deletes a single entry from the probe position model and from the probe dict. The signal
        informs the GUI that the data was changed.
        :param: QtCore.QModelIndex index
        """
        key = self.probe_position_model.items[index.row()][
            1]  # the probe position number
        # delete the entry both in the dictionary and in the list model
        del self.probe_dict[key]
        del self.probe_position_model.items[index.row()]
        self.sigProbeListChanged.emit()

    def delete_all_probes(self):
        """ This method resets the probe position dict and deletes all items from the probe position model. The GUI is
        informed that the data has changed.
        """
        self.probe_dict = {}
        self.probe_position_model.items = []
        self.sigProbeListChanged.emit()

    @QtCore.Slot(str, str, int, int)
    def add_injection_step(self, procedure, product, volume, flowrate):
        """ This method adds an entry tho the hybridization or photobleaching sequence model and informs the
        GUI that the underlying data has changed.
        :param: str procedure: identifier to select to which model the entry should be added
        :param: str product
        :param: int volume: amount of product to be injected (in ul)
        :param: int flowrate: target flowrate in ul/min
        """
        if procedure == 'Hybridization':
            self.hybridization_injection_sequence_model.items.append(
                (procedure, product, volume, flowrate, None))
            self.sigHybridizationListChanged.emit()
        elif procedure == 'Photobleaching':
            self.photobleaching_injection_sequence_model.items.append(
                (procedure, product, volume, flowrate, None))
            self.sigPhotobleachingListChanged.emit()
        else:
            pass

    @QtCore.Slot(str, int)
    def add_incubation_step(self, procedure, time):
        """ This method adds an incubation time entry tho the hybridization or photobleaching sequence model and emits
        a signal to inform the GUI that the underlying data has changed.
        :param: str procedure: identifier to select to which model the entry should be added
        :param: int time: incubation time in seconds
        """
        if procedure == 'Hybridization':
            self.hybridization_injection_sequence_model.items.append(
                (procedure, None, None, None, time))
            self.sigHybridizationListChanged.emit()
        elif procedure == 'Photobleaching':
            self.photobleaching_injection_sequence_model.items.append(
                (procedure, None, None, None, time))
            self.sigPhotobleachingListChanged.emit()
        else:
            pass

    @QtCore.Slot(QtCore.QModelIndex)
    def delete_hybr_step(self, index):
        """ This method deletes an entry from the hybridization sequence model and notifies about data modification.
        @:param: QtCore.QModelIndex index: selected entry
        """
        del self.hybridization_injection_sequence_model.items[index.row()]
        self.sigHybridizationListChanged.emit()

    @QtCore.Slot(QtCore.QModelIndex)
    def delete_photobl_step(self, index):
        """ This method deletes an entry from the photobleaching sequence model and notifies about data modification.
        @:param: QtCore.QModelIndex index: selected entry
        """
        del self.photobleaching_injection_sequence_model.items[index.row()]
        self.sigPhotobleachingListChanged.emit()

    def delete_hybr_all(self):
        """ This method deletes all entries from the hybridization sequence model and notifies about data modification.
        """
        self.hybridization_injection_sequence_model.items = []
        self.sigHybridizationListChanged.emit()

    def delete_photobl_all(self):
        """ This method deletes all entries from the photobleaching sequence model and notifies about data modification.
        """
        self.photobleaching_injection_sequence_model.items = []
        self.sigPhotobleachingListChanged.emit()

# ----------------------------------------------------------------------------------------------------------------------
# Methods to load and save files
# ----------------------------------------------------------------------------------------------------------------------

    def load_injections(self, path):
        """ This method allows to open a file and fills in data to the buffer dict, probe dict and to the models
        if the specified document contains all relevant information. The GUI is notified and updated with the new data.
        :param: str path: full path to the file
        """
        try:
            # delete the current content of models
            self.delete_all_buffer()
            self.delete_all_probes()
            self.delete_hybr_all()
            self.delete_photobl_all()

            with open(path, 'r') as stream:
                documents = yaml.safe_load(
                    stream
                )  # yaml.full_load(stream)  # use this when pyyaml updated everywhere
                self.buffer_dict = documents['buffer']
                self.probe_dict = documents['probes']
                hybridization_list = documents['hybridization list']
                photobleaching_list = documents['photobleaching list']

                # update the models based on the dictionaries / lists content
                self.buffer_list_model.items = [(self.buffer_dict[key], key)
                                                for key in self.buffer_dict]
                self.probe_position_model.items = [(self.probe_dict[key], key)
                                                   for key in self.probe_dict]

                for i in range(len(hybridization_list)):
                    entry = hybridization_list[i]  # entry is a dict
                    self.hybridization_injection_sequence_model.items.append(
                        (entry['procedure'], entry['product'], entry['volume'],
                         entry['flowrate'], entry['time']))

                for i in range(len(photobleaching_list)):
                    entry = photobleaching_list[i]  # entry is a dict
                    self.photobleaching_injection_sequence_model.items.append(
                        (entry['procedure'], entry['product'], entry['volume'],
                         entry['flowrate'], entry['time']))

                self.sigBufferListChanged.emit()
                self.sigProbeListChanged.emit()
                self.sigHybridizationListChanged.emit()
                self.sigPhotobleachingListChanged.emit()

        except KeyError:
            self.log.warning('Injections not loaded. Document is incomplete.')
            self.sigIncompleteLoad.emit()

    def save_injections(self, path):
        """ This method allows to write the data from the models and from the buffer dict and probe position dict
        to a file. For readability, hybridization and photobleaching sequence model entries are converted into a
        dictionary format.
        :param: str path: full path
        """
        # prepare the list entries in dictionary format for good readability in the file
        hybridization_list = []
        photobleaching_list = []

        for entry_num, item in enumerate(
                self.hybridization_injection_sequence_model.items, 1):
            if item[1] is not None:
                product = str.split(
                    item[1], ':'
                )[0]  # write just the product name, not the corresponding valve pos
            else:
                product = None
            entry = self.make_dict_entry(entry_num, item[0], product, item[2],
                                         item[3], item[4])
            hybridization_list.append(entry)

        for entry_num, item in enumerate(
                self.photobleaching_injection_sequence_model.items, 1):
            if item[1] is not None:
                product = str.split(item[1], ':')[0]
            else:
                product = None
            entry = self.make_dict_entry(entry_num, item[0], product, item[2],
                                         item[3], item[4])
            photobleaching_list.append(entry)

        # write a complete file containing buffer_dict, probe_dict, hybridization_list and photobleaching_list
        with open(path, 'w') as file:
            dict_file = {
                'buffer': self.buffer_dict,
                'probes': self.probe_dict,
                'hybridization list': hybridization_list,
                'photobleaching list': photobleaching_list
            }
            yaml.safe_dump(dict_file, file,
                           default_flow_style=False)  # , sort_keys=False
            self.log.info('Injections saved to {}'.format(path))

    @staticmethod
    def make_dict_entry(step_num,
                        procedure,
                        product,
                        volume,
                        flowrate,
                        time=None):
        """ Helper function.
        :param int step_num: number of the current step in an injection list
        :param str procedure: name of the procedure: Hybridization or Photobleaching
        :param str product: name of the product
        :param int volume: quantity of product in ul to be injected
        :param int flowrate: target flowrate in ul/min
        :param int time: time in seconds for incubation steps
        """
        inj_step_dict = {
            'step_number': step_num,
            'procedure': procedure,
            'product': product,
            'volume': volume,
            'flowrate': flowrate,
            'time': time
        }
        return inj_step_dict
Example #29
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

    """

    _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 = {'label': self._first_axis_label,
                 'ID': self._first_axis_ID,
                 'unit': 'm',
                 'ramp': None,
                 'pos_min': self._min_first,
                 'pos_max': self._max_first,
                 'pos_step': self.step_first_axis,
                 'vel_min': self._vel_min_first,
                 'vel_max': self._vel_max_first,
                 'vel_step': self._vel_step_first,
                 'acc_min': None,
                 'acc_max': None,
                 'acc_step': None}

        axis1 = {'label': self._second_axis_label,
                 'ID': self._second_axis_ID,
                 'unit': 'm',
                 'ramp': None,
                 'pos_min': self._min_second,
                 'pos_max': self._max_second,
                 'pos_step': self.step_second_axis,
                 'vel_min': self._vel_min_second,
                 'vel_max': self._vel_max_second,
                 'vel_step': self._vel_step_second,
                 'acc_min': None,
                 'acc_max': None,
                 'acc_step': None}

        axis2 = {'label': self._third_axis_label,
                 'ID': self._third_axis_ID,
                 'unit': 'm',
                 'ramp': None,
                 'pos_min': self._min_third,
                 'pos_max': self._max_third,
                 'pos_step': self.step_third_axis,
                 'vel_min': self._vel_min_third,
                 'vel_max': self._vel_max_third,
                 'vel_step': self._vel_step_third,
                 'acc_min': None,
                 'acc_max': None,
                 '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:
            try:
                answer = answer + self._serial_connection_xyz.read()[:-1]
            except:
                still_reading = False
        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
Example #30
0
class PulserDummy(Base, PulserInterface):
    """ Dummy class for  PulseInterface

    Be careful in adjusting the method names in that class, since some of them
    are also connected to the mwsourceinterface (to give the AWG the possibility
    to act like a microwave source).

    Example config for copy-paste:

    pulser_dummy:
        module.Class: 'pulser_dummy.PulserDummy'

    """

    activation_config = StatusVar(default=None)
    force_sequence_option = ConfigOption('force_sequence_option', default=False)

    def __init__(self, config, **kwargs):
        super().__init__(config=config, **kwargs)

        self.log.info('Dummy Pulser: I will simulate an AWG :) !')

        self.connected = False
        self.sample_rate = 25e9

        # Deactivate all channels at first:
        self.channel_states = {'a_ch1': False, 'a_ch2': False, 'a_ch3': False,
                               'd_ch1': False, 'd_ch2': False, 'd_ch3': False, 'd_ch4': False,
                               'd_ch5': False, 'd_ch6': False, 'd_ch7': False, 'd_ch8': False}

        # for each analog channel one value
        self.amplitude_dict = {'a_ch1': 1.0, 'a_ch2': 1.0, 'a_ch3': 1.0}
        self.offset_dict = {'a_ch1': 0.0, 'a_ch2': 0.0, 'a_ch3': 0.0}

        # for each digital channel one value
        self.digital_high_dict = {'d_ch1': 5.0, 'd_ch2': 5.0, 'd_ch3': 5.0, 'd_ch4': 5.0,
                                  'd_ch5': 5.0, 'd_ch6': 5.0, 'd_ch7': 5.0, 'd_ch8': 5.0}
        self.digital_low_dict = {'d_ch1': 0.0, 'd_ch2': 0.0, 'd_ch3': 0.0, 'd_ch4': 0.0,
                                 'd_ch5': 0.0, 'd_ch6': 0.0, 'd_ch7': 0.0, 'd_ch8': 0.0}

        self.waveform_set = set()
        self.sequence_dict = dict()

        self.current_loaded_assets = dict()

        self.use_sequencer = True
        self.interleave = False

        self.current_status = 0    # that means off, not running.

    def on_activate(self):
        """ Initialisation performed during activation of the module.
        """
        self.connected = True

        self.channel_states = {'a_ch1': False, 'a_ch2': False, 'a_ch3': False,
                               'd_ch1': False, 'd_ch2': False, 'd_ch3': False, 'd_ch4': False,
                               'd_ch5': False, 'd_ch6': False, 'd_ch7': False, 'd_ch8': False}

        if self.activation_config is None:
            self.activation_config = self.get_constraints().activation_config['config5']
        elif self.activation_config not in self.get_constraints().activation_config.values():
            self.activation_config = self.get_constraints().activation_config['config5']

        for chnl in self.activation_config:
            self.channel_states[chnl] = True

    def on_deactivate(self):
        """ Deinitialisation performed during deactivation of the module.
        """
        self.connected = False

    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()

        if self.interleave:
            constraints.sample_rate.min = 12.0e9
            constraints.sample_rate.max = 24.0e9
            constraints.sample_rate.step = 4.0e8
            constraints.sample_rate.default = 24.0e9
        else:
            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_config = OrderedDict()
        activation_config['config0'] = frozenset(
            {'a_ch1', 'd_ch1', 'd_ch2', 'a_ch2', 'd_ch3', 'd_ch4'})
        activation_config['config1'] = frozenset(
            {'a_ch2', 'd_ch1', 'd_ch2', 'a_ch3', 'd_ch3', 'd_ch4'})
        # Usage of channel 1 only:
        activation_config['config2'] = frozenset({'a_ch2', 'd_ch1', 'd_ch2'})
        # Usage of channel 2 only:
        activation_config['config3'] = frozenset({'a_ch3', 'd_ch3', 'd_ch4'})
        # Usage of Interleave mode:
        activation_config['config4'] = frozenset({'a_ch1', 'd_ch1', 'd_ch2'})
        # Usage of only digital channels:
        activation_config['config5'] = frozenset(
            {'d_ch1', 'd_ch2', 'd_ch3', 'd_ch4', 'd_ch5', 'd_ch6', 'd_ch7', 'd_ch8'})
        # Usage of only one analog channel:
        activation_config['config6'] = frozenset({'a_ch1'})
        activation_config['config7'] = frozenset({'a_ch2'})
        activation_config['config8'] = frozenset({'a_ch3'})
        # Usage of only the analog channels:
        activation_config['config9'] = frozenset({'a_ch2', 'a_ch3'})
        constraints.activation_config = activation_config

        constraints.sequence_option = SequenceOption.FORCED if self.force_sequence_option else SequenceOption.OPTIONAL

        return constraints

    def pulser_on(self):
        """ Switches the pulsing device on.

        @return int: error code (0:stopped, -1:error, 1:running)
        """
        if self.current_status == 0:
            self.current_status = 1
            self.log.info('PulserDummy: Switch on the Output.')
            time.sleep(1)
            return 0
        else:
            return -1

    def pulser_off(self):
        """ Switches the pulsing device off.

        @return int: error code (0:stopped, -1:error, 1:running)
        """
        if self.current_status == 1:
            self.current_status = 0
            self.log.info('PulserDummy: Switch off the Output.')
        return 0

    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
        """
        waveforms = list()

        # Sanity checks
        if len(analog_samples) > 0:
            number_of_samples = len(analog_samples[list(analog_samples)[0]])
        elif len(digital_samples) > 0:
            number_of_samples = len(digital_samples[list(digital_samples)[0]])
        else:
            self.log.error('No analog or digital samples passed to write_waveform method in dummy '
                           'pulser.')
            return -1, waveforms

        for chnl, samples in analog_samples.items():
            if len(samples) != number_of_samples:
                self.log.error('Unequal length of sample arrays for different channels in dummy '
                               'pulser.')
                return -1, waveforms
        for chnl, samples in digital_samples.items():
            if len(samples) != number_of_samples:
                self.log.error('Unequal length of sample arrays for different channels in dummy '
                               'pulser.')
                return -1, waveforms

        # Determine if only digital samples are active. In that case each channel will get a
        # waveform. Otherwise only the analog channels will have a waveform with digital channel
        # samples included (as it is the case in Tektronix and Keysight AWGs).
        # Simulate a 1Gbit/s transfer speed. Assume each analog waveform sample is 5 bytes large
        # (4 byte float and 1 byte marker bitmask). Assume each digital waveform sample is 1 byte.
        if len(analog_samples) > 0:
            for chnl in analog_samples:
                waveforms.append(name + chnl[1:])
                time.sleep(number_of_samples * 5 * 8 / 1024 ** 3)
        else:
            for chnl in digital_samples:
                waveforms.append(name + chnl[1:])
                time.sleep(number_of_samples * 8 / 1024 ** 3)

        self.waveform_set.update(waveforms)

        self.log.info('Waveforms with nametag "{0}" directly written on dummy pulser.'.format(name))
        # print(number_of_samples, waveforms)
        return number_of_samples, waveforms

    def write_sequence(self, name, sequence_parameter_list):
        print(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)
        """
        # Check if all waveforms are present on virtual device memory
        for waveform_tuple, param_dict in sequence_parameter_list:
            for waveform in waveform_tuple:
                if waveform not in self.waveform_set:
                    self.log.error('Failed to create sequence "{0}" due to waveform "{1}" not '
                                   'present in device memory.'.format(name, waveform))
                    return -1

        if name in self.sequence_dict:
            del self.sequence_dict[name]

        self.sequence_dict[name] = len(sequence_parameter_list[0][0])
        time.sleep(1)

        self.log.info('Sequence with name "{0}" directly written on dummy pulser.'.format(name))
        return len(sequence_parameter_list)

    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.waveform_set)

    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(self.sequence_dict)

    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.
        """
        if isinstance(waveform_name, str):
            waveform_name = [waveform_name]

        deleted_waveforms = list()
        for waveform in waveform_name:
            if waveform in self.waveform_set:
                self.waveform_set.remove(waveform)
                deleted_waveforms.append(waveform)

        return deleted_waveforms

    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.
        """
        if isinstance(sequence_name, str):
            sequence_name = [sequence_name]

        deleted_sequences = list()
        for sequence in sequence_name:
            if sequence in self.sequence_dict:
                del self.sequence_dict[sequence]
                deleted_sequences.append(sequence)

        return deleted_sequences

    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 load_dict:  dict|list, 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, 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')
        """

        print('load_dict:')
        print(load_dict)

        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

        # Determine if the device is purely digital and get all active channels
        analog_channels = [chnl for chnl in self.activation_config if chnl.startswith('a')]
        digital_channels = [chnl for chnl in self.activation_config if chnl.startswith('d')]
        pure_digital = len(analog_channels) == 0

        # Check if waveforms are present in virtual dummy device memory and specified channels are
        # active. Create new load dict.
        new_loaded_assets = dict()
        for channel, waveform in load_dict.items():
            if waveform not in self.waveform_set:
                self.log.error('Loading failed. Waveform "{0}" not found on device memory.'
                               ''.format(waveform))
                return self.current_loaded_assets
            if pure_digital:
                if 'd_ch{0:d}'.format(channel) not in digital_channels:
                    self.log.error('Loading failed. Digital channel {0:d} not active.'
                                   ''.format(channel))
                    return self.current_loaded_assets
            else:
                if 'a_ch{0:d}'.format(channel) not in analog_channels:
                    self.log.error('Loading failed. Analog channel {0:d} not active.'
                                   ''.format(channel))
                    return self.current_loaded_assets
            new_loaded_assets[channel] = waveform
        self.current_loaded_assets = new_loaded_assets
        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.
        For devices that have a workspace (i.e. AWG) this will load the sequence from the device
        workspace into the channels.

        @param sequence_name:  str, name of the sequence to load

        @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 sequence_name not in self.sequence_dict:
            self.log.error('Sequence loading failed. No sequence with name "{0}" found on device '
                           'memory.'.format(sequence_name))
            return self.get_loaded_assets()

        # Determine if the device is purely digital and get all active channels
        analog_channels = natural_sort(chnl for chnl in self.activation_config if chnl.startswith('a'))
        digital_channels = natural_sort(chnl for chnl in self.activation_config if chnl.startswith('d'))
        pure_digital = len(analog_channels) == 0

        if pure_digital and len(digital_channels) != self.sequence_dict[sequence_name]:
            self.log.error('Sequence loading failed. Number of active digital channels ({0:d}) does'
                           ' not match the number of tracks in the sequence ({1:d}).'
                           ''.format(len(digital_channels), self.sequence_dict[sequence_name]))
            return self.get_loaded_assets()
        if not pure_digital and len(analog_channels) != self.sequence_dict[sequence_name]:
            self.log.error('Sequence loading failed. Number of active analog channels ({0:d}) does'
                           ' not match the number of tracks in the sequence ({1:d}).'
                           ''.format(len(analog_channels), self.sequence_dict[sequence_name]))
            return self.get_loaded_assets()

        new_loaded_assets = dict()
        if pure_digital:
            for track_index, chnl in enumerate(digital_channels):
                chnl_num = int(chnl.split('ch')[1])
                new_loaded_assets[chnl_num] = '{0}_{1:d}'.format(sequence_name, track_index)
        else:
            for track_index, chnl in enumerate(analog_channels):
                chnl_num = int(chnl.split('ch')[1])
                new_loaded_assets[chnl_num] = '{0}_{1:d}'.format(sequence_name, track_index)

        self.current_loaded_assets = new_loaded_assets
        return self.get_loaded_assets()

    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')
        """
        # Determine if it's a waveform or a sequence
        asset_type = None
        for asset_name in self.current_loaded_assets.values():
            if 'ch' in asset_name.rsplit('_', 1)[1]:
                current_type = 'waveform'
            else:
                current_type = 'sequence'

            if asset_type is None or asset_type == current_type:
                asset_type = current_type
            else:
                self.log.error('Unable to determine loaded asset type. Mixed naming convention '
                               'assets loaded (waveform and sequence tracks).')
                return dict(), ''

        return self.current_loaded_assets, asset_type

    def clear_all(self):
        """ Clears all loaded waveform from the pulse generators RAM.

        @return int: error code (0:OK, -1:error)

        Unused for digital pulse generators without storage capability
        (PulseBlaster, FPGA).
        """
        self.current_loaded_assets = dict()
        self.waveform_set = set()
        self.sequence_dict = dict()
        return 0

    def get_status(self):
        """ Retrieves the status of the pulsing hardware

        @return (int, dict): inter value of the current status with the
                             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: 'Device is active and running.'}
        # All the other status messages should have higher integer values
        # then 1.
        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.
        """
        constraint = self.get_constraints().sample_rate
        if sample_rate > constraint.max:
            self.sample_rate = constraint.max
        elif sample_rate < constraint.min:
            self.sample_rate = constraint.min
        else:
            self.sample_rate = 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 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: with keys being the generic string channel names and items
                      being the values for those channels. Amplitude is always
                      denoted in Volt-peak-to-peak and Offset in (absolute)
                      Voltage.

        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 no entries provided then the levels of all channels where simply
        returned. If no analog channels provided, return just an empty dict.
        Example of a possible input:
            amplitude = ['a_ch1','a_ch4'], offset =[1,3]
        to obtain the amplitude of channel 1 and 4 and the offset
            {'a_ch1': -0.5, 'a_ch4': 2.0} {'a_ch1': 0.0, 'a_ch3':-0.75}
        since no high request was performed.

        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)!
        """
        if amplitude is None:
            amplitude = []
        if offset is None:
            offset = []

        ampl = dict()
        off = dict()

        if not amplitude and not offset:

            for a_ch, pp_amp in self.amplitude_dict.items():
                ampl[a_ch] = pp_amp

            for a_ch, offset in self.offset_dict.items():
                off[a_ch] = offset

        else:
            for a_ch in amplitude:
                ampl[a_ch] = self.amplitude_dict[a_ch]

            for a_ch in offset:
                off[a_ch] = self.offset_dict[a_ch]

        return ampl, off

    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.

        Note: After setting the analog and/or offset of the device, retrieve
              them again for obtaining the actual set value(s) and use that
              information 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)!
        """
        if amplitude is None:
            amplitude = dict()
        if offset is None:
            offset = dict()

        for a_ch, amp in amplitude.items():
            self.amplitude_dict[a_ch] = amp

        for a_ch, off in offset.items():
            self.offset_dict[a_ch] = off

        return self.get_analog_level(amplitude=list(amplitude), offset=list(offset))

    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 = ['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} {}
        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 = []

        if not low and not high:
            low_val = self.digital_low_dict
            high_val = self.digital_high_dict
        else:
            low_val = dict()
            high_val = dict()
            for d_ch in low:
                low_val[d_ch] = self.digital_low_dict[d_ch]
            for d_ch in high:
                high_val[d_ch] = self.digital_high_dict[d_ch]

        return low_val, high_val

    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 = dict()
        if high is None:
            high = dict()

        for d_ch, low_voltage in low.items():
            self.digital_low_dict[d_ch] = low_voltage

        for d_ch, high_voltage in high.items():
            self.digital_high_dict[d_ch] = high_voltage

        return self.get_digital_level(low=list(low), high=list(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 number 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 parameters are passed to this method all channels will be asked
        for their setting.
        """
        if ch is None:
            ch = []

        active_ch = {}

        if not ch:
            active_ch = self.channel_states

        else:
            for channel in ch:
                active_ch[channel] = self.channel_states[channel]

        return active_ch

    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 = {}
        old_activation = self.channel_states.copy()
        for channel in ch:
            self.channel_states[channel] = ch[channel]

        active_channel_set = {chnl for chnl, is_active in self.channel_states.items() if is_active}
        if active_channel_set not in self.get_constraints().activation_config.values():
            self.log.error('Channel activation to be set not found in constraints.\n'
                           'Channel activation unchanged.')
            self.channel_states = old_activation
        else:
            self.activation_config = active_channel_set

        return self.get_active_channels(ch=list(ch))

    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 self.interleave

    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.
        """

        self.interleave = state
        return self.get_interleave()

    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)
        """

        self.log.info('It is so nice that you talk to me and told me "{0}"; '
                      'as a dummy it is very dull out here! :) '.format(command))
        return 0

    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
        """

        self.log.info('Dude, I\'m a dummy! Your question \'{0}\' is way too '
                      'complicated for me :D !'.format(question))
        return 'I am a dummy!'

    def reset(self):
        """ Reset the device.

        @return int: error code (0:OK, -1:error)
        """
        self.__init__()
        self.connected = True
        self.log.info('Dummy reset!')
        return 0