diff --git a/README.md b/README.md
index d1415125b1..560399ad5d 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# qudi
Qudi is a suite of tools for operating multi-instrument and multi-computer laboratory experiments.
-Originally built around a confocal fluorescence microscope experiments, it has grown to be a generally applicaple framework for controlling experiments.
+Originally built around a confocal fluorescence microscope experiments, it has grown to be a generally applicable framework for controlling experiments.
## Features
* A modular and extendable architecture
@@ -16,7 +16,7 @@ Originally built around a confocal fluorescence microscope experiments, it has g
* etc.
## Citation
-If you are publishing scientific results, mentioning Qudi in your methods decscription is the least you can do as good scientific practice.
+If you are publishing scientific results, mentioning Qudi in your methods description is the least you can do as good scientific practice.
You should cite our paper [Qudi: A modular python suite for experiment control and data processing](http://doi.org/10.1016/j.softx.2017.02.001) for this purpose.
## Documentation
diff --git a/config/example/default.cfg b/config/example/default.cfg
index 6d0493215d..03df7e4b6a 100644
--- a/config/example/default.cfg
+++ b/config/example/default.cfg
@@ -77,9 +77,21 @@ hardware:
mydummyswitch1:
module.Class: 'switches.switch_dummy.SwitchDummy'
+ name: 'First' # optional
+ remember_states: True # optional
+ switches:
+ one: ['down', 'up']
+ two: ['down', 'up']
+ three: ['low', 'middle', 'high']
mydummyswitch2:
module.Class: 'switches.switch_dummy.SwitchDummy'
+ name: 'Second' # optional
+ remember_states: True # optional
+ switches:
+ 'An even longer name of the switch itself':
+ - 'Very long name of a random state'
+ - 'Another very long name of a random state'
myspectrometer:
module.Class: 'spectrometer.spectrometer_dummy.SpectrometerInterfaceDummy'
@@ -121,6 +133,7 @@ logic:
pidlogic:
module.Class: 'pid_logic.PIDLogic'
+ timestep: 0.1
connect:
controller: 'softpid'
savelogic: 'savelogic'
@@ -180,9 +193,17 @@ logic:
switchlogic:
module.Class: 'switch_logic.SwitchLogic'
+ watchdog_interval: 1
+ autostart_watchdog: True
+ connect:
+ switch: 'switchinterfuse'
+
+ switchinterfuse:
+ module.Class: 'interfuse.switch_combiner_interfuse.SwitchCombinerInterfuse'
connect:
switch1: 'mydummyswitch1'
switch2: 'mydummyswitch2'
+ extend_hardware_name: True
scannerlogic:
module.Class: 'confocal_logic.ConfocalLogic'
@@ -402,7 +423,7 @@ gui:
savelogic: 'savelogic'
switches:
- module.Class: 'switcher.switchgui.SwitchGui'
+ module.Class: 'switch.switch_gui.SwitchGui'
connect:
switchlogic: 'switchlogic'
diff --git a/documentation/changelog.md b/documentation/changelog.md
index 181e9c8737..88f6d9f0b0 100644
--- a/documentation/changelog.md
+++ b/documentation/changelog.md
@@ -4,7 +4,8 @@
Changes/New features:
-* Added functionality to simultaneously record multiple frequency ranges in the ODMR toolchain
+* Added support for Keysight M8195A and M8190A AWGs.
+* Added functionality to simultaneously record multiple frequency ranges in the ODMR toolchain
in case the hardware supports it.
* Cleanup/Improvement/Debug of POI manager (logic and GUI)
* New POI manager tool _POI selector_ which allows adding of new POIs by clicking inside the scan
@@ -57,11 +58,11 @@ The potential sequence_options are:
* `FORCED` (only output as sequence possible)
* Added interfuse to correct geometrical aberration on scanner via polynomial transformations
* added the option to do a purely analog ODMR scan.
-* Added new GUI, logic, interface and hardware modules to replace the "slow counter" tool in the
-future. The new tools are designed to be able to stream any kind of time series data efficiently
-for multiple analog and digital channels. See example config on how to set up the
-time series/streaming modules (_time_series_gui.py_, _time_series_reader_logic.py_).
-For a drop-in replacement of the obsolete slow counter together with a NI x-series card,
+* Added new GUI, logic, interface and hardware modules to replace the "slow counter" tool in the
+future. The new tools are designed to be able to stream any kind of time series data efficiently
+for multiple analog and digital channels. See example config on how to set up the
+time series/streaming modules (_time_series_gui.py_, _time_series_reader_logic.py_).
+For a drop-in replacement of the obsolete slow counter together with a NI x-series card,
please use _ni_x_series_in_streamer.py_ as hardware module.
* added multi channel option to process_interface and process_control_interface
* added the option of an additional path for fit methods
@@ -75,7 +76,7 @@ please use _ni_x_series_in_streamer.py_ as hardware module.
* Added a config option to fastcomtec7887 module to support 7889 model
* Added fastcomec 7887/9 support of dma mode through config option
* Fixed bug in spincore pulseblaster hardware that affected only old models
-* Added a netobtain in spincore pulseblaster hardware to speedup remote loading
+* Added a netobtain in spincore pulseblaster hardware to speedup remote loading
* Adding hardware file of HydraHarp 400 from Pico Quant, basing on the 3.0.0.2 version of function library and user manual.
* reworked the QDPlotter to now contain fits and a scalable number of plots. Attention: custom notebooks might break by this change.
* Set proper minimum wavelength value in constraints of Tektronix AWG7k series HW module
@@ -86,6 +87,12 @@ please use _ni_x_series_in_streamer.py_ as hardware module.
* Update hardware module controlling the cryocon temperature regulator
* Added a hardware file to interface Thorlabs filter wheels via scripts
* Bug fixes to core: made error messages sticky, respecting dependencies when restarting.
+* Added a config option to regulate pid logic timestep length
+* New SwitchInterface and updated logic plus GUI
+* Added biexponential fit function, model and estimator
+* Added custom circular loading indicator widget `qtwidgets.loading_indicator.CircleLoadingIndicator`
+* added property disable_wheel to custom ScienSponBox and ScienDSpinBox to deactivate wheel scrolling if required
+* Added possibility to fit data of all ranges in ODMR module when Fit range is -1
*
* Added basic field calculation tool with NV center.
@@ -96,10 +103,11 @@ Config changes:
of the `SequenceGeneratorLogic` can now either be a string for a single path
or a list of strings for multiple paths.
* There is an option for the fit logic, to give an additional path: `additional_fit_methods_path`
-* Added NVcalculator_logic and NVcalculator_gui.
-* There is an option for the fit logic, to give an additional path: `additional_fit_methods_path`
* The connectors and file names of the GUI and logic modules of the QDPlotter have been changed.
-* QDPlotter now needs a new connection to the fit logic.
+* QDPlotter now needs a new connection to the fit logic.
+* The tool chain for the switch logic has changed.
+To combine multiple switches one needs to use the `switch_combiner_interfuse`
+instead of multiple connectors in the logic.
## Release 0.10
Released on 14 Mar 2019
@@ -235,6 +243,7 @@ This can be used to specify the axis labels for the measurement (excluding units
* Introduced separate fit tools for each of the two plots in the pulsed analysis tab
* Automatically clears fit data when changing the alternative plot type or starting a new
measurement.
+ * Adding in NI switches the possibility to invert the output and to use PFI channels.
Config changes:
* **All** pulsed related logic module paths need to be changed because they have been moved in the logic
diff --git a/gui/odmr/odmrgui.py b/gui/odmr/odmrgui.py
index b36f6c6f48..1719933ad2 100644
--- a/gui/odmr/odmrgui.py
+++ b/gui/odmr/odmrgui.py
@@ -119,6 +119,8 @@ def on_activate(self):
constraints = self._odmr_logic.get_hw_constraints()
# Adjust range of scientific spinboxes above what is possible in Qt Designer
+ self._mw.cw_frequency_DoubleSpinBox.setMaximum(constraints.max_frequency)
+ self._mw.cw_frequency_DoubleSpinBox.setMinimum(constraints.min_frequency)
self._mw.cw_power_DoubleSpinBox.setMaximum(constraints.max_power)
self._mw.cw_power_DoubleSpinBox.setMinimum(constraints.min_power)
self._mw.sweep_power_DoubleSpinBox.setMaximum(constraints.max_power)
diff --git a/gui/odmr/ui_odmrgui.ui b/gui/odmr/ui_odmrgui.ui
index 5d3b54d854..c8e701c83f 100644
--- a/gui/odmr/ui_odmrgui.ui
+++ b/gui/odmr/ui_odmrgui.ui
@@ -626,14 +626,14 @@
-
- 0
+ -1
-
- Range
+ Fit range
diff --git a/gui/switch/switch_gui.py b/gui/switch/switch_gui.py
new file mode 100644
index 0000000000..4658069a20
--- /dev/null
+++ b/gui/switch/switch_gui.py
@@ -0,0 +1,292 @@
+# -*- coding: utf-8 -*-
+"""
+This file contains the qudi switch GUI module.
+
+Qudi is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Qudi is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Qudi. If not, see .
+
+Copyright (c) the Qudi Developers. See the COPYRIGHT.txt file at the
+top-level directory of this distribution and at
+"""
+
+from enum import IntEnum
+from core.connector import Connector
+from core.statusvariable import StatusVar
+from gui.guibase import GUIBase
+from qtpy import QtWidgets, QtCore, QtGui
+from .switch_state_widgets import SwitchRadioButtonWidget, ToggleSwitchWidget
+
+
+class SwitchStyle(IntEnum):
+ TOGGLE_SWITCH = 0
+ RADIO_BUTTON = 1
+
+
+class StateColorScheme(IntEnum):
+ DEFAULT = 0
+ HIGHLIGHT = 1
+
+
+class SwitchMainWindow(QtWidgets.QMainWindow):
+ """ Main Window for the SwitchGui module """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.setWindowTitle('qudi: ')
+ # Create main layout and central widget
+ self.main_layout = QtWidgets.QGridLayout()
+ self.main_layout.setColumnStretch(1, 1)
+ self.main_layout.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+ self.main_layout.setSizeConstraint(QtWidgets.QLayout.SetFixedSize)
+ widget = QtWidgets.QWidget()
+ widget.setLayout(self.main_layout)
+ widget.setFixedSize(1, 1)
+ self.setCentralWidget(widget)
+
+ # Create QActions and menu bar
+ menu_bar = QtWidgets.QMenuBar()
+ self.setMenuBar(menu_bar)
+
+ menu = menu_bar.addMenu('Menu')
+ self.action_close = QtWidgets.QAction('Close Window')
+ self.action_close.setCheckable(False)
+ self.action_close.setIcon(QtGui.QIcon('artwork/icons/oxygen/22x22/application-exit.png'))
+ self.addAction(self.action_close)
+ menu.addAction(self.action_close)
+
+ menu = menu_bar.addMenu('View')
+ self.action_periodic_state_check = QtWidgets.QAction('Periodic State Checking')
+ self.action_periodic_state_check.setCheckable(True)
+ menu.addAction(self.action_periodic_state_check)
+ separator = menu.addSeparator()
+ separator.setText('Switch Appearance')
+ self.switch_view_actions = [QtWidgets.QAction('use toggle switches'),
+ QtWidgets.QAction('use radio buttons')]
+ self.switch_view_action_group = QtWidgets.QActionGroup(self)
+ for action in self.switch_view_actions:
+ action.setCheckable(True)
+ self.switch_view_action_group.addAction(action)
+ menu.addAction(action)
+ self.action_view_highlight_state = QtWidgets.QAction('highlight state labels')
+ self.action_view_highlight_state.setCheckable(True)
+ menu.addAction(self.action_view_highlight_state)
+ self.action_view_alt_toggle_style = QtWidgets.QAction('alternative toggle switch')
+ self.action_view_alt_toggle_style.setCheckable(True)
+ menu.addAction(self.action_view_alt_toggle_style)
+
+ # close window upon triggering close action
+ self.action_close.triggered.connect(self.close)
+ return
+
+
+class SwitchGui(GUIBase):
+ """ A graphical interface to switch a hardware by hand.
+ """
+
+ # declare connectors
+ switchlogic = Connector(interface='SwitchLogic')
+
+ # declare status variables
+ _switch_style = StatusVar(name='switch_style',
+ default=SwitchStyle.TOGGLE_SWITCH,
+ representer=lambda _, x: int(x),
+ constructor=lambda _, x: SwitchStyle(x))
+ _state_colorscheme = StatusVar(name='state_colorscheme',
+ default=StateColorScheme.DEFAULT,
+ representer=lambda _, x: int(x),
+ constructor=lambda _, x: StateColorScheme(x))
+ _alt_toggle_switch_style = StatusVar(name='alt_toggle_switch_style', default=False)
+
+ # declare signals
+ sigSwitchChanged = QtCore.Signal(str, str)
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._mw = None
+ self._widgets = dict()
+
+ def on_activate(self):
+ """ Create all UI objects and show the window.
+ """
+ self._mw = SwitchMainWindow()
+ self.restoreWindowPos(self._mw)
+ try:
+ self._mw.switch_view_actions[self._switch_style].setChecked(True)
+ except IndexError:
+ self._mw.switch_view_actions[0].setChecked(True)
+ self._switch_style = SwitchStyle(0)
+ self._mw.action_view_highlight_state.setChecked(
+ self._state_colorscheme == StateColorScheme.HIGHLIGHT
+ )
+ self._mw.action_view_alt_toggle_style.setChecked(self._alt_toggle_switch_style)
+ self._mw.setWindowTitle(f'qudi: {self.switchlogic().device_name.title()}')
+
+ self._populate_switches()
+
+ self.sigSwitchChanged.connect(self.switchlogic().set_state, QtCore.Qt.QueuedConnection)
+ self._mw.action_periodic_state_check.toggled.connect(
+ self.switchlogic().toggle_watchdog, QtCore.Qt.QueuedConnection
+ )
+ self._mw.switch_view_action_group.triggered.connect(self._update_switch_appearance)
+ self._mw.action_view_highlight_state.triggered.connect(self._update_state_colorscheme)
+ self._mw.action_view_alt_toggle_style.triggered.connect(self._update_toggle_switch_style)
+ self.switchlogic().sigWatchdogToggled.connect(
+ self._watchdog_updated, QtCore.Qt.QueuedConnection
+ )
+ self.switchlogic().sigSwitchesChanged.connect(
+ self._switches_updated, QtCore.Qt.QueuedConnection
+ )
+
+ self._watchdog_updated(self.switchlogic().watchdog_active)
+ self._switches_updated(self.switchlogic().states)
+ self._update_state_colorscheme()
+ self.show()
+
+ def on_deactivate(self):
+ """ Hide window empty the GUI and disconnect signals
+ """
+ self.switchlogic().sigSwitchesChanged.disconnect(self._switches_updated)
+ self.switchlogic().sigWatchdogToggled.disconnect(self._watchdog_updated)
+ self._mw.action_view_highlight_state.triggered.disconnect()
+ self._mw.action_view_alt_toggle_style.triggered.disconnect()
+ self._mw.switch_view_action_group.triggered.disconnect()
+ self._mw.action_periodic_state_check.toggled.disconnect()
+ self.sigSwitchChanged.disconnect()
+
+ self.saveWindowPos(self._mw)
+ self._delete_switches()
+ self._mw.close()
+
+ def show(self):
+ """ Make sure that the window is visible and at the top.
+ """
+ self._mw.show()
+
+ def _populate_switches(self):
+ """ Dynamically build the gui
+ """
+ self._widgets = dict()
+ for ii, (switch, states) in enumerate(self.switchlogic().available_states.items()):
+ label = self._get_switch_label(switch)
+ if len(states) > 2 or self._switch_style == SwitchStyle.RADIO_BUTTON:
+ switch_widget = SwitchRadioButtonWidget(switch_states=states)
+ self._widgets[switch] = (label, switch_widget)
+ self._mw.main_layout.addWidget(self._widgets[switch][0], ii, 0)
+ self._mw.main_layout.addWidget(self._widgets[switch][1], ii, 1)
+ switch_widget.sigStateChanged.connect(self.__get_state_update_func(switch))
+ elif self._switch_style == SwitchStyle.TOGGLE_SWITCH:
+ if self._alt_toggle_switch_style:
+ switch_widget = ToggleSwitchWidget(switch_states=states, thumb_track_ratio=1.35)
+ else:
+ switch_widget = ToggleSwitchWidget(switch_states=states, thumb_track_ratio=0.9)
+ self._widgets[switch] = (label, switch_widget)
+ switch_widget.setSizePolicy(QtWidgets.QSizePolicy.Fixed,
+ QtWidgets.QSizePolicy.Fixed)
+ self._mw.main_layout.addWidget(self._widgets[switch][0], ii, 0)
+ self._mw.main_layout.addWidget(switch_widget, ii, 1)
+ switch_widget.sigStateChanged.connect(self.__get_state_update_func(switch))
+
+ @staticmethod
+ def _get_switch_label(switch):
+ """ Helper function to create a QLabel for a single switch.
+
+ @param str switch: The name of the switch to create the label for
+ @return QWidget: QLabel with switch name
+ """
+ label = QtWidgets.QLabel(f'{switch}:')
+ font = QtGui.QFont()
+ font.setBold(True)
+ font.setPointSize(11)
+ # font.setPixelSize(int(round(0.75 * QtWidgets.QLineEdit().sizeHint().height())))
+ label.setFont(font)
+ # label.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding,
+ # QtWidgets.QSizePolicy.MinimumExpanding)
+ label.setMinimumWidth(label.sizeHint().width())
+ label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+ return label
+
+ def _delete_switches(self):
+ """ Delete all the buttons from the main layout. """
+ for switch in reversed(tuple(self._widgets)):
+ label, widget = self._widgets[switch]
+ widget.sigStateChanged.disconnect()
+ self._mw.main_layout.removeWidget(label)
+ self._mw.main_layout.removeWidget(widget)
+ label.setParent(None)
+ widget.setParent(None)
+ del self._widgets[switch]
+ label.deleteLater()
+ widget.deleteLater()
+
+ @QtCore.Slot(dict)
+ def _switches_updated(self, states):
+ """ Helper function to update the GUI on a change of the states in the logic.
+ This function is connected to the signal coming from the switchlogic signaling a change in states.
+ @param dict states: The state dict of the form {"switch": "state"}
+ @return: None
+ """
+ for switch, state in states.items():
+ self._widgets[switch][1].set_state(state)
+
+ @QtCore.Slot(bool)
+ def _watchdog_updated(self, enabled):
+ """ Update the menu action accordingly if the watchdog has been (de-)activated.
+
+ @param bool enabled: Watchdog active (True) or inactive (False)
+ """
+ if enabled != self._mw.action_periodic_state_check.isChecked():
+ self._mw.action_periodic_state_check.blockSignals(True)
+ self._mw.action_periodic_state_check.setChecked(enabled)
+ self._mw.action_periodic_state_check.blockSignals(False)
+
+ def _update_switch_appearance(self, action):
+ index = self._mw.switch_view_actions.index(action)
+ if index != self._switch_style:
+ self._switch_style = SwitchStyle(index)
+ self._mw.close()
+ self._delete_switches()
+ self._mw.centralWidget().setFixedSize(1, 1)
+ self._populate_switches()
+ self._switches_updated(self.switchlogic().states)
+ self._update_state_colorscheme()
+ self._mw.show()
+
+ def _update_state_colorscheme(self):
+ self._state_colorscheme = StateColorScheme(self._mw.action_view_highlight_state.isChecked())
+ if self._state_colorscheme is StateColorScheme.HIGHLIGHT:
+ checked_color = self._mw.palette().highlight().color()
+ unchecked_color = None
+ else:
+ checked_color = None
+ unchecked_color = None
+ for widget in self._widgets.values():
+ widget[1].set_state_colors(unchecked_color, checked_color)
+ widget[1].update()
+
+ @QtCore.Slot(bool)
+ def _update_toggle_switch_style(self, checked):
+ if self._alt_toggle_switch_style != checked:
+ self._alt_toggle_switch_style = checked
+ if self._switch_style == SwitchStyle.TOGGLE_SWITCH:
+ self._mw.close()
+ self._delete_switches()
+ self._mw.centralWidget().setFixedSize(1, 1)
+ self._populate_switches()
+ self._switches_updated(self.switchlogic().states)
+ self._update_state_colorscheme()
+ self._mw.show()
+
+ def __get_state_update_func(self, switch):
+ def update_func(state):
+ self.sigSwitchChanged.emit(switch, state)
+ return update_func
diff --git a/gui/switch/switch_state_widgets.py b/gui/switch/switch_state_widgets.py
new file mode 100644
index 0000000000..a6288b5abe
--- /dev/null
+++ b/gui/switch/switch_state_widgets.py
@@ -0,0 +1,187 @@
+# -*- coding: utf-8 -*-
+"""
+This file contains the qudi switch state QWidgets for the GUI module SwitchGui.
+
+Qudi is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Qudi is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Qudi. If not, see .
+
+Copyright (c) the Qudi Developers. See the COPYRIGHT.txt file at the
+top-level directory of this distribution and at
+"""
+
+from qtpy import QtWidgets, QtCore, QtGui
+from qtwidgets.toggle_switch import ToggleSwitch
+
+
+class SwitchRadioButtonWidget(QtWidgets.QWidget):
+ """
+ """
+
+ sigStateChanged = QtCore.Signal(str)
+
+ def __init__(self, parent=None, switch_states=('Off', 'On')):
+ assert len(switch_states) >= 2, 'switch_states must be tuple of at least 2 strings'
+ assert all(isinstance(s, str) and s for s in switch_states), \
+ 'switch state must be non-empty str'
+ super().__init__(parent=parent)
+ layout = QtWidgets.QHBoxLayout()
+ layout.setAlignment(QtCore.Qt.AlignCenter)
+ layout.setContentsMargins(2, 2, 2, 2)
+ self.setLayout(layout)
+
+ self.switch_states = tuple(switch_states)
+ self._state_colors = (None, None)
+ self.radio_buttons = {state: QtWidgets.QRadioButton() for state in switch_states}
+ self._labels = {state: QtWidgets.QLabel(state) for state in switch_states}
+ button_group = QtWidgets.QButtonGroup(self)
+ for ii, (state, button) in enumerate(self.radio_buttons.items()):
+ button.setLayoutDirection(QtCore.Qt.RightToLeft)
+ button_group.addButton(button, ii)
+ label = self._labels[state]
+ label.setTextFormat(QtCore.Qt.RichText)
+ label.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+ layout.addWidget(button)
+ layout.addWidget(label)
+ layout.addStretch()
+ button_group.buttonToggled.connect(self.__button_toggled_cb)
+
+ def __button_toggled_cb(self, button, checked):
+ """
+
+ @param button:
+ @param checked:
+ """
+ if checked:
+ for state, radio in self.radio_buttons.items():
+ if button is radio:
+ self.sigStateChanged.emit(state)
+ self._update_colors()
+
+ @property
+ def switch_state(self):
+ for state, button in self.radio_buttons.items():
+ if button.isChecked():
+ return state
+
+ @switch_state.setter
+ def switch_state(self, state):
+ self.set_state(state)
+
+ @QtCore.Slot(str)
+ def set_state(self, state):
+ assert state in self.switch_states, f'Invalid switch state: "{state}"'
+ button = self.radio_buttons[state]
+ if not button.isChecked():
+ button.blockSignals(True)
+ button.setChecked(True)
+ button.blockSignals(False)
+ self._update_colors()
+
+ def set_state_colors(self, unchecked=None, checked=None):
+ assert unchecked is None or isinstance(unchecked, QtGui.QColor), \
+ 'arguments must be QColor object or None'
+ assert checked is None or isinstance(checked, QtGui.QColor), \
+ 'arguments must be QColor object or None'
+ self._state_colors = (unchecked, checked)
+ self._update_colors()
+
+ def _update_colors(self):
+ for state, button in self.radio_buttons.items():
+ label = self._labels[state]
+ color = self._state_colors[int(button.isChecked())]
+ if color is None:
+ label.setText(state)
+ else:
+ label.setText(f'{state}')
+
+
+class ToggleSwitchWidget(QtWidgets.QWidget):
+ """
+ """
+
+ sigStateChanged = QtCore.Signal(str)
+
+ def __init__(self, parent=None, switch_states=('Off', 'On'), thumb_track_ratio=1):
+ super().__init__(parent=parent)
+ assert len(switch_states) == 2, 'switch_states must be tuple of exactly 2 strings'
+ assert all(isinstance(s, str) and s for s in switch_states), \
+ 'switch state must be non-empty str'
+ self.switch_states = tuple(switch_states)
+ self._state_colors = (None, None)
+
+ layout = QtWidgets.QHBoxLayout()
+ layout.setAlignment(QtCore.Qt.AlignCenter)
+ layout.setContentsMargins(2, 2, 2, 2)
+ self.setLayout(layout)
+ self.toggle_switch = ToggleSwitch(None, *switch_states, thumb_track_ratio)
+ self.toggle_switch.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
+ if thumb_track_ratio > 1:
+ self.labels = (QtWidgets.QLabel(switch_states[0]), QtWidgets.QLabel(switch_states[1]))
+ self.labels[0].setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+ self.labels[0].setTextFormat(QtCore.Qt.RichText)
+ self.labels[1].setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+ self.labels[1].setTextFormat(QtCore.Qt.RichText)
+ layout.addWidget(self.labels[0])
+ layout.addWidget(self.toggle_switch)
+ layout.addWidget(self.labels[1])
+ else:
+ self.labels = None
+ layout.addWidget(self.toggle_switch)
+ self.toggle_switch.clicked.connect(self.__button_triggered_cb)
+
+ @property
+ def switch_state(self):
+ return self.toggle_switch.current_state
+
+ @switch_state.setter
+ def switch_state(self, state):
+ self.set_state(state)
+
+ @QtCore.Slot(str)
+ def set_state(self, state):
+ assert state in self.switch_states, f'Invalid switch state: "{state}"'
+ self.toggle_switch.setChecked(bool(self.switch_states.index(state)))
+ self._update_colors()
+
+ def set_state_colors(self, unchecked=None, checked=None):
+ assert unchecked is None or isinstance(unchecked, QtGui.QColor), \
+ 'arguments must be QColor object or None'
+ assert checked is None or isinstance(checked, QtGui.QColor), \
+ 'arguments must be QColor object or None'
+ self._state_colors = (unchecked, checked)
+ self._update_colors()
+
+ def _update_colors(self):
+ if self.labels is not None:
+ checked = self.toggle_switch.isChecked()
+ color = self._state_colors[int(not checked)]
+ if color is None:
+ self.labels[0].setText(self.switch_states[0])
+ else:
+ self.labels[0].setText(f'{self.switch_states[0]}')
+ color = self._state_colors[int(checked)]
+ if color is None:
+ self.labels[1].setText(self.switch_states[1])
+ else:
+ self.labels[1].setText(f'{self.switch_states[1]}')
+
+ @QtCore.Slot()
+ @QtCore.Slot(bool)
+ def __button_triggered_cb(self, checked):
+ """
+
+ @param button:
+ @param checked:
+ """
+ self.sigStateChanged.emit(self.toggle_switch.current_state)
+ self._update_colors()
diff --git a/gui/switcher/switchgui.py b/gui/switcher/switchgui.py
deleted file mode 100644
index b14a005760..0000000000
--- a/gui/switcher/switchgui.py
+++ /dev/null
@@ -1,125 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-This file contains the Qudi console GUI module.
-
-Qudi is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-Qudi is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with Qudi. If not, see .
-
-Copyright (c) the Qudi Developers. See the COPYRIGHT.txt file at the
-top-level directory of this distribution and at
-"""
-
-import os
-
-from core.connector import Connector
-from gui.guibase import GUIBase
-from qtpy import QtWidgets
-from qtpy import QtCore
-from qtpy import uic
-
-
-class SwitchGui(GUIBase):
- """ A grephical interface to mofe switches by hand and change their calibration.
- """
-
- # declare connectors
- switchlogic = Connector(interface='SwitchLogic')
-
- def on_activate(self):
- """Create all UI objects and show the window.
- """
- self._mw = SwitchMainWindow()
- lsw = self.switchlogic()
- # For each switch that the logic has, add a widget to the GUI to show its state
- for hw in lsw.switches:
- frame = QtWidgets.QGroupBox(hw, self._mw.scrollAreaWidgetContents)
- frame.setAlignment(QtCore.Qt.AlignLeft)
- frame.setFlat(False)
- self._mw.layout.addWidget(frame)
- layout = QtWidgets.QVBoxLayout(frame)
- for switch in lsw.switches[hw]:
- swidget = SwitchWidget(switch, lsw.switches[hw][switch])
- layout.addWidget(swidget)
-
- self.restoreWindowPos(self._mw)
- self.show()
-
- def show(self):
- """Make sure that the window is visible and at the top.
- """
- self._mw.show()
-
- def on_deactivate(self):
- """ Hide window and stop ipython console.
- """
- self.saveWindowPos(self._mw)
- self._mw.close()
-
-
-class SwitchMainWindow(QtWidgets.QMainWindow):
- """ Helper class for window loaded from UI file.
- """
- def __init__(self):
- """ Create the switch GUI window.
- """
- # Get the path to the *.ui file
- this_dir = os.path.dirname(__file__)
- ui_file = os.path.join(this_dir, 'ui_switchgui.ui')
-
- # Load it
- super().__init__()
- uic.loadUi(ui_file, self)
- self.show()
-
- # Add layout that we want to fill
- self.layout = QtWidgets.QVBoxLayout(self.scrollArea)
-
-class SwitchWidget(QtWidgets.QWidget):
- """ A widget that shows all data associated to a switch.
- """
- def __init__(self, switch, hwobject):
- """ Create a switch widget.
-
- @param dict switch: dict that contains reference to hardware module as 'hw' and switch number as 'n'.
- """
- # Get the path to the *.ui file
- this_dir = os.path.dirname(__file__)
- ui_file = os.path.join(this_dir, 'ui_switch_widget.ui')
-
- # Load it
- super().__init__()
- uic.loadUi(ui_file, self)
-
- # get switch states from the logic and put them in the GUI elements
- self.switch = switch
- self.hw = hwobject
- self.SwitchButton.setChecked( self.hw.getSwitchState(self.switch) )
- self.calOffVal.setValue( self.hw.getCalibration(self.switch, 'Off') )
- self.calOnVal.setValue(self.hw.getCalibration(self.switch, 'On'))
- self.switchTimeLabel.setText('{0}s'.format(self.hw.getSwitchTime(self.switch)))
- # connect button
- self.SwitchButton.clicked.connect(self.toggleSwitch)
-
- def toggleSwitch(self):
- """ Invert the state of the switch associated with this widget.
- """
- if self.SwitchButton.isChecked():
- self.hw.switchOn(self.switch)
- else:
- self.hw.switchOff(self.switch)
-
- def switchStateUpdated(self):
- """ Get state of switch from hardware module and adjust checkbox to correct value.
- """
- self.SwitchButton.setChecked(self.hw.getSwitchState(self.switch))
-
diff --git a/gui/switcher/ui_switch_widget.ui b/gui/switcher/ui_switch_widget.ui
deleted file mode 100644
index 48c3f07efe..0000000000
--- a/gui/switcher/ui_switch_widget.ui
+++ /dev/null
@@ -1,106 +0,0 @@
-
-
- SwitchWidget
-
-
-
- 0
- 0
- 400
- 58
-
-
-
-
- 0
- 0
-
-
-
-
- 400
- 58
-
-
-
-
- 400
- 58
-
-
-
- Form
-
-
- -
-
-
- Switching Time
-
-
-
- -
-
-
- Switching time
-
-
-
- -
-
-
- On
-
-
- true
-
-
- false
-
-
-
- -
-
-
- 1000000.000000000000000
-
-
-
- -
-
-
- 1000000.000000000000000
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- On Calibration
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Off Calibration
-
-
-
-
-
-
-
-
diff --git a/gui/switcher/ui_switchgui.ui b/gui/switcher/ui_switchgui.ui
deleted file mode 100644
index 7ab170fc87..0000000000
--- a/gui/switcher/ui_switchgui.ui
+++ /dev/null
@@ -1,83 +0,0 @@
-
-
- MainWindow
-
-
-
- 0
- 0
- 466
- 600
-
-
-
- qudi: Switches
-
-
-
- -
-
-
- true
-
-
-
-
- 0
- 0
- 446
- 562
-
-
-
-
-
-
-
-
-
-
-
- ../../artwork/icons/oxygen/22x22/application-exit.png../../artwork/icons/oxygen/22x22/application-exit.png
-
-
- Close
-
-
-
-
-
-
- actionClose
- triggered()
- MainWindow
- close()
-
-
- -1
- -1
-
-
- 232
- 299
-
-
-
-
-
diff --git a/hardware/awg/keysight_m819x.py b/hardware/awg/keysight_m819x.py
new file mode 100644
index 0000000000..1323073151
--- /dev/null
+++ b/hardware/awg/keysight_m819x.py
@@ -0,0 +1,2816 @@
+# -*- coding: utf-8 -*-
+
+"""
+This file contains the Qudi hardware module for the Keysight M819X AWG series.
+
+Qudi is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Qudi is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Qudi. If not, see .
+
+Copyright (c) the Qudi Developers. See the COPYRIGHT.txt file at the
+top-level directory of this distribution and at
+"""
+
+
+import visa
+import os
+import time
+import numpy as np
+import scipy.interpolate
+from fnmatch import fnmatch
+from collections import OrderedDict
+from abc import abstractmethod
+import re
+
+from core.module import Base
+from core.configoption import ConfigOption
+from interface.pulser_interface import PulserInterface, PulserConstraints, SequenceOption
+from core.util.modules import get_home_dir
+
+
+class AWGM819X(Base, PulserInterface):
+ """
+ A hardware module for AWGs of the Keysight M819X series for generating
+ waveforms and sequences thereof.
+ """
+
+ _visa_address = ConfigOption(name='awg_visa_address', default='TCPIP0::localhost::hislip0::INSTR', missing='warn')
+ _awg_timeout = ConfigOption(name='awg_timeout', default=20, missing='warn')
+ _pulsed_file_dir = ConfigOption(name='pulsed_file_dir', default=os.path.join(get_home_dir(), 'pulsed_file_dir'),
+ missing='warn')
+ _assets_storage_path = ConfigOption(name='assets_storage_path', default=os.path.join(get_home_dir(), 'saved_pulsed_assets'),
+ missing='warn')
+ _sample_rate_div = ConfigOption(name='sample_rate_div', default=1, missing='warn')
+ _dac_amp_mode = None
+ _wave_mem_mode = None
+ _wave_file_extension = '.bin'
+ _wave_transfer_datatype = 'h'
+
+ # explicitly set low/high levels for [[d_ch1_low, d_ch1_high], [d_ch2_low, d_ch2_high], ...]
+ _d_ch_level_low_high = ConfigOption(name='d_ch_level_low_high', default=[], missing='nothing')
+
+ def __init__(self, config, **kwargs):
+ super().__init__(config=config, **kwargs)
+
+ self._BRAND = ''
+ self._MODEL = ''
+ self._SERIALNUMBER = ''
+ self._FIRMWARE_VERSION = ''
+
+ self._sequence_mode = False # set in on_activate()
+ self._debug_check_all_commands = False # # For development purpose, might slow down
+
+ @property
+ @abstractmethod
+ def n_ch(self):
+ pass
+
+ @property
+ @abstractmethod
+ def marker_on(self):
+ pass
+
+ @property
+ @abstractmethod
+ def interleaved_wavefile(self):
+ pass
+
+ def on_activate(self):
+ """Initialisation performed during activation of the module.
+ """
+ self._rm = visa.ResourceManager()
+
+ self._pulsed_file_dir = self._pulsed_file_dir.replace('/', '\\') # as expected from awg drier
+ self._assets_storage_path = self._assets_storage_path.replace('/', '\\')
+ self._create_dir(self._pulsed_file_dir)
+ self._create_dir(self._assets_storage_path)
+
+ # connect to awg using PyVISA
+ try:
+ self.awg = self._rm.open_resource(self._visa_address)
+ # set timeout by default to 30 sec
+ self.awg.timeout = self._awg_timeout * 1000
+ except:
+ self.awg = None
+ self.log.error('VISA address "{0}" not found by the pyVISA resource manager.\nCheck '
+ 'the connection by using for example "Keysight Connection Expert".'
+ ''.format(self._visa_address))
+ return
+
+ if self.awg is not None:
+ mess = self.query('*IDN?').split(',')
+ self._BRAND = mess[0]
+ self._MODEL = mess[1]
+ self._SERIALNUMBER = mess[2]
+ self._FIRMWARE_VERSION = mess[3]
+
+ self.log.info('Load the device model "{0}" from "{1}" with '
+ 'serial number "{2}" and firmware version "{3}" '
+ 'successfully.'.format(self._MODEL, self._BRAND,
+ self._SERIALNUMBER,
+ self._FIRMWARE_VERSION))
+ self._sequence_mode = 'SEQ' in self.query('*OPT?').split(',')
+ self._init_device()
+
+ def on_deactivate(self):
+ """ Required tasks to be performed during deactivation of the module. """
+
+ try:
+ self.awg.close()
+ self.connected = False
+ except:
+ self.log.warning('Closing AWG connection using pyvisa failed.')
+ self.log.info('Closed connection to AWG')
+
+ @abstractmethod
+ 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:
+ {: ,
+ : ,
+ ...}
+
+ 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).
+ """
+ pass
+
+ def pulser_on(self):
+ """ Switches the pulsing device on.
+
+ @return int: error code (0:OK, -1:error, higher number corresponds to
+ current status of the device. Check then the
+ class variable status_dic.)
+ """
+ self._write_output_on()
+
+ # Sec. 6.4 from manual:
+ # In the program it is recommended to send the command for starting
+ # data generation (:INIT:IMM) as the last command. This way
+ # intermediate stop/restarts (e.g. when changing sample rate or
+ # loading a waveform) are avoided and optimum execution performance is
+ # achieved.
+
+ # wait until the AWG switched the outputs on
+ while not self._is_output_on():
+ time.sleep(0.25)
+
+ self.write(':INIT:IMM')
+ self.write('*WAI')
+
+ # in dynamic mode with external pattern jump, we start by a software trigger
+ # subsequent triggers are generated by external control hw
+ if self.get_trigger_mode() == "trig" and self.get_dynamic_mode():
+ self.send_trigger_event()
+
+ return self.get_status()[0]
+
+ def pulser_off(self):
+ """ Switches the pulsing device off.
+ @return int: error code (0:OK, -1:error, higher number corresponds to
+ current status of the device. Check then the
+ class variable status_dic.)
+ """
+
+ self.write(':ABOR')
+
+ # wait until the AWG has actually stopped
+ while self._is_awg_running():
+ time.sleep(0.25)
+
+ return self.get_status()[0]
+
+ def load_waveform(self, load_dict, to_nextfree_segment=False):
+ """ 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 is 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 is 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.
+ """
+
+ self.set_seq_mode('ARB')
+ self._delete_all_sequences() # leave sequence mode
+
+ self.log.debug("Load_waveform call with dict/list {}".format(load_dict))
+
+ load_dict = self._load_list_2_dict(load_dict)
+
+ active_analog = self._get_active_d_or_a_channels(only_analog=True)
+
+ # Check if all channels to load to are active
+ channels_to_set = {'a_ch{0:d}'.format(chnl_num) for chnl_num in load_dict}
+ if not channels_to_set.issubset(active_analog):
+ self.log.error('Unable to load waveforms into channels.\n'
+ 'One or more channels to set are not active.\n'
+ 'channels_to_set are: ', channels_to_set, 'and\n'
+ 'analog_channels are: ', active_analog)
+ return self.get_loaded_assets()
+
+ # Check if all waveforms to load are present on device memory
+ if not set(load_dict.values()).issubset(self.get_waveform_names()):
+ self.log.error('Unable to load waveforms into channels.\n'
+ 'One or more waveforms to load are missing: {}'.format(
+ set(load_dict.values())
+ ))
+ return self.get_loaded_assets()
+
+ if load_dict == {}:
+ self.log.warning('No file and channel provided for load!\n'
+ 'Correct that!\nCommand will be ignored.')
+ return self.get_loaded_assets()
+
+ self._load_wave_from_memory(load_dict, to_nextfree_segment=to_nextfree_segment)
+
+ self.set_trigger_mode('cont')
+ self.check_dev_error()
+
+ 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.
+ 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.
+ """
+
+ if not (set(self.get_loaded_assets()[0].values())).issubset(set([sequence_name])):
+ self.log.error('Unable to load sequence into channels.\n'
+ 'Make sure to call write_sequence() first.')
+ return self.get_loaded_assets()
+
+ self.write_all_ch(':FUNC{}:MODE STS', all_by_one={'m8195a': True}) # activate the sequence mode
+ """
+ select the first segment in your sequence, before any dynamic sequence selection.
+ """
+ self.write_all_ch(":STAB{}:SEQ:SEL 0", all_by_one={'m8195a': True})
+ self.write_all_ch(":STAB{}:DYN ON", all_by_one={'m8195a': True})
+
+ return 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. '_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')
+ """
+
+ # Get all active channels
+ active_analog = self._get_active_d_or_a_channels(only_analog=True)
+ channel_numbers = self.chstr_2_chnum(active_analog, return_list=True)
+
+ # Get assets per channel
+ loaded_assets = dict()
+ type_per_ch = []
+ is_err = False
+
+ for chnl_num in channel_numbers:
+
+ asset_name = 'ERROR_NAME'
+
+ if self.get_loaded_assets_num(chnl_num, mode='segment') >= 1 \
+ and self.get_loaded_assets_num(chnl_num, mode='sequence') == 0:
+ # arb mode with at least one waveform
+
+ type_per_ch.append('waveform')
+ seg_id_active = int(self.query(':TRAC{}:SEL?'.format(chnl_num)))
+ ids_avail = self.get_loaded_assets_id(chnl_num, mode='segment')
+ if seg_id_active not in ids_avail:
+ seg_id_active = ids_avail[0]
+ self.log.error("Active segment id {} outside available sequences ({}) for unknown reason."
+ " Set to segment id to 1.".format(seg_id_active, ids_avail))
+
+ self.write(':TRAC1:SEL {:d}'.format(seg_id_active))
+ self.write(':TRAC2:SEL {:d}'.format(seg_id_active))
+
+ asset_name = self.get_loaded_asset_name_by_id(chnl_num, seg_id_active, mode='segment')
+
+ elif self.get_loaded_assets_num(chnl_num, mode='segment') >= 1 \
+ and self.get_loaded_assets_num(chnl_num, mode='sequence') == 1:
+ # seq mode with at least one waveform
+ # currently only a single uploaded sequence supported
+ type_per_ch.append('sequence')
+ asset_name = self.get_loaded_assets_name(chnl_num, mode='sequence')[0]
+ elif self.get_loaded_assets_num(chnl_num, mode='segment') == 0 \
+ and self.get_loaded_assets_num(chnl_num, mode='sequence') == 0:
+ # arb mode but no waveform
+
+ type_per_ch.append('waveform')
+ asset_name = ''
+
+ else:
+ is_err = True
+ """
+ if self.get_loaded_assets_num(chnl_num, mode='segment') > 1 \
+ and self.get_loaded_assets_num(chnl_num, mode='sequence') == 0:
+
+ self.log.error("Multiple segments, but no sequence defined")
+ """
+ if self.get_loaded_assets_num(chnl_num, mode='sequence') > 1:
+ self.log.error("Multiple sequences defined. Should only be 1.")
+ # todo: implement more than 1 sequence
+
+ loaded_assets[chnl_num] = asset_name
+
+ if not all(x == type_per_ch[0] for x in type_per_ch) or not channel_numbers:
+ # make sure type is same for all channels
+ is_err = True
+ if is_err:
+ self.log.error('Unable to determine loaded assets.')
+ return dict(), ''
+
+ return loaded_assets, type_per_ch[0] # interface requires same type for all ch
+
+ def clear_all(self):
+ """ Clears all loaded waveforms from the pulse generators RAM/workspace.
+
+ @return int: error code (0:OK, -1:error)
+ """
+
+ self.write_all_ch(':TRAC{}:DEL:ALL', all_by_one={'m8195a': True})
+ self._flag_segment_table_req_update = True
+
+ return
+
+ 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 = {-1: 'Failed Request or Communication',
+ 0: 'Device has stopped, but can receive commands',
+ 1: 'Device is active and running'}
+
+ current_status = -1 if self.awg is None else int(self._is_awg_running())
+ # All the other status messages should have higher integer values then 1.
+
+ return current_status, status_dic
+
+ def get_sample_rate(self):
+ """ Get the sample rate of the pulse generator hardware
+
+ @return float: The current sample rate of the device (in Hz)
+
+ Do not return a saved sample rate from an attribute, but instead retrieve the current
+ sample rate directly from the device.
+ """
+ sample_rate = float(self.query(':FREQ:RAST?')) / self._sample_rate_div
+ return sample_rate
+
+ def set_sample_rate(self, sample_rate):
+ """ Set the sample rate of the pulse generator hardware.
+
+ @param float sample_rate: The sampling rate to be set (in Hz)
+
+ @return float: the sample rate returned from the device (in Hz).
+
+ Note: After setting the sampling rate of the device, use the actually set return value for
+ further processing.
+ """
+ sample_rate_GHz = (sample_rate * self._sample_rate_div) / 1e9
+ self.write(':FREQ:RAST {0:.4G}GHz\n'.format(sample_rate_GHz))
+ while int(self.query('*OPC?')) != 1:
+ time.sleep(0.25)
+ time.sleep(0.2)
+ return self.get_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}
+ """
+
+ amp = dict()
+ off = dict()
+
+ chnl_list = self._get_all_analog_channels()
+
+ # get pp amplitudes
+ if amplitude is None:
+ for ch_num, chnl in enumerate(chnl_list, 1):
+ amp[chnl] = float(self.query(':VOLT{0:d}?'.format(ch_num)))
+ else:
+ for chnl in amplitude:
+ if chnl in chnl_list:
+ ch_num = self.chstr_2_chnum(chnl)
+ amp[chnl] = float(self.query(':VOLT{0:d}?'.format(ch_num)))
+ else:
+ self.log.warning('Get analog amplitude from channel "{0}" failed. '
+ 'Channel non-existent.'.format(chnl))
+
+ # get voltage offsets
+ if offset is None:
+ for ch_num, chnl in enumerate(chnl_list):
+ ch_num = self.chstr_2_chnum(chnl)
+ off[chnl] = float(self.query(':VOLT{0:d}:OFFS?'.format(ch_num)))
+ else:
+ for chnl in offset:
+ if chnl in chnl_list:
+ ch_num = self.chstr_2_chnum(chnl)
+ off[chnl] = float(self.query(':VOLT{0:d}:OFFS?'.format(ch_num)))
+ else:
+ self.log.warning('Get analog offset from channel "{0}" failed. '
+ 'Channel non-existent.'.format(chnl))
+ return amp, off
+
+ def set_analog_level(self, amplitude=None, offset=None):
+ """ Set amplitude and/or offset value of the provided analog channel(s).
+
+ @param dict amplitude: dictionary, with key being the channel descriptor string
+ (i.e. 'a_ch1', 'a_ch2') and items being the amplitude values
+ (in Volt peak to peak, i.e. the full amplitude) for the desired
+ channel.
+ @param dict offset: dictionary, with key being the channel descriptor string
+ (i.e. 'a_ch1', 'a_ch2') and items being the offset values
+ (in absolute volt) for the desired channel.
+
+ @return (dict, dict): tuple of two dicts with the actual set values for amplitude and
+ offset for ALL channels.
+
+ 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.
+ """
+ # Check the inputs by using the constraints...
+ constraints = self.get_constraints()
+ # ...and the available analog channels
+ analog_channels = self._get_all_analog_channels()
+
+ # amplitude sanity check
+ if amplitude is not None:
+ for chnl in amplitude:
+ ch_num = self.chstr_2_chnum(chnl)
+ if chnl not in analog_channels:
+ self.log.warning('Channel to set (a_ch{0}) not available in AWG.\nSetting '
+ 'analogue voltage for this channel ignored.'.format(ch_num))
+ del amplitude[chnl]
+ if amplitude[chnl] < constraints.a_ch_amplitude.min:
+ self.log.warning('Minimum Vpp for channel "{0}" is {1}. Requested Vpp of {2}V '
+ 'was ignored and instead set to min value.'
+ ''.format(chnl, constraints.a_ch_amplitude.min,
+ amplitude[chnl]))
+ amplitude[chnl] = constraints.a_ch_amplitude.min
+ elif amplitude[chnl] > constraints.a_ch_amplitude.max:
+ self.log.warning('Maximum Vpp for channel "{0}" is {1}. Requested Vpp of {2}V '
+ 'was ignored and instead set to max value.'
+ ''.format(chnl, constraints.a_ch_amplitude.max,
+ amplitude[chnl]))
+ amplitude[chnl] = constraints.a_ch_amplitude.max
+ # offset sanity check
+ if offset is not None:
+ for chnl in offset:
+ ch_num = self.chstr_2_chnum(chnl)
+ if chnl not in analog_channels:
+ self.log.warning('Channel to set (a_ch{0}) not available in AWG.\nSetting '
+ 'offset voltage for this channel ignored.'.format(chnl))
+ del offset[chnl]
+ if offset[chnl] < constraints.a_ch_offset.min:
+ self.log.warning('Minimum offset for channel "{0}" is {1}. Requested offset of '
+ '{2}V was ignored and instead set to min value.'
+ ''.format(chnl, constraints.a_ch_offset.min, offset[chnl]))
+ offset[chnl] = constraints.a_ch_offset.min
+ elif offset[chnl] > constraints.a_ch_offset.max:
+ self.log.warning('Maximum offset for channel "{0}" is {1}. Requested offset of '
+ '{2}V was ignored and instead set to max value.'
+ ''.format(chnl, constraints.a_ch_offset.max,
+ offset[chnl]))
+ offset[chnl] = constraints.a_ch_offset.max
+
+ if amplitude is not None:
+ for chnl, amp in amplitude.items():
+ ch_num = self.chstr_2_chnum(chnl)
+ self.write(':VOLT{0} {1:.4f}'.format(ch_num, amp))
+ while int(self.query('*OPC?')) != 1:
+ time.sleep(0.25)
+
+ if offset is not None:
+ for chnl, off in offset.items():
+ ch_num = self.chstr_2_chnum(chnl)
+ self.write(':VOLT{0}:OFFS {1:.4f}'.format(ch_num, off))
+ while int(self.query('*OPC?')) != 1:
+ time.sleep(0.25)
+ return self.get_analog_level()
+
+ def get_digital_level(self, low=None, high=None):
+ """ Retrieve the digital low and high level of the provided/all channels.
+
+ @param list low: optional, if the low value (in Volt) of a specific channel is desired.
+ @param list high: optional, if the high value (in Volt) of a specific channel is desired.
+
+ @return: (dict, dict): tuple of two dicts, with keys being the channel descriptor strings
+ (i.e. 'd_ch1', 'd_ch2') and items being the values for those
+ channels. Both low and high value of a channel is denoted in volts.
+
+ Note: Do not return a saved low and/or high value but instead retrieve
+ the current low and/or high value directly from the device.
+
+ If nothing (or None) is passed then the levels of all channels are being returned.
+ If no digital channels are present, return just an empty dict.
+
+ Example of a possible input:
+ low = ['d_ch1', 'd_ch4']
+ to obtain the low voltage values of digital channel 1 an 4. A possible answer might be
+ {'d_ch1': -0.5, 'd_ch4': 2.0} {'d_ch1': 1.0, 'd_ch2': 1.0, 'd_ch3': 1.0, 'd_ch4': 4.0}
+ Since no high request was performed, the high values for ALL channels are returned (here 4).
+ """
+
+ low_val = {}
+ high_val = {}
+
+ digital_channels = self._get_all_digital_channels()
+
+ if low is None:
+ low = digital_channels
+ if high is None:
+ high = digital_channels
+
+ # get low marker levels
+ for chnl in low:
+ if chnl not in digital_channels:
+ continue
+ low_val[chnl] = float(
+ self.query(self._get_digital_ch_cmd(chnl) + ':LOW?'))
+ # get high marker levels
+ for chnl in high:
+ if chnl not in digital_channels:
+ continue
+ high_val[chnl] = float(
+ self.query(self._get_digital_ch_cmd(chnl) + ':HIGH?'))
+
+ 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 descriptor string
+ (i.e. 'd_ch1', 'd_ch2') and items being the low values (in volt) for the
+ desired channel.
+ @param dict high: dictionary, with key being the channel descriptor string
+ (i.e. 'd_ch1', 'd_ch2') and items being the high values (in volt) for the
+ desired channel.
+
+ @return (dict, dict): tuple of two dicts where first dict denotes the current low value and
+ the second dict the high value for ALL digital channels.
+ Keys are the channel descriptor strings (i.e. 'd_ch1', 'd_ch2')
+
+ If nothing is passed then the command will return the current voltage levels.
+
+ Note: After setting the high and/or low values of the device, use the actual set return
+ values for further processing.
+ """
+ if low is None:
+ low = self.get_digital_level()[0]
+ if high is None:
+ high = self.get_digital_level()[1]
+
+ #If you want to check the input use the constraints:
+ constraints = self.get_constraints()
+ digital_channels = self._get_all_digital_channels()
+
+ # Check the constraints for marker high level
+ for key in high:
+ if high[key] < constraints.d_ch_high.min:
+ self.log.warning('Voltages for digital values are too small for high. Setting to minimum value')
+ high[key] = constraints.d_ch_high.min
+ elif high[key] > constraints.d_ch_high.max:
+ self.log.warning('Voltages for digital values are too high for high. Setting to maximum value')
+ high[key] = constraints.d_ch_high.max
+
+ # Check the constraints for marker low level
+ for key in low:
+ if low[key] < constraints.d_ch_low.min:
+ self.log.warning('Voltages for digital values are too small for low. Setting to minimum value')
+ low[key] = constraints.d_ch_low.min
+ elif low[key] > constraints.d_ch_low.max:
+ self.log.warning('Voltages for digital values are too high for low. Setting to maximum value')
+ low[key] = constraints.d_ch_low.max
+
+ # Check the difference between marker high and low
+ for key in high:
+ if high[key] - low[key] < 0.125:
+ self.log.warning('Voltage difference is too small. Reducing low voltage level.')
+ low[key] = high[key] - 0.125
+ elif high[key] - low[key] > 2.25:
+ self.log.warning('Voltage difference is too large. Increasing low voltage level.')
+ low[key] = high[key] - 2.25
+
+
+ # set high marker levels
+ for chnl in low and high:
+ if chnl not in digital_channels:
+ continue
+
+ offs =(high[chnl] + low[chnl])/2
+ ampl = high[chnl] - low[chnl]
+ self.write(self._get_digital_ch_cmd(chnl) + ':AMPL {}'.format(ampl))
+ self.write(self._get_digital_ch_cmd(chnl) + ':OFFS {}'.format(offs))
+
+ return self.get_digital_level()
+
+ @abstractmethod
+ def get_active_channels(self, ch=None):
+ pass
+
+ 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.
+ """
+ current_channel_state = self.get_active_channels()
+
+ if ch is None:
+ return current_channel_state
+
+ if not set(current_channel_state).issuperset(ch):
+ self.log.error('Trying to (de)activate channels that are not present in M8190A.\n'
+ 'Setting of channel activation aborted.')
+ return current_channel_state
+
+ # Determine new channel activation states
+ new_channels_state = current_channel_state.copy()
+ for chnl in ch:
+ new_channels_state[chnl] = ch[chnl]
+
+ # check if the channels to set are part of the activation_config constraints
+ constraints = self.get_constraints()
+ new_active_channels = {chnl for chnl in new_channels_state if new_channels_state[chnl]}
+ if new_active_channels not in constraints.activation_config.values():
+ self.log.error('activation_config to set ({0}) is not allowed according to constraints.'
+ ''.format(new_active_channels))
+ return current_channel_state
+
+ self._set_active_ch(new_channels_state)
+
+ return self.get_active_channels()
+
+ def write_waveform(self, name, analog_samples, digital_samples, is_first_chunk, is_last_chunk,
+ total_number_of_samples):
+ """
+ Write a new waveform or append samples to an already existing waveform on the device memory.
+ The flags is_first_chunk and is_last_chunk can be used as indicator if a new waveform should
+ be created or if the write process to a waveform should be terminated.
+
+ NOTE: All sample arrays in analog_samples and digital_samples must be of equal length!
+
+ @param str name: the name of the waveform to be created/append to
+ @param dict analog_samples: keys are the generic analog channel names (i.e. 'a_ch1') and
+ values are 1D numpy arrays of type float32 containing the
+ voltage samples normalized to half Vpp (between -1 and 1).
+ @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 = []
+
+ # Sanity checks
+ if len(analog_samples) == 0:
+ self.log.error('No analog samples passed to write_waveform method in M8190A.')
+ return -1, waveforms
+
+ min_samples = self.get_constraints().waveform_length.min
+ if total_number_of_samples < min_samples:
+ self.log.error('Unable to write waveform.\nNumber of samples to write ({0:d}) is '
+ 'smaller than the allowed minimum waveform length ({1:d}).'
+ ''.format(total_number_of_samples, min_samples))
+ return -1, waveforms
+
+ active_analog = self._get_active_d_or_a_channels(only_analog=True)
+ active_channels = set(self.get_active_channels().keys())
+
+ # Sanity check of channel numbers
+ if active_channels != set(analog_samples.keys()).union(set(digital_samples.keys())):
+ self.log.error('Mismatch of channel activation and sample array dimensions for '
+ 'waveform creation.\nChannel activation is: {0}\nSample arrays have: '
+ ''.format(active_channels,
+ set(analog_samples.keys()).union(set(digital_samples.keys()))))
+ return -1, waveforms
+
+ to_segment_id = 1 # pc_hdd mode
+ if self._wave_mem_mode == 'awg_segments':
+ to_segment_id = -1
+
+ waveforms = self._write_wave_to_memory(name, analog_samples, digital_samples, active_analog,
+ to_segment_id=to_segment_id)
+
+ self.check_dev_error()
+
+ return total_number_of_samples, waveforms
+
+ def write_sequence(self, name, sequence_parameters):
+ """
+ Write a new sequence on the device memory.
+ In wave_mem_mode == 'pc_hdd' and if elements in the sequence are not available on the AWG yet, they will be
+ transferred from the PC.
+
+ @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)
+ """
+
+ # Check if device has sequencer option installed
+ if not self.has_sequence_mode():
+ self.log.error('Direct sequence generation in AWG not possible. Sequencer option not '
+ 'installed.')
+ return -1
+
+ # Check if all waveforms are present on device memory
+ avail_waveforms = set(self.get_waveform_names())
+ for waveform_tuple, param_dict in sequence_parameters:
+ if not avail_waveforms.issuperset(waveform_tuple):
+ self.log.error('Failed to create sequence "{0}" due to waveforms "{1}" not '
+ 'present in memory. Try to load them again.'.format(name, waveform_tuple))
+ return -1
+
+ num_steps = len(sequence_parameters)
+
+ if self._wave_mem_mode == 'pc_hdd':
+ # todo: check whether skips on already loaded waveforms that should be updated
+ # check whether this works as intended with mechanism
+ # generate new needs to invalidate loaded assets
+ loaded_segments_ch1 = self.get_loaded_assets_name(1, mode='segment')
+ loaded_segments_ch2 = self.get_loaded_assets_name(2, mode='segment')
+
+ waves_loaded_here = []
+ # transfer waveforms in sequence from local pc to segments in awg mem
+ for waveform_tuple, param_dict in sequence_parameters:
+ # todo: need to handle other than 2 channels?
+ waveform_list = []
+ waveform_list.append(waveform_tuple[0])
+ waveform_list.append(waveform_tuple[1])
+ wave_ch1 = self._remove_file_extension(waveform_tuple[0])
+ wave_ch2 = self._remove_file_extension(waveform_tuple[1])
+
+ if not (wave_ch1 in loaded_segments_ch1 and
+ wave_ch2 in loaded_segments_ch2) \
+ and not (wave_ch1 in waves_loaded_here and
+ wave_ch2 in waves_loaded_here):
+
+ self.log.debug("Couldn't find segments {} and {} on device for writing sequence {}. Loading...".format(
+ wave_ch1, wave_ch2, name))
+ self.load_waveform(waveform_list, to_nextfree_segment=True)
+ waves_loaded_here.append(wave_ch1)
+ waves_loaded_here.append(wave_ch2)
+ else:
+ self.log.debug("Segments {} and {} already on device for writing sequence {}. Skipping load.".format(
+ wave_ch1, wave_ch2, name))
+
+ self.log.debug("Loading of waveforms for sequence write finished.")
+ elif self._wave_mem_mode == 'awg_segments':
+ # all segments must be present on device mem already
+ pass
+ else:
+ raise ValueError("Unknown memory mode: {}".format(self._wave_mem_mode))
+
+ """
+ 8190a manual: When using dynamic sequencing, the arm mode must be set to self-armed
+ and all advancement modes must be set to Auto.
+ Additionally, the trigger mode Gated is not allowed.
+ """
+ self.write_all_ch(':FUNC{}:MODE STS', all_by_one={'m8195a': True}) # activate the sequence mode
+ self.write_all_ch(':STAB{}:RES', all_by_one={'m8195a': True}) # Reset all sequence table entries to default values
+
+ self._delete_all_sequences()
+ self._define_new_sequence(name, num_steps)
+
+ # write the actual sequence table
+ ctr_steps_written = 0
+ goto_in_sequence = False
+ for step, (wfm_tuple, seq_step) in enumerate(sequence_parameters, 1):
+
+ index = step - 1
+
+ if seq_step['go_to'] != -1:
+ goto_in_sequence = True
+
+ control = self._get_sequence_control_bin(sequence_parameters, index)
+
+ seq_loop_count = 1
+ if seq_step.repetitions == -1:
+ # this is ugly, limits maximal waiting time. 1 Sa -> approx. 0.3 s
+ seg_loop_count = 4294967295 # max value, todo: from constraints
+ else:
+ seg_loop_count = seq_step.repetitions + 1 # if repetitions = 0 then do it once
+ seg_start_offset = 0 # play whole segement from start...
+ seg_end_offset = 0xFFFFFFFF # to end
+
+ self.log.debug("For sequence table step {} with {} reps: control: {}".format(step,
+ seq_loop_count,
+ control))
+
+ segment_id_ch1 = self.get_segment_id(self._remove_file_extension(wfm_tuple[0]), 1) \
+ if len(wfm_tuple) >= 1 else -1
+ segment_id_ch2 = self.get_segment_id(self._remove_file_extension(wfm_tuple[1]), 2) \
+ if len(wfm_tuple) == 2 else -1
+
+ try:
+ # creates all segments as data entries
+ if segment_id_ch1 > -1:
+ # STAB will default to STAB1 on 8190A
+ self.write(':STAB:DATA {0}, {1}, {2}, {3}, {4}, {5}, {6}'
+ .format(index,
+ control,
+ seq_loop_count,
+ seg_loop_count,
+ segment_id_ch1,
+ seg_start_offset,
+ seg_end_offset))
+ if segment_id_ch2 > -1:
+ self.write(':STAB2:DATA {0}, {1}, {2}, {3}, {4}, {5}, {6}'
+ .format(index,
+ control,
+ seq_loop_count,
+ seg_loop_count,
+ segment_id_ch2,
+ seg_start_offset,
+ seg_end_offset))
+
+ if segment_id_ch1 + segment_id_ch2 > -1:
+ ctr_steps_written += 1
+ self.log.debug("Writing seqtable entry {}: {}".format(index, step))
+ else:
+ self.log.error("Failed while writing seqtable entry {}: {}".format(index, step))
+
+ except Exception as e:
+ self.log.error("Unknown error occured while writing to seq table: {}".format(str(e)))
+
+ if goto_in_sequence and self.get_constraints().sequence_order == "LINONLY": # SequenceOrderOption.LINONLY:
+ self.log.warning("Found go_to in step of sequence {}. Not supported and ignored.".format(name))
+
+ while int(self.query('*OPC?')) != 1:
+ time.sleep(0.25)
+
+ return int(ctr_steps_written)
+
+ def get_waveform_names(self):
+ """ Retrieve the names of all uploaded waveforms on the device.
+
+ @return list: List of all uploaded waveform name strings in the device workspace.
+ """
+
+ names = []
+
+ if self._wave_mem_mode == 'pc_hdd':
+ names = self.query('MMEM:CAT?').replace('"', '').replace("'", "").split(",")[2::3]
+ elif self._wave_mem_mode == 'awg_segments':
+
+ active_analog = self._get_active_d_or_a_channels(only_analog=True)
+ channel_numbers = self.chstr_2_chnum(active_analog, return_list=True)
+
+ for chnl_num in channel_numbers:
+ names.extend(self.get_loaded_assets_name(chnl_num, 'segment'))
+ names = list(set(names)) # make unique
+
+ else:
+ raise ValueError("Unknown memory mode: {}".format(self._wave_mem_mode))
+
+ return 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.
+ """
+ sequence_list = list()
+
+ if not self.has_sequence_mode():
+ return sequence_list
+
+ if self._wave_mem_mode == 'pc_hdd':
+ # get only the files from the dir and skip possible directories
+ log = os.listdir(self._assets_storage_path)
+ file_list = [line for line in log if not os.path.isdir(line)]
+
+ for filename in file_list:
+ if filename.endswith(('.seq', '.seqx', '.sequence')):
+ if filename not in sequence_list:
+ sequence_list.append(self._remove_file_extension(filename))
+ elif self._wave_mem_mode == 'awg_segments':
+ seqs_ch1 = self.get_loaded_assets_name(1, 'sequence')
+ seqs_ch2 = self.get_loaded_assets_name(2, 'sequence')
+
+ if seqs_ch1 != seqs_ch2:
+ self.log.error("Sequence tables for ch1/2 seem unaligned! ch1: {} ch2: {}".format(seqs_ch1,
+ seqs_ch2))
+ return seqs_ch1
+ else:
+ raise ValueError("Unknown memory mode: {}".format(self._wave_mem_mode))
+
+
+ return sequence_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 without _ch? postfix.
+ 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]
+
+ avail_waveforms = self.get_waveform_names() # incl _ch?.bin postfix
+ deleted_waveforms = list()
+
+ for name in waveform_name:
+ name_ch = self._name_with_ch(name, '?')
+ for waveform in avail_waveforms:
+ if fnmatch(waveform.lower(), name_ch + "{}".format(self._wave_file_extension)):
+ # delete case insensitive from hdd
+ self.write(':MMEM:DEL "{0}"'.format(waveform))
+ deleted_waveforms.append(waveform)
+
+ if fnmatch(waveform, name_ch):
+ # delete from awg memory
+ active_analog = self._get_active_d_or_a_channels(only_analog=True)
+ for ch_str in active_analog:
+ ch_num = self.chstr_2_chnum(ch_str)
+ try:
+ id = self.asset_name_2_id(self._name_with_ch(name, ch_num), ch_num, mode='segment')
+ except ValueError: # got already deleted
+ continue
+ self.write('TRAC{}:DEL {:d}'.format(ch_num, id))
+ # set to available segment
+ ids_avail = self.get_loaded_assets_id(ch_num)
+ if ids_avail:
+ self.write('TRAC{}:SEL {:d}'.format(ch_num, ids_avail[0]))
+ deleted_waveforms.append(waveform)
+
+ for loaded_waveform in self.get_loaded_assets()[0].values():
+ # in pc_hdd mode, avail_waveforms are only on hdd, need to clear the awg mem
+ if self._wave_mem_mode == 'pc_hdd' and fnmatch(loaded_waveform, name_ch):
+ self.clear_all()
+
+ return list(set(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]
+
+ avail_sequences = self.get_sequence_names()
+ deleted_sequences = list()
+
+ # deletes .sequence files from hdd
+ for name in sequence_name:
+ for sequence in avail_sequences:
+ # in pc_hdd mode, no need to delete
+ # .sequence files are handled by sequence generator logic
+ if fnmatch(sequence, name):
+ # awg_segment mode
+ self._delete_all_sequences() # all, as currently only support for 1 sequence
+ deleted_sequences.append(name)
+
+ if name in self.get_loaded_assets()[0].values():
+ # clear the AWG incl. all waveforms on awg memory and sequence table
+ # todo: delete only waveforms in sequence or think about only unloading the sequence
+ # while keeping the waveforms
+ self.clear_all()
+ self.write_all_ch(':STAB{}:RES',
+ all_by_one={'m8195a': True}) # Reset all sequence table entries to default values
+ deleted_sequences.append(name)
+
+ return list(set(deleted_sequences))
+
+ 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.warning('Interleave mode not available for the AWG M819xA '
+ 'Series!\n'
+ 'Method call will be ignored.')
+ return self.get_interleave()
+
+ def reset(self):
+ """ Reset the device.
+
+ @return int: error code (0:OK, -1:error)
+ """
+ self.write('*RST')
+ self.write('*WAI')
+
+ self._flag_segment_table_req_update = True
+
+ return 0
+
+ ################################################################################
+ ### Non interface methods ###
+ ################################################################################
+
+ def set_seq_mode(self, mode):
+ self.write_all_ch(":FUNC{}:MODE {}", mode, all_by_one={'m8195a': True})
+
+ def _set_dac_resolution(self):
+ pass
+
+ def _set_awg_mode(self):
+ pass
+
+ def _set_sample_rate_div(self):
+ pass
+
+ def _set_dac_amplifier_mode(self):
+ pass
+
+ @abstractmethod
+ def _write_output_on(self):
+ pass
+
+ @abstractmethod
+ def _get_digital_ch_cmd(self, d_ch_name):
+ pass
+
+ @abstractmethod
+ def _get_init_output_levels(self):
+ pass
+
+ @abstractmethod
+ def _get_sequence_control_bin(self, sequence_parameters, idx_step):
+ pass
+
+ def _delete_all_sequences(self):
+ # awg8195a has no sequence subsystem and does not need according cmds
+ pass
+
+ def _define_new_sequence(self, name, n_steps):
+ pass
+
+ def _init_device(self):
+ """ Run those methods during the initialization process."""
+
+ self.reset()
+ constr = self.get_constraints()
+
+ self.write(':ROSC:SOUR INT') # Chose source for reference clock
+
+ self._set_awg_mode()
+
+ # General procedure according to Sec. 8.22.6 in AWG8190A manual:
+ # To prepare your module for arbitrary waveform generation follow these steps:
+ # 1. Select one of the direct modes (precision or speed mode) or one of the interpolated modes ((x3, x12, x24 and x48)
+
+ self._set_dac_resolution()
+ self._set_sample_rate_div()
+ self.set_seq_mode('ARB')
+
+ # 2. Define one or multiple segments using the various forms of TRAC:DEF
+ # done in load_waveform
+ # 3. Fill the segments with values and marker data
+ # empty at init
+ # 4. Select the segment to be output in arbitrary waveform mode using
+ self.write(':TRAC1:SEL 1')
+ self.write(':TRAC2:SEL 1')
+
+ # Set the waveform directory on the local pc:
+ self.write(':MMEM:CDIR "{0}"'.format(self._pulsed_file_dir))
+
+ self.set_sample_rate(constr.sample_rate.default)
+
+ self._set_dac_amplifier_mode()
+
+ init_levels = self._get_init_output_levels()
+ self.set_analog_level(amplitude=init_levels['a_ampl'], offset=init_levels['a_offs'])
+ self.set_digital_level(low=init_levels['d_ampl_low'], high=init_levels['d_ampl_high'])
+
+ self._segment_table = [[], []] # [0]: ch1, [1]: ch2. Local, read-only copy of the device segment table
+ self._flag_segment_table_req_update = True # local copy requires update
+
+ def _load_list_2_dict(self, load_dict):
+
+ def _create_load_dict_allch(load_dict):
+ waveform = load_dict[0] # awg8196a: 1 name per segment, no individual name per channel
+ new_dict = dict()
+
+ active_analog = self._get_active_d_or_a_channels(only_analog=True)
+
+ for a_ch in active_analog:
+ ch_num = self.chstr_2_chnum(a_ch)
+ new_dict[ch_num] = waveform
+
+ return new_dict
+
+ if isinstance(load_dict, list):
+ new_dict = dict()
+ has_ch_ext = True
+
+ for waveform in load_dict:
+ pattern = ".*_ch[0-9]+?"
+ has_ch_ext = True if re.match(pattern, waveform) is not None else False
+ if has_ch_ext:
+ channel = int(waveform.rsplit('_ch', 1)[1][0])
+ new_dict[channel] = waveform
+ else:
+ break
+ if not has_ch_ext:
+ new_dict = _create_load_dict_allch(load_dict)
+
+ return new_dict
+
+ elif isinstance(load_dict, dict):
+ return load_dict
+ else:
+ self.log.error("Load dict of unexpected type: {}".format(type(load_dict)))
+
+ def _load_wave_from_memory(self, load_dict, to_nextfree_segment=False):
+ if self._wave_mem_mode == 'pc_hdd':
+ path = self._pulsed_file_dir
+ offset = 0
+
+ if not to_nextfree_segment:
+ self.clear_all()
+
+ for chnl_num, waveform in load_dict.items():
+ name = waveform.split('.bin', 1)[0] # name in front of .bin/.bin8
+ filepath = os.path.join(path, waveform)
+
+ data = self.query_bin(':MMEM:DATA? "{0}"'.format(filepath))
+ n_samples = len(data)
+ if self.interleaved_wavefile:
+ n_samples = int(n_samples / 2)
+ segment_id = self.query('TRAC{0:d}:DEF:NEW? {1:d}'.format(chnl_num, n_samples)) \
+ + '_ch{:d}'.format(chnl_num)
+ segment_id_per_ch = segment_id.rsplit("_ch", 1)[0]
+ self.write_bin(':TRAC{0}:DATA {1}, {2},'.format(chnl_num, segment_id_per_ch, offset), data)
+ self.write(':TRAC{0}:NAME {1}, "{2}"'.format(chnl_num, segment_id_per_ch, name))
+
+ self._check_uploaded_wave_name(chnl_num, name, segment_id_per_ch)
+
+ self._flag_segment_table_req_update = True
+ self.log.debug("Loading waveform {} of len {} to AWG ch {}, segment {}.".format(
+ name, n_samples, chnl_num, segment_id_per_ch))
+
+ elif self._wave_mem_mode == 'awg_segments':
+
+ if to_nextfree_segment:
+ self.log.warning("In awg_segments memory mode, 'to_nextfree_segment' has no effect."
+ "Loading only marks active, segments need to be written before.")
+ # m8195a: 1 name per segment, no individual name per channel
+
+ for chnl_num, waveform in load_dict.items():
+ waveform = load_dict[chnl_num]
+ name = waveform
+ if name.split(',')[0] == name:
+ segment_id = self.asset_name_2_id(name, chnl_num, mode='segment')
+ else:
+ segment_id = np.int(name.split(',')[0])
+ self.write(':TRAC{0}:SEL {1}'.format(chnl_num, segment_id))
+
+
+ else:
+ raise ValueError("Unknown memory mode: {}".format(self._wave_mem_mode))
+
+ def send_trigger_event(self):
+ self.write(":TRIG:BEG")
+
+ def set_trigger_mode(self, mode="cont"):
+ """
+ Trigger mode according to manual 3.3.
+ :param mode: "cont", "trig" or "gate"
+ :return:
+ """
+ if mode is "cont":
+ self.write_all_ch(":INIT:CONT{}:STAT ON", all_by_one={'m8195a': True})
+ self.write_all_ch(":INIT:GATE{}:STAT OFF", all_by_one={'m8195a': True})
+ elif mode is "trig":
+ self.write_all_ch(":INIT:CONT{}:STAT OFF", all_by_one={'m8195a': True})
+ self.write_all_ch(":INIT:GATE{}:STAT OFF", all_by_one={'m8195a': True})
+ elif mode is "gate":
+ self.write_all_ch(":INIT:CONT{}:STAT OFF", all_by_one={'m8195a': True})
+ self.write_all_ch(":INIT:GATE{}:STAT ON", all_by_one={'m8195a': True})
+ else:
+ self.log.error("Unknown trigger mode: {}".format(mode))
+
+ def get_trigger_mode(self):
+ cont = bool(int(self.query_all_ch(":INIT:CONT{}:STAT?", all_by_one={'m8195a': True})))
+ gate = bool(int(self.query_all_ch(":INIT:GATE{}:STAT?", all_by_one={'m8195a': True})))
+
+ if cont and not gate:
+ return "cont"
+ if not cont and not gate:
+ return "trig"
+ if not cont and gate:
+ return "gate"
+
+ self.log.warning("Unexpected trigger mode found. Cont {}, Gate {}".format(
+ cont, gate))
+ return ""
+
+ def get_dynamic_mode(self):
+ return self.query_all_ch(":STAB{}:DYN?", all_by_one={'m8195a': True})
+
+ def check_dev_error(self):
+
+ has_error_occured = False
+
+ for i in range(30): # error buffer of device is 30
+ raw_str = self.query(':SYST:ERR?', force_no_check=True)
+ is_error = not ('0' in raw_str[0])
+ if is_error:
+ self.log.warn("AWG issued error: {}".format(raw_str))
+ has_error_occured = True
+ else:
+ break
+
+ return has_error_occured
+
+ def _digital_ch_2_internal(self, d_ch_name):
+ if d_ch_name not in self.ch_map:
+ self.log.error("Don't understand digital channel name: {}".format(d_ch_name))
+
+ return self.ch_map[str(d_ch_name)]
+
+ def _digital_ch_corresponding_analogue_ch(self, d_ch_name):
+ int_name = self._digital_ch_2_internal(d_ch_name)
+ if '1' in int_name:
+ return 'a_ch1'
+ elif '2' in int_name:
+ return 'a_ch2'
+ else:
+ raise RuntimeError("Unknown exception. Should only have 1 or 2 in marker name")
+
+ def _analogue_ch_corresponding_digital_chs(self, a_ch_name):
+ # return value must be: [sample marker, sync marker]
+ return self.ch_map_a2d[a_ch_name]
+
+ @abstractmethod
+ def _set_active_ch(self, new_channels_state):
+ pass
+
+ @abstractmethod
+ def float_to_sample(self, val):
+ pass
+
+ def _float_to_int(self, val, n_bits):
+
+ """
+ :param val: np.array(dtype=float64) of sampled values from sequencegenerator.sample_pulse_block_ensemble().
+ normed (-1...1) where 1 encodes the full Vpp as set in 'PulsedGui/Pulsegenerator Settings'.
+ If MW ampl in 'PulsedGui/Predefined methods' < as full Vpp, amplitude reduction will be
+ performed digitally (reducing the effective digital resolution in bits).
+ :param n_bits: number of bits; sets the highest integer allowed. Eg. 8 bits -> int in [-128, 127]
+ :return: np.array(dtype=int16)
+ """
+
+ bitsize = int(2 ** n_bits)
+ min_intval = -bitsize / 2
+ max_intval = bitsize / 2 - 1
+
+ max_u_samples = 1 # data should be normalized in (-1..1)
+
+ if max(abs(val)) > 1:
+ self.log.warning("Samples from sequencegenerator out of range. Normalizing to -1..1. Please change the "
+ "maximum peak to peak Voltage in the Pulse Generator Settings if you want to use a higher "
+ "power.")
+ biggest_val = max([abs(np.min(val)), np.max(val)])
+ max_u_samples = biggest_val
+ # manual 8.22.4 Waveform Data Format in Direct Mode
+ # 2 bits LSB reserved for markers
+ mapper = scipy.interpolate.interp1d([-max_u_samples, max_u_samples], [min_intval, max_intval])
+
+ return mapper(val)
+
+ def bool_to_sample(self, val_dch_1, val_dch_2, int_type_str='int16'):
+ """
+ Takes 2 digital sample values from the sequence generator and converts them to int.
+ For AWG819x always two digital channels are tied with a single analogue output.
+ The resulting int values are used to construct the binary samples.
+ :param vals_dch_1: np.ndarray, digital samples from the sequence generator
+ :param vals_dch_2: np.ndarray, digital samples from the sequence generator
+ :param int_type_str: int type the output is casted to
+ :return:
+ """
+
+ bit_dch_1 = 0x1 & np.asarray(val_dch_1).astype(int_type_str)
+ bit_dch_2 = 0x2 & (np.asarray(val_dch_2).astype(int_type_str) << 1)
+
+ return bit_dch_1 + bit_dch_2
+
+ @abstractmethod
+ def _compile_bin_samples(self, analog_samples, digital_samples, ch_num):
+ """
+ Creates a binary sample output that combines analog and digital samples
+ from the sequence generator in the correct format.
+
+ :return binary samples as expected from awg hardware
+ """
+ pass
+
+
+ def _name_with_ch(self, name, ch_num):
+ """
+ Prepares the final wavename with (M8190A) or without (M8195A) channel extension.
+ Preserves capital letters.
+ Eg. Rabi -> Rabi_ch1
+ """
+ return name + '_ch' + str(ch_num)
+
+ def _wavename_2_fname(self, wave_name):
+ """
+ Preserves capital letters. Note that Windows FS can't keep different files with
+ equal names except for capital / non capital letters.
+ Handled by deleting case insensitive before writing in _write_to_memory().
+ :param wave_name:
+ :return:
+ """
+ return str(wave_name + self._wave_file_extension)
+
+ def _fname_2_wavename(self, fname, incl_ch_postfix=True):
+ # works for file name with (8190a) and without (8195a) _ch? postfix
+ if incl_ch_postfix:
+ return fname.split(".")[0]
+ else:
+ return re.split("(_ch[0-9])", fname)[0]
+
+ def _check_uploaded_wave_name(self, ch_num, wave_name, segment_id):
+
+ wave_name_on_dev = self.get_loaded_asset_name_by_id(ch_num, segment_id)
+ if wave_name_on_dev != wave_name:
+ self.log.warning("Name of waveform altered during upload: {} -> {} Unsupported characters?".format(
+ wave_name, wave_name_on_dev
+ ))
+ return 1
+
+ return 0
+
+ def _write_wave_to_memory(self, name, analog_samples, digital_samples, active_analog, to_segment_id=1):
+ """
+ :param name:
+ :param analog_samples:
+ :param digital_samples:
+ :param active_analog:
+ :param to_segment_id: id of the segment table the wave will be written to. -1: take next free segment.
+ :return:
+ """
+ waveforms = []
+
+ for idx_ch, ch_str in enumerate(active_analog):
+
+ ch_num = self.chstr_2_chnum(ch_str)
+ wave_name = self._name_with_ch(name, ch_num)
+
+ comb_samples = self._compile_bin_samples(analog_samples, digital_samples, ch_str)
+
+ t_start = time.perf_counter()
+
+ if self._wave_mem_mode == 'pc_hdd':
+ # todo: check if working for awg8195a
+ if to_segment_id != 1:
+ self.log.warning("In pc_hdd memory mode, 'to_segment_id' has no effect."
+ "Writing to hdd without setting segment.")
+
+ filename = self._wavename_2_fname(wave_name)
+ waveforms.append(filename)
+
+ if idx_ch == 0:
+ # deletes waveform, all channels
+ self.delete_waveform(self._fname_2_wavename(filename, incl_ch_postfix=False))
+ self.write_bin(':MMEM:DATA "{0}", '.format(filename), comb_samples)
+
+ self.log.debug("Waveform {} written to {}".format(wave_name, filename))
+
+ elif self._wave_mem_mode == 'awg_segments':
+
+ if wave_name in self.get_loaded_assets_name(ch_num):
+ seg_id_exist = self.asset_name_2_id(wave_name, ch_num, mode='segment')
+ self.write("TRAC{:d}:DEL {}".format(ch_num, seg_id_exist))
+ self.log.debug("Deleting segment {} ch {} for existing wave {}".format(seg_id_exist, ch_num, wave_name))
+
+ segment_id = to_segment_id
+ if name.split(',')[0] != name:
+ # todo: this breaks if there is a , in the name without number
+ segment_id = np.int(name.split(',')[0])
+ self.log.warning("Loading wave to specified segment ({}) via name will deprecate.".format(segment_id))
+ if segment_id == -1:
+ # to next free segment
+ segment_id = self.query('TRAC{0:d}:DEF:NEW? {1:d}'.format(ch_num, len(analog_samples[ch_str])))
+ # only need the next free id, definition and writing is performed below again
+ # so delete defined segment again
+ self.write("TRAC{:d}:DEL {}".format(ch_num, segment_id))
+
+ segment_id_ch = str(segment_id) + '_ch{:d}'.format(ch_num)
+ self.log.debug("Writing wave {} to ch {} segment_id {}".format(wave_name, ch_str, segment_id_ch))
+
+ # delete if the segment is already existing
+ loaded_segments_id = self.get_loaded_assets_id(ch_num)
+ if str(segment_id) in loaded_segments_id:
+ # clear the segment
+ self.write(':TRAC:DEL {0}'.format(segment_id))
+
+ # define the size of a waveform segment, marker samples do not count. If the channel is sourced from
+ # Extended Memory, the same segment is defined on all other channels sourced from Extended Memory.
+ # Comb samples written, but len(comb_samples) doesn't know whether interleaved data.
+ self.write(':TRAC{0}:DEF {1}, {2}, {3}'.format(int(ch_num), segment_id, len(analog_samples[ch_str]), 0))
+
+ # name the segment
+ self.write(':TRAC{0}:NAME {1}, "{2}"'.format(int(ch_num), segment_id, wave_name)) # name the segment
+ # upload
+ self.write_bin(':TRAC{0}:DATA {1}, {2},'.format(int(ch_num), segment_id, 0), comb_samples)
+
+ self._check_uploaded_wave_name(ch_num, wave_name, segment_id)
+
+ waveforms.append(wave_name)
+ self._flag_segment_table_req_update = True
+
+ else:
+ raise ValueError("Unknown memory mode: {}".format(self._wave_mem_mode))
+ try:
+ transfer_speed_mbs = (comb_samples.nbytes/(1024*1024))/(time.perf_counter() - t_start)
+ self.log.debug('Written ({2:.1f} MB/s) to ch={0}: max ampl: {1}'.format(ch_str,
+ analog_samples[ch_str].max(),
+ transfer_speed_mbs))
+ except ZeroDivisionError:
+ pass
+
+ return waveforms
+
+ def has_sequence_mode(self):
+ """ Asks the pulse generator whether sequence mode exists.
+
+ @return: bool, True for yes, False for no.
+ """
+ return self._sequence_mode
+
+ 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)
+ """
+ bytes_written, enum_status_code = self.awg.write(command)
+
+ if self._debug_check_all_commands:
+ if 0 != self.check_dev_error():
+ self.log.warn("Check failed after command: {}".format(command))
+
+ return int(enum_status_code)
+
+ def write_bin(self, command, values):
+ """ Sends a command string to the device.
+
+ @param string command: string containing the command
+
+ @return int: error code (0:OK, -1:error)
+ """
+ self.awg.timeout = None
+ bytes_written, enum_status_code = self.awg.write_binary_values(command, datatype=self._wave_transfer_datatype, is_big_endian=False,
+ values=values)
+ self.awg.timeout = self._awg_timeout * 1000
+ return int(enum_status_code)
+
+ def write_all_ch(self, command, *args, all_by_one=None):
+ """
+ :param command: visa command
+ :param all_by_one: dict, eg. {"m8190a": False, "m8195a": True}. Set true when for
+ the specific device one command, not separate with ch_nums is required.
+ Eg. "TRAC:SEL" instead for "TRAC1:SEL" and "TRAC2:SEL"
+ If device not listed, will default to False.
+ :param args: replacement list which is filled into command
+ :return:
+ """
+
+ if all_by_one is None:
+ all_by_one = {'m8190a': False, 'm8195a': False}
+
+ single_cmd = False
+ if self._MODEL.lower() in all_by_one:
+ single_cmd = bool(all_by_one[self._MODEL.lower()])
+
+ if single_cmd:
+ # replace first braces that, usually to indicate channel
+ command = command.replace("{","", 1)
+ command = command.replace("}", "", 1)
+ self.write(command.format(*args))
+ else:
+ for a_ch in self._get_all_analog_channels():
+ ch_num = self.chstr_2_chnum(a_ch)
+ self.write(command.format(ch_num, *args))
+
+ def query_all_ch(self, command, *args, all_by_one=None):
+ """
+ :param command: visa command
+ :param all_by_one: dict, eg. {"m8190a": False, "m8195a": True}. Set true when for
+ the specific device one command, not separate with ch_nums is required.
+ Eg. "TRAC:SEL" instead for "TRAC1:SEL" and "TRAC2:SEL"
+ If device not listed, will default to False.
+ :param args: replacement list which is filled into command
+ :return: response of all channels collapsed to single value if all channels equal
+ error and response of first channel if otherwise
+ """
+
+ if all_by_one is None:
+ all_by_one = {'m8190a': False, 'm8195a': False}
+
+ single_cmd = False
+
+ if self._MODEL.lower() in all_by_one:
+ single_cmd = bool(all_by_one[self._MODEL.lower()])
+
+ if single_cmd:
+ # replace first braces that, usually to indicate channel
+ command = command.replace("{", "", 1)
+ command = command.replace("}", "", 1)
+
+ return self.query(command.format(*args))
+ else:
+ retlist = []
+ for a_ch in self._get_all_analog_channels():
+ ch_num = self.chstr_2_chnum(a_ch)
+ retlist = self.query(command.format(ch_num, *args))
+ collapsed_ret = np.unique(np.asarray(retlist))
+ if collapsed_ret.size > 1:
+ self.log.error("Unexpected non-identical response on channels: {}".format(retlist))
+
+ return collapsed_ret[0]
+
+ def query(self, question, force_no_check=False):
+ """ 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
+ """
+ ret = self.awg.query(question).strip().strip('"')
+ if self._debug_check_all_commands and not force_no_check:
+ if 0 != self.check_dev_error():
+ self.log.warn("Check failed after query: {}".format(question))
+
+ return ret
+
+ def query_bin(self, question):
+
+ return self.awg.query_binary_values(question, datatype=self._wave_transfer_datatype, is_big_endian=False)
+
+ def _is_awg_running(self):
+ """
+ Aks the AWG if the AWG is running
+ @return: bool, (True: running, False: stoped)
+ """
+ # 0 No Output is running
+ # 1 CH01 is running
+ # 2 CH02 is running
+ # 4 CH03 is running
+ # 8 CH04 is running
+ # run_state is sum of these
+
+ run_state = self.query(':STAT:OPER:RUN:COND?')
+
+ if int(run_state) == 0:
+ return False
+ else:
+ return True
+
+ def _is_output_on(self):
+ """
+ Asks the AWG if the outputs are on
+ @return: bool, (True: Outputs are on, False: Outputs are switched off)
+ """
+
+ state = 0
+
+ state += int(self.query(':OUTP1?'))
+ state += int(self.query(':OUTP2?'))
+
+ if int(state) == 0:
+ return False
+ else:
+ return True
+
+ def _get_all_channels(self):
+ """
+ Helper method to return a sorted list of all technically available channel descriptors
+ (e.g. ['a_ch1', 'a_ch2', 'd_ch1', 'd_ch2'])
+
+ @return list: Sorted list of channels
+ """
+ configs = self.get_constraints().activation_config
+ if 'all' in configs:
+ largest_config = configs['all']
+ else:
+ largest_config = list(configs.values())[0]
+ for config in configs.values():
+ if len(largest_config) < len(config):
+ largest_config = config
+ return sorted(largest_config)
+
+ def _get_all_analog_channels(self):
+ """
+ Helper method to return a sorted list of all technically available analog channel
+ descriptors (e.g. ['a_ch1', 'a_ch2'])
+
+ @return list: Sorted list of analog channels
+ """
+ return [chnl for chnl in self._get_all_channels() if chnl.startswith('a')]
+
+ def _get_active_d_or_a_channels(self, only_analog=False, only_digital=False):
+ """
+ Helper method to quickly get only digital or analog active channels.
+ :return: list: Sorted list of selected a/d channels
+ """
+
+ activation_dict = self.get_active_channels()
+ active_channels = {chnl for chnl in activation_dict if activation_dict[chnl]}
+
+ active_ch = sorted(chnl for chnl in active_channels)
+ active_analog = sorted(chnl for chnl in active_channels if chnl.startswith('a'))
+ active_digital = sorted(chnl for chnl in active_channels if chnl.startswith('d'))
+
+ if only_analog:
+ active_ch = active_analog
+ if only_digital:
+ active_ch = active_digital
+ if only_analog and only_digital:
+ active_ch = []
+
+ return active_ch
+
+ def _get_all_digital_channels(self):
+ """
+ Helper method to return a sorted list of all technically available digital channel
+ descriptors (e.g. ['d_ch1', 'd_ch2'])
+
+ @return list: Sorted list of digital channels
+ """
+ return [chnl for chnl in self._get_all_channels() if chnl.startswith('d')]
+
+ def _output_levels_by_config(self, d_ampl_low, d_ampl_high):
+ if self._d_ch_level_low_high:
+ for i in range(len(self._d_ch_level_low_high)):
+ ch_idx = i + 1
+ low = self._d_ch_level_low_high[i][0]
+ high = self._d_ch_level_low_high[i][1]
+ ch_str = 'd_ch{:d}'.format(ch_idx)
+
+ if ch_str in d_ampl_low.keys() and ch_str in d_ampl_high.keys():
+ d_ampl_low['d_ch{:d}'.format(ch_idx)] = low
+ d_ampl_high['d_ch{:d}'.format(ch_idx)] = high
+ else:
+ pass # use passed (default) values
+
+ self.log.debug("Overriding output levels from config: d_ampl_low: {}, d_ampl_high: {}".format(
+ d_ampl_low, d_ampl_high))
+
+ return d_ampl_low, d_ampl_high
+
+ def update_segment_table(self):
+ segment_table_1 = self.read_segment_table(1)
+ segment_table_2 = self.read_segment_table(2)
+ self._segment_table[0] = segment_table_1
+ self._segment_table[1] = segment_table_2
+
+ self._flag_segment_table_req_update = False
+
+ return segment_table_1, segment_table_2
+
+ def get_segment_table(self, ch_num, force_from_local_copy=-1):
+
+ if force_from_local_copy < 0:
+ if self._flag_segment_table_req_update:
+ self.log.debug("Local segment table seems out-of-date. Fetching.")
+ segment_table_1, segment_table_2 = self.update_segment_table()
+ else:
+ segment_table_1, segment_table_2 = self._segment_table[0], self._segment_table[1]
+
+ else:
+ if force_from_local_copy == 1:
+ if self._flag_segment_table_req_update:
+ self.log.warning("Forcing out-of-date local segment table that needs update")
+ segment_table_1, segment_table_2 = self._segment_table[0], self._segment_table[1]
+
+ elif force_from_local_copy == 0:
+ segment_table_1, segment_table_2 = self.update_segment_table()
+ else:
+ raise ValueError("Unexpected input for force_from_local_copy: {}".format(force_from_local_copy))
+
+ if ch_num == 1:
+ return segment_table_1
+ elif ch_num == 2:
+ return segment_table_2
+ else:
+ raise ValueError("Unexpected channel number: {}".format(ch_num))
+
+ def read_segment_table(self, ch_num):
+
+ self.log.debug("Reading device segment table for ch {}".format(ch_num))
+
+ names = self.get_loaded_assets_name(ch_num, mode='segment')
+ ids = self.get_loaded_assets_id(ch_num, mode='segment')
+
+ zipped = zip(ids, names)
+ segment_table = []
+
+ if len(names) != len(ids):
+ self.log.error("Segment table on device seems unaligned.")
+ return segment_table
+
+ segment_table = [[x,y] for x,y in sorted(zipped)]
+
+ return segment_table
+
+ def get_segment_name(self, seg_id, ch_num):
+ # awg 8190a has 2 separate sequencer per channel!
+
+ segment_table = self.get_segment_table(ch_num)
+
+ try:
+ idx_id = [row[0] for row in segment_table].index(seg_id)
+ name = [row[1] for row in segment_table][idx_id]
+
+ except ValueError:
+ self.log.warning("Couldn't find segment id {} in ch {}".format(seg_id, ch_num))
+ return ''
+ return name
+
+ def get_segment_id(self, segment_waveform_name, ch_num):
+ """
+ Finds id of a given waveform name.
+ :param segment_waveform_name: waveform name without (eg. .bin) extension
+ :param ch_num: analog awg channel
+ :return: -1 if not found
+ """
+
+ segment_table = self.get_segment_table(ch_num)
+
+ try:
+ # np.array would allow slicing, but list comprehension probably better performance
+ idx_id = [row[1] for row in segment_table].index(segment_waveform_name)
+ id = [row[0] for row in segment_table][idx_id]
+
+ except ValueError:
+ self.log.warning("Couldn't find waveform {} in ch {}".format(segment_waveform_name, ch_num))
+ return -1
+ return id
+
+ @abstractmethod
+ def _get_loaded_seq_catalogue(self, ch_num):
+ pass
+
+ @abstractmethod
+ def _get_loaded_seq_name(self, ch_num, idx):
+ pass
+
+ def get_loaded_assets_num(self, ch_num, mode='segment'):
+ """
+ Retrieves the total number of assets uploaded to the awg memory.
+ This is not == "loaded_asset" which is the waveform / segment marked active.
+ """
+ if mode == 'segment':
+ raw_str = self.query(':TRAC{:d}:CAT?'.format(ch_num))
+ elif mode == 'sequence':
+ raw_str = self._get_loaded_seq_catalogue(ch_num)
+ else:
+ self.log.error("Unknown assets mode: {}".format(mode))
+ return 0
+
+ if raw_str.replace(" ","") == "0,0": # awg response on 8195A without spaces
+ return 0
+ else:
+ splitted = raw_str.rsplit(',')
+
+ return int(len(splitted)/2)
+
+ def get_loaded_assets_id(self, ch_num, mode='segment'):
+
+ if mode == 'segment':
+ raw_str = self.query(':TRAC{:d}:CAT?'.format(ch_num))
+ elif mode == 'sequence':
+ raw_str = self._get_loaded_seq_catalogue(ch_num)
+ else:
+ self.log.error("Unknown assets mode: {}".format(mode))
+ return []
+ n_assets = self.get_loaded_assets_num(ch_num, mode)
+
+ if n_assets == 0:
+ return []
+ else:
+ splitted = raw_str.rsplit(',')
+ ids = [int(x) for x in splitted[0::2]]
+
+ return ids
+
+ def get_loaded_assets_name(self, ch_num, mode='segment'):
+ """
+ Retrieves the names of all assets uploaded to the awg memory.
+ This is not == "loaded_asset" which is the waveform / segment marked active.
+ """
+
+ asset_ids = self.get_loaded_assets_id(ch_num, mode)
+ names = []
+ for i in asset_ids:
+
+ if mode == 'segment':
+ names.append(self.query(':TRAC{:d}:NAME? {:d}'.format(ch_num, i)))
+ elif mode == 'sequence':
+ names.append(self._get_loaded_seq_name(ch_num, i))
+ else:
+ self.log.error("Unknown assets mode: {}".format(mode))
+ return 0
+
+ return names
+
+ def get_loaded_asset_name_by_id(self, ch_num, id, mode='segment'):
+ asset_names = self.get_loaded_assets_name(ch_num, mode)
+ asset_ids = self.get_loaded_assets_id(ch_num, mode)
+
+ try:
+ idx = asset_ids.index(int(id))
+ except ValueError:
+ self.log.warning("Couldn't find {} id {} in loaded assetes".format(mode, id))
+ return ""
+
+ return asset_names[idx]
+
+ def asset_name_2_id(self, name, ch_num, mode='segment'):
+ names = self.get_loaded_assets_name(ch_num, mode)
+ idx = names.index(name)
+
+ return self.get_loaded_assets_id(ch_num, mode)[idx]
+
+ def get_sequencer_state(self, ch_num):
+ """
+ Queries the state of the sequencer.
+ :param ch_num: 1 or 2
+ :return: state, sequence table id
+ state:
+ 0: idle
+ 1: waiting for trigger
+ 2: running
+ 3: waiting for advancement event
+ """
+
+ awg_mode = self.query("FUNC{:d}:MODE?".format(ch_num))
+ if awg_mode == 'ARB':
+ self.log.warning("Sequencer state is undefined in arb mode")
+ return 0, 0
+
+ bin_str = "{:b}".format(int(self.query("STAB{:d}:SEQ:STAT?".format(ch_num))))
+ state = int(bin_str[0:2], 2)
+ if state != 0:
+ seq_table_id = int(bin_str[2:], 2)
+ else:
+ seq_table_id = 0
+
+ return state, seq_table_id
+
+ def set_trig_polarity(self, pol='pos'):
+ if pol is "pos":
+ self.write(":ARM:TRIG:SLOP POS")
+ elif pol is "neg":
+ self.write(":ARM:TRIG:SLOP NEG")
+ elif pol is "both":
+ self.write(":ARM:TRIG:SLOP EITH")
+
+ else:
+ self.log.error("Unknown trigger polarity: {}".format(pol))
+
+ def _remove_file_extension(self, filename):
+ """
+ Removes filename, even if dot in filename.
+ eg. rabi.1.bin -> rabi.1
+ rabi.1 -> rabi
+ rabi -> rabi
+ :param filename:
+ :return:
+ """
+ return filename.rsplit('.', 1)[0]
+
+ def _create_dir(self, path):
+ if not os.path.exists(path):
+ try:
+ os.mkdir(path)
+ self.log.info("Folder was missing, so created: {}".format(path))
+ except Exception as e:
+ self.log.warning("Couldn't create folder: {}. {}".format(path, str(e)))
+
+ def sequence_set_start_segment(self, seqtable_id):
+ # todo: need to implement? alernatively shuffle sequuence while generating
+ raise NotImplementedError
+
+ def chstr_2_chnum(self, chstr, return_list=False):
+ """
+ Converts a channel name like 'a_ch1' to channel number internally used to address
+ this channel in VISA commands. Eg. 'd_ch1' -> 3 on M8195A.
+ :param chstr: list of str or str
+ :return: list of int or int
+ """
+
+ def single_str_2_num(chstr):
+ if 'a_ch' in chstr:
+ ch_num = int(chstr.rsplit('_ch', 1)[1])
+ elif 'd_ch' in chstr:
+ # this is M8195A specific
+ ch_num = int(chstr.rsplit('_ch', 1)[1]) + 2
+ if self._MODEL == 'M8190A':
+ ch_num = None
+ self.log.warning("Returning None from channel string {}. Belongs to analog ch for 8190A".format(chstr))
+ else:
+ raise ValueError("Unknown channel string: {}".format(chstr))
+ return ch_num
+
+ if isinstance(chstr, str):
+ chstr = [chstr]
+
+ num_list = [single_str_2_num(s) for s in chstr]
+
+ if len(num_list) == 1 and not return_list:
+ return num_list[0]
+
+ return num_list
+
+
+class AWGM8195A(AWGM819X):
+ """ A hardware module for the Keysight M8195A series for generating
+ waveforms and sequences thereof.
+
+ Example config for copy-paste:
+
+ awg8195:
+ module.Class: 'awg.keysight_M819x.AWGM8195A'
+ awg_visa_address: 'TCPIP0::localhost::hislip0::INSTR'
+ awg_timeout: 20
+ pulsed_file_dir: 'C:/Software/pulsed_files' # asset directories should be equal
+ assets_storage_path: 'C:/Software/saved_pulsed_assets' # to the ones in sequencegeneratorlogic
+ sample_rate_div: 1
+ awg_mode: 'MARK'
+ """
+
+ awg_mode_cfg = ConfigOption(name='awg_mode', default='MARK', missing='warn')
+
+ _wave_mem_mode = ConfigOption(name='waveform_memory_mode', default='awg_segments', missing='nothing')
+ _wave_file_extension = '.bin8'
+ _wave_transfer_datatype = 'b'
+
+ _dac_resolution = 8 # fixed 8 bit
+ # physical output channel mapping
+ ch_map = {'d_ch1': 3, 'd_ch2': 4} # awg8195a: digital channels are analogue channels, only different config
+
+ def __init__(self, config, **kwargs):
+ super().__init__(config=config, **kwargs)
+
+ self._sequence_names = [] # awg8195a can only store a single sequence
+
+ if self._wave_mem_mode == 'pc_hdd':
+ self.log.warning("wave_mem_mode pc_hdd is experimental on m8195a")
+
+ @property
+ def n_ch(self):
+ return 4
+
+ @property
+ def awg_mode(self):
+ return self.query(':INST:DACM?')
+
+ @property
+ def marker_on(self):
+ if self.awg_mode == 'MARK' or self.awg_mode == 'DCM':
+ return True
+ return False
+
+ @property
+ def interleaved_wavefile(self):
+ """
+ Whether wavefroms need to be uploaded in a interleaved intermediate format.
+ Not to confuse with interleave mode from get_interleave().
+ :return: True/False: need interleaved wavefile?
+ """
+ return self.marker_on
+
+ 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:
+ {: ,
+ : ,
+ ...}
+
+ 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()
+
+ # The compatible file formats are hardware specific.
+ constraints.waveform_format = ['bin8']
+ constraints.dac_resolution = {'min': 8, 'max': 8, 'step': 1, 'unit': 'bit'}
+
+ if self._MODEL == 'M8195A':
+ constraints.sample_rate.min = 53.76e9 / self._sample_rate_div
+ constraints.sample_rate.max = 65.0e9 / self._sample_rate_div
+ constraints.sample_rate.step = 1.0e7
+ constraints.sample_rate.default = 65.00e9 / self._sample_rate_div
+ else:
+ self.log.error('The current AWG model has no valid sample rate '
+ 'constraints')
+
+ # manual 1.5.4: Depending on the Sample Rate Divider, the 256 sample wide output of the sequencer
+ # is divided by 1, 2 or 4.
+ constraints.waveform_length.step = 256 / self._sample_rate_div
+ constraints.waveform_length.min = 1280 # != p 108 manual, but tested manually ('MARK')
+ constraints.waveform_length.max = int(16e9)
+ constraints.waveform_length.default = 1280
+
+ # analog channel
+ constraints.a_ch_amplitude.min = 0.075 # from soft frontpanel
+ constraints.a_ch_amplitude.max = 2
+ constraints.a_ch_amplitude.step = 0.0002 # not used anymore
+ constraints.a_ch_amplitude.default = 1
+ constraints.a_ch_amplitude.default_marker = 1
+
+ # digital channel
+ constraints.d_ch_low.min = 0
+ constraints.d_ch_low.max = 1
+ constraints.d_ch_low.step = 0.0002
+ constraints.d_ch_low.default = 0.0
+
+ constraints.d_ch_high.min = 0
+ constraints.d_ch_high.max = 2
+ constraints.d_ch_high.step = 0.0002
+ constraints.d_ch_high.default = 1
+
+ # offset
+ constraints.a_ch_offset.max = 1
+ constraints.a_ch_offset.min = 0
+ constraints.a_ch_offset.default = 0
+ constraints.a_ch_offset.default_marker = 0.5 # default value if analog channel is used as marker
+
+ # constraints.sampled_file_length.min = 256
+ # constraints.sampled_file_length.max = 2_000_000_000
+ # constraints.sampled_file_length.step = 256
+ # constraints.sampled_file_length.default = 256
+
+ constraints.waveform_num.min = 1
+ constraints.waveform_num.max = 16777215
+ constraints.waveform_num.default = 1
+
+ constraints.sequence_num.min = 1
+ constraints.sequence_num.max = 16777215
+ constraints.sequence_num.step = 1
+ constraints.sequence_num.default = 1
+
+ # If sequencer mode is available then these should be specified
+ constraints.repetitions.min = 0
+ constraints.repetitions.max = 65536
+ constraints.repetitions.step = 1
+ constraints.repetitions.default = 0
+
+ # the name a_ch and d_ch 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()
+ if self._MODEL == 'M8195A':
+ awg_mode = self.awg_mode
+ if awg_mode == 'MARK':
+ activation_config['all'] = frozenset({'a_ch1', 'd_ch1', 'd_ch2'})
+ elif awg_mode == 'SING':
+ activation_config['all'] = frozenset({'a_ch1'})
+ elif awg_mode == 'DUAL':
+ activation_config['all'] = frozenset({'a_ch1', 'a_ch2'})
+ elif awg_mode == 'FOUR':
+ activation_config['all'] = frozenset({'a_ch1', 'a_ch2', 'a_ch3', 'a_ch4'})
+
+ constraints.activation_config = activation_config
+
+ return constraints
+
+ def _get_init_output_levels(self):
+
+ constr = self.get_constraints()
+
+ if self.awg_mode == 'MARK':
+
+ a_ampl = {'a_ch1': constr.a_ch_amplitude.default} # peak to peak voltage
+ d_ampl_low = {'d_ch1': constr.d_ch_low.default, 'd_ch2': constr.d_ch_low.default}
+ d_ampl_high = {'d_ch1': constr.d_ch_high.default, 'd_ch2': constr.d_ch_high.default}
+ a_offs = {}
+
+ elif self.awg_mode == 'FOUR':
+
+ a_ampl = {'a_ch1': constr.a_ch_amplitude.default, 'a_ch2': constr.a_ch_amplitude.default,
+ 'a_ch3': constr.a_ch_amplitude.default, 'a_ch4': constr.a_ch_amplitude.default}
+ a_offs = {'a_ch1': constr.a_ch_offset.default, 'a_ch2': constr.a_ch_offset.default_marker,
+ 'a_ch3': constr.a_ch_offset.default_marker, 'a_ch4': constr.a_ch_offset.default_marker}
+ d_ampl_low = {}
+ d_ampl_high = {}
+
+ else:
+ self.log.error('The chosen AWG ({0}) mode is not implemented yet!'.format(self.awg_mode))
+
+ d_ampl_low, d_ampl_high = self._output_levels_by_config(d_ampl_low, d_ampl_high)
+
+ return {'a_ampl': a_ampl, 'a_offs': a_offs,
+ 'd_ampl_low': d_ampl_low, 'd_ampl_high': d_ampl_high}
+
+ def _set_awg_mode(self):
+ # set only on init by config option, not during runtime
+
+ awg_mode = self.awg_mode_cfg
+ self.write(':INSTrument:DACMode {0}'.format(awg_mode))
+
+ # see manual 1.5.5
+ if awg_mode == 'MARK' or awg_mode == 'SING' or awg_mode == 'DUAL' or awg_mode == 'FOUR':
+ self.write_all_ch(':TRAC{}:MMOD EXT')
+ else:
+ raise ValueError("Unknown mode: {}".format(awg_mode))
+
+ if awg_mode != 'MARK':
+ self.log.error("Setting awg mode {} that is currently not supported! "
+ "Be careful and please report bugs and bug fixes back on github.".format(awg_mode))
+
+ if self.awg_mode != awg_mode:
+ self.log.error("Setting awg mode failed, is still: {}".format(self.awg_mode))
+
+ def _set_sample_rate_div(self):
+ self.write(':INST:MEM:EXT:RDIV DIV{0}'.format(self._sample_rate_div)) # TODO dependent on DACMode
+
+ def _write_output_on(self):
+ self.write_all_ch("OUTP{} ON")
+
+ def _compile_bin_samples(self, analog_samples, digital_samples, ch_str):
+
+ interleaved = self.interleaved_wavefile
+ self.log.debug("Compiling samples for {}, interleaved: {}".format(ch_str, interleaved))
+
+ a_samples = self.float_to_sample(analog_samples[ch_str])
+
+ if interleaved and ch_str == 'a_ch1':
+ d_samples = self.bool_to_sample(digital_samples['d_ch1'], digital_samples['d_ch2'],
+ int_type_str='int8')
+ # the analog and digital samples are stored in the following format: a1, d1, a2, d2, a3, d3, ...
+ comb_samples = np.zeros(2 * a_samples.size, dtype=np.int8)
+ comb_samples[::2] = a_samples
+ comb_samples[1::2] = d_samples
+
+ else:
+ comb_samples = a_samples
+
+ return comb_samples
+
+ def _get_digital_ch_cmd(self, digital_ch_name):
+ d_ch_internal = self._digital_ch_2_internal(digital_ch_name)
+ return ':VOLT{0:d}'.format(d_ch_internal)
+
+ 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 = []
+
+ active_ch = dict()
+
+ if ch ==[]:
+
+ # because 0 = False and 1 = True
+ awg_mode = self.awg_mode
+
+ if awg_mode == 'MARK':
+ active_ch['a_ch1'] = bool(int(self.query(':OUTP1?')))
+ active_ch['d_ch1'] = bool(int(self.query(':OUTP3?')))
+ active_ch['d_ch2'] = bool(int(self.query(':OUTP4?')))
+ elif awg_mode == 'SING':
+ active_ch['a_ch1'] = bool(int(self.query(':OUTP1?')))
+ elif awg_mode == 'DUAL':
+ active_ch['a_ch1'] = bool(int(self.query(':OUTP1?')))
+ active_ch['a_ch4'] = bool(int(self.query(':OUTP4?')))
+ elif awg_mode == 'FOUR':
+ active_ch['a_ch1'] = bool(int(self.query(':OUTP1?')))
+ active_ch['a_ch2'] = bool(int(self.query(':OUTP2?')))
+ active_ch['a_ch3'] = bool(int(self.query(':OUTP3?')))
+ active_ch['a_ch4'] = bool(int(self.query(':OUTP4?')))
+
+ else:
+
+ for channel in ch:
+ if 'a_ch' in channel:
+ ana_chan = int(channel[4:])
+ active_ch[channel] = bool(int(self.ask(':OUTP{0}?'.format(ana_chan))))
+
+ elif 'd_ch'in channel:
+ self.log.warning('Digital channel "{0}" cannot be '
+ 'activated! Command ignored.'
+ ''.format(channel))
+ active_ch[channel] = False
+
+ return active_ch
+
+ def _set_active_ch(self, new_channels_state):
+ # get lists of all analog channels
+ analog_channels = self._get_all_analog_channels()
+ digital_channels = self._get_all_digital_channels()
+
+ # Also (de)activate the channels accordingly
+ for a_ch in analog_channels:
+ ach_num = self.chstr_2_chnum(a_ch)
+ # (de)activate the analog channel
+ if new_channels_state[a_ch]:
+ self.write('OUTP{0:d} ON'.format(ach_num))
+ else:
+ self.write('OUTP{0:d} OFF'.format(ach_num))
+
+ for d_ch in digital_channels:
+ dch_num = self.chstr_2_chnum(d_ch)
+ # (de)activate the digital channel
+ if new_channels_state[d_ch]:
+ self.write('OUTP{0:d} ON'.format(dch_num))
+ else:
+ self.write('OUTP{0:d} OFF'.format(dch_num))
+
+ def float_to_sample(self, val):
+
+ val_int = self._float_to_int(val, self._dac_resolution)
+
+ return val_int.astype('int8')
+
+ def _define_new_sequence(self, name, num_steps):
+ # no storage system for sequences on 8195a
+ self._sequence_names = [name]
+
+ def _delete_all_sequences(self):
+ # no storage system for sequences on 8195a
+ self._sequence_names = []
+
+ def _get_loaded_seq_catalogue(self, ch_num):
+
+ if not self._sequence_names:
+ return '0, 0' # signals no sequence loaded
+
+ # mimic awg8190 format 'sequence_id, length in segments'
+ # we don't need the length here, so dummy value
+ return '0, -1'
+
+ def _get_loaded_seq_name(self, ch_num, idx):
+
+ if idx > 0:
+ self.log.warn("AWG8195A does not support loading of multiple sequences")
+
+ return self._sequence_names[0]
+
+ def _get_sequence_control_bin(self, sequence_parameters, idx_step):
+
+ index = idx_step
+ num_steps = len(sequence_parameters)
+
+ if self.awg_mode == 'MARK':
+ control = 2 ** 24 # set marker
+ elif self.awg_mode == 'FOUR':
+ control = 0
+ else:
+ self.log.error("The AWG mode '{0}' is not implemented yet!".format(self.awg_mode))
+ return
+
+ if index == 0:
+ control += 2 ** 28 # set start sequence
+ if index + 1 == num_steps:
+ control += 2 ** 30 # set end sequence
+
+ return control
+
+ def _name_with_ch(self, name, ch_num):
+ """
+ M8195A has only one wave file for all channels, no channel extension needed.
+ """
+ return name
+
+
+class AWGM8190A(AWGM819X):
+ """ A hardware module for the Keysight M8190A series for generating
+ waveforms and sequences thereof.
+
+ Example config for copy-paste:
+
+ awg8190:
+ module.Class: 'awg.keysight_M819x.AWGM8190A'
+ awg_visa_address: 'TCPIP0::localhost::hislip0::INSTR'
+ awg_timeout: 20
+ pulsed_file_dir: 'C:/Software/pulsed_files' # asset directories should be equal
+ assets_storage_path: 'C:/Software/aved_pulsed_assets' # to the ones in sequencegeneratorlogic
+ sample_rate_div: 1
+ dac_resolution_bits: 14
+ """
+
+ _dac_amp_mode = 'direct' # see manual 1.2 'options'
+ _wave_mem_mode = ConfigOption(name='waveform_memory_mode', default='pc_hdd', missing='nothing')
+ _wave_file_extension = '.bin'
+ _wave_transfer_datatype = 'h'
+
+ _dac_resolution = ConfigOption(name='dac_resolution_bits', default='14',
+ missing='warn') # 8190 supports 12 (speed) or 14 (precision)
+
+ # physical output channel mapping
+ ch_map = {'d_ch1': 'MARK1:SAMP', 'd_ch2': 'MARK2:SAMP', 'd_ch3': 'MARK1:SYNC', 'd_ch4': 'MARK2:SYNC'}
+ ch_map_a2d = {'a_ch1': ['d_ch1', 'd_ch3'], 'a_ch2': ['d_ch2', 'd_ch4']} # corresponding marker channels
+
+ @property
+ def n_ch(self):
+ return 2
+
+ @property
+ def marker_on(self):
+ # no reason to deactivate any marker for M8190A, as active makers do not impose restrcitions
+ return True
+
+ @property
+ def interleaved_wavefile(self):
+ return 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:
+ {: ,
+ : ,
+ ...}
+
+ 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()
+
+ # The compatible file formats are hardware specific.
+ constraints.waveform_format = ['bin']
+ constraints.dac_resolution = {'min': 12, 'max': 14, 'step': 2,
+ 'unit': 'bit'}
+
+ if self._MODEL != 'M8190A':
+ self.log.error('This driver is for Keysight M8190A only, but detected: {}'.format(
+ self._MODEL
+ ))
+
+ if self._dac_resolution == 12:
+ constraints.sample_rate.min = 125e6 / self._sample_rate_div
+ constraints.sample_rate.max = 12e9 / self._sample_rate_div
+ constraints.sample_rate.step = 1.0e7
+ constraints.sample_rate.default = 12e9 / self._sample_rate_div
+ elif self._dac_resolution == 14:
+ constraints.sample_rate.min = 125e6 / self._sample_rate_div
+ constraints.sample_rate.max = 8e9 / self._sample_rate_div
+ constraints.sample_rate.step = 1.0e7
+ constraints.sample_rate.default = 8e9 / self._sample_rate_div
+ else:
+ raise ValueError("Unsupported DAC resolution: {}".format(self._dac_resolution))
+
+ # manual 8.22.3 Waveform Granularity and Size
+ if self._dac_resolution == 12:
+ constraints.waveform_length.step = 64
+ constraints.waveform_length.min = 320
+ constraints.waveform_length.default = 320
+ elif self._dac_resolution == 14:
+ constraints.waveform_length.step = 48
+ constraints.waveform_length.min = 240
+ constraints.waveform_length.default = 240
+
+ constraints.waveform_length.max = 2147483648 # assumes option -02G
+
+ constraints.a_ch_amplitude.min = 0.1 # from soft frontpanel, single ended min
+ constraints.a_ch_amplitude.max = 0.700 # single ended max
+ if self._dac_resolution == 12:
+ # 0.7Vpp/2^12=0.0019; for DAC resolution of 12 bits (data sheet p. 17)
+ constraints.a_ch_amplitude.step = 1.7090e-4
+ elif self._dac_resolution == 14:
+ constraints.a_ch_amplitude.step = 4.2725e-5
+ constraints.a_ch_amplitude.default = 0.500
+
+ constraints.d_ch_low.min = -0.5
+ constraints.d_ch_low.max = 1.75
+ constraints.d_ch_low.step = 0.0002
+ constraints.d_ch_low.default = 0.0
+
+ constraints.d_ch_high.min = 0.5 # manual p. 245
+ constraints.d_ch_high.max = 1.75
+ constraints.d_ch_high.step = 0.0002
+ constraints.d_ch_high.default = 1.5
+
+ constraints.waveform_num.min = 1
+ constraints.waveform_num.max = 524287 # manual p. 261
+ constraints.waveform_num.default = 1
+
+ constraints.sequence_num.min = 1
+ constraints.sequence_num.max = 524288 - 1 # manual p. 251
+ constraints.sequence_num.step = 1
+ constraints.sequence_num.default = 1
+
+ constraints.sequence_option = SequenceOption.OPTIONAL
+ constraints.sequence_order = "LINONLY" # SequenceOrderOption.LINONLY
+
+ # If sequencer mode is available then these should be specified
+ constraints.repetitions.min = 0
+ constraints.repetitions.max = 65536
+ constraints.repetitions.step = 1
+ constraints.repetitions.default = 0
+
+ # the name a_ch and d_ch 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()
+
+ if self._MODEL == 'M8190A':
+ # all allowed configs
+ # digital channels belong to analogue counterparts
+ activation_config['all'] = {'a_ch1', 'a_ch2',
+ 'd_ch1', 'd_ch2', 'd_ch3', 'd_ch4'}
+ # sample marker are more accurate than sync markers -> lower d_ch numbers
+ activation_config['ch1_2mrk'] = {'a_ch1',
+ 'd_ch1', 'd_ch3'}
+ activation_config['ch2_2mrk'] = {'a_ch2',
+ 'd_ch2', 'd_ch4'}
+
+ constraints.activation_config = activation_config
+
+ return constraints
+
+ def _get_init_output_levels(self):
+
+ constr = self.get_constraints()
+
+ a_ampl = {'a_ch1': constr.a_ch_amplitude.default, 'a_ch2': constr.a_ch_amplitude.default}
+
+ d_ampl_low = {'d_ch1': constr.d_ch_low.default, 'd_ch2': constr.d_ch_low.default,
+ 'd_ch3': constr.d_ch_low.default, 'd_ch4': constr.d_ch_low.default}
+ d_ampl_high = {'d_ch1': constr.d_ch_high.default, 'd_ch2': constr.d_ch_high.default,
+ 'd_ch3': constr.d_ch_high.default, 'd_ch4': constr.d_ch_high.default}
+ d_ampl_low, d_ampl_high = self._output_levels_by_config(d_ampl_low, d_ampl_high)
+
+ a_offs = {}
+
+ return {'a_ampl': a_ampl, 'a_offs': a_offs,
+ 'd_ampl_low': d_ampl_low, 'd_ampl_high': d_ampl_high}
+
+ def _set_dac_resolution(self):
+ if self._dac_resolution == 12:
+ self.write(':TRAC1:DWID WSP')
+ self.write(':TRAC2:DWID WSP')
+ elif self._dac_resolution == 14:
+ self.write(':TRAC1:DWID WPR')
+ self.write(':TRAC2:DWID WPR')
+ else:
+ self.log.error("Unsupported DAC resolution: {}.".format(self._dac_resolution))
+
+ def _set_dac_amplifier_mode(self):
+ # todo: implement choosing amp mode
+ if self._dac_amp_mode != 'direct':
+ raise NotImplementedError("Non direct output '{}' not yet implemented."
+ .format(self._dac_amp_mode))
+ self.write(':OUTP1:ROUT DAC')
+ self.write(':OUTP2:ROUT DAC')
+
+ def _write_output_on(self):
+ self.write_all_ch("OUTP{}:NORM ON")
+
+ def _compile_bin_samples(self, analog_samples, digital_samples, ch_num):
+
+ marker = self.marker_on
+
+ a_samples = self.float_to_sample(analog_samples[ch_num])
+ marker_sample = digital_samples[self._analogue_ch_corresponding_digital_chs(ch_num)[0]]
+ marker_sync = digital_samples[self._analogue_ch_corresponding_digital_chs(ch_num)[1]]
+ d_samples = self.bool_to_sample(marker_sample, marker_sync, int_type_str='int16')
+ if marker:
+ comb_samples = a_samples + d_samples
+ else:
+ comb_samples = a_samples
+
+ return comb_samples
+
+ def _get_digital_ch_cmd(self, digital_ch_name):
+ d_ch_internal = self._digital_ch_2_internal(digital_ch_name)
+ return ':{}:VOLT'.format(d_ch_internal)
+
+ 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 = []
+
+ active_ch = dict()
+
+ if ch == []:
+ active_ch['a_ch1'] = bool(int(self.query(':OUTP1:NORM?')))
+ active_ch['a_ch2'] = bool(int(self.query(':OUTP2:NORM?')))
+
+ # marker channels are active if corresponding analogue channel on
+ active_ch['d_ch1'] = active_ch[self._digital_ch_corresponding_analogue_ch('d_ch1')]
+ active_ch['d_ch2'] = active_ch[self._digital_ch_corresponding_analogue_ch('d_ch2')]
+ active_ch['d_ch3'] = active_ch[self._digital_ch_corresponding_analogue_ch('d_ch3')]
+ active_ch['d_ch4'] = active_ch[self._digital_ch_corresponding_analogue_ch('d_ch4')]
+
+ else:
+ for channel in ch:
+ if 'a_ch' in channel:
+ ana_chan = int(channel[4:])
+ active_ch[channel] = bool(int(self.ask(':OUTP{0}:NORM?'.format(ana_chan))))
+
+ elif 'd_ch' in channel:
+ active_ch[channel] = active_ch[self._digital_ch_corresponding_analogue_ch(channel)]
+
+ return active_ch
+
+ def _set_active_ch(self, new_channels_state):
+
+ # get lists of all analog channels
+ analog_channels = self._get_all_analog_channels()
+ digital_channels = self._get_all_digital_channels()
+ current_channel_state = self.get_active_channels()
+
+ # awg 8190: no own channels, digital channels belong to analogue ones
+ # iterate digital channels and activate if corresponding analogue is on
+ for chnl in current_channel_state.copy():
+ if chnl.startswith('d_'):
+ if new_channels_state[self._digital_ch_corresponding_analogue_ch(chnl)]:
+ new_channels_state[chnl] = True
+ else:
+ new_channels_state[chnl] = False
+
+ # Also (de)activate the channels accordingly
+ # awg8190a: digital channels belong to analogue ones
+ for a_ch in analog_channels:
+ ach_num = self.chstr_2_chnum(a_ch)
+ # (de)activate the analog channel
+ if new_channels_state[a_ch]:
+ self.write('OUTP{0:d}:NORM ON'.format(ach_num))
+ else:
+ self.write('OUTP{0:d}:NORM OFF'.format(ach_num))
+
+ def float_to_sample(self, val):
+
+ val_int = self._float_to_int(val, self._dac_resolution)
+ shiftbits = 16 - self._dac_resolution # 2 for marker, dac: 12 -> 2, dac: 14 -> 4
+
+ return val_int.astype('int16') << shiftbits
+
+ def _delete_all_sequences(self):
+
+ self.write_all_ch(':SEQ{}:DEL:ALL')
+
+ def _define_new_sequence(self, name, n_steps):
+
+ seq_id_ch1 = int(self.query(":SEQ1:DEF:NEW? {:d}".format(n_steps)))
+ seq_id_ch2 = int(self.query(":SEQ2:DEF:NEW? {:d}".format(n_steps)))
+ if seq_id_ch1 != seq_id_ch2:
+ self.log.warning("Sequence tables for channels seem not aligned.")
+ self.write(":SEQ1:NAME {:d}, '{}'".format(seq_id_ch1, name))
+ self.write(":SEQ2:NAME {:d}, '{}'".format(seq_id_ch2, name))
+
+ def _get_loaded_seq_catalogue(self, ch_num):
+ return self.query(':SEQ{:d}:CAT?'.format(ch_num))
+
+ def _get_loaded_seq_name(self, ch_num, idx):
+ """
+ :param ch_num:
+ :param idx: 0,1,2. Not the sequenceId = seqtable id of first element in sequence
+ :return:
+ """
+ seq_id = self.get_loaded_assets_id(ch_num, 'sequence')[idx]
+ return self.query(':SEQ{:d}:NAME? {:d}'.format(ch_num, seq_id))
+
+ def _get_sequence_control_bin(self, sequence_parameters, idx_step):
+
+ index = idx_step
+ wfm_tuple, seq_step = sequence_parameters[index]
+ num_steps = len(sequence_parameters)
+
+ try:
+ next_step = sequence_parameters[index + 1][1]
+ except IndexError:
+ next_step = None
+
+ control = 0
+
+ if index == 0:
+ control = 0x1 << 28 # bit 28 (=0x10000000): mark as sequence start
+ if index + 1 == num_steps:
+ control = 0x1 << 30 # bit 30: mark as sequence end
+
+ # in use case with external pattern jump, every segment with an address
+ # defines a "sequence" (as defined in Keysight manual)
+ if 'pattern_jump_address' in seq_step:
+ control = 0x1 << 28
+ if next_step:
+ if 'pattern_jump_address' in next_step:
+ control = 0x1 << 30
+
+ control += 0x1 << 24 # always enable markers
+
+ return control
+
diff --git a/hardware/ni_x_series_in_streamer.py b/hardware/ni_x_series_in_streamer.py
index 2f2cf322b4..7f748b180e 100644
--- a/hardware/ni_x_series_in_streamer.py
+++ b/hardware/ni_x_series_in_streamer.py
@@ -1080,16 +1080,16 @@ def terminate_all_tasks(self):
self._di_readers = list()
self._ai_reader = None
- for i in range(len(self._di_task_handles)):
+ while len(self._di_task_handles) > 0:
try:
- if not self._di_task_handles[i].is_task_done():
- self._di_task_handles[i].stop()
- self._di_task_handles[i].close()
+ if not self._di_task_handles[-1].is_task_done():
+ self._di_task_handles[-1].stop()
+ self._di_task_handles[-1].close()
except ni.DaqError:
self.log.exception('Error while trying to terminate digital counter task.')
err = -1
finally:
- del self._di_task_handles[i]
+ del self._di_task_handles[-1]
self._di_task_handles = list()
if self._ai_task_handle is not None:
diff --git a/hardware/switches/digital_switch_ni.py b/hardware/switches/digital_switch_ni.py
new file mode 100644
index 0000000000..bead268c38
--- /dev/null
+++ b/hardware/switches/digital_switch_ni.py
@@ -0,0 +1,213 @@
+# -*- coding: utf-8 -*-
+"""
+Control external hardware by the output of the digital channels of a NI card.
+
+Qudi is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Qudi is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Qudi. If not, see .
+
+Copyright (c) the Qudi Developers. See the COPYRIGHT.txt file at the
+top-level directory of this distribution and at
+"""
+
+import time
+import re
+import nidaqmx
+from core.module import Base
+from core.configoption import ConfigOption
+from core.util.mutex import RecursiveMutex
+from interface.switch_interface import SwitchInterface
+
+from core.statusvariable import StatusVar
+
+
+class DigitalSwitchNI(Base, SwitchInterface):
+ """ This class enables to control a switch via the NI card.
+ Control external hardware by the output of the digital channels of a NI card.
+
+ Example config for copy-paste:
+
+ digital_switch_ni:
+ module.Class: 'switches.digital_switch_ni.DigitalSwitchNI'
+ channel: '/Dev1/port0/line30:31' # optional
+ name: 'My Switch Hardware Name' # optional
+ switch_time: 0.1
+ remember_states: True
+ switches: # optional
+ One: ['Low', 'High']
+ Two: ['Off', 'On']
+ """
+ # Channels of the NI Card to be used for switching.
+ # Can either be a single channel or multiple lines.
+ _channel = ConfigOption(name='channel', default='/Dev1/port0/line31', missing='warn')
+ # switch_time to wait after setting the states for the connected hardware to react
+ _switch_time = ConfigOption(name='switch_time', default=0.1, missing='nothing')
+ # optionally customize all switches in config. Each switch needs a tuple of 2 state names.
+ # If used, you must specify as many switches as you have specified channels
+ _switches = ConfigOption(name='switches', default=None, missing='nothing')
+ # optional name of the hardware
+ _hardware_name = ConfigOption(name='name', default=None, 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=True, missing='nothing')
+ # if inverted is True, first entry in switches is "high" and second is "low"
+ _inverted_states = ConfigOption(name='inverted_states', default=False, missing='nothing')
+
+ _states = StatusVar(name='states', default=None)
+
+ def __init__(self, *args, **kwargs):
+ """ Create the digital switch output control module
+ """
+ super().__init__(*args, **kwargs)
+ self.lock = RecursiveMutex()
+
+ self._channels = tuple()
+
+ def on_activate(self):
+ """ Prepare module, connect to hardware.
+ The number of switches is automatically determined from the ConfigOption channel:
+ /Dev1/port0/line31 lead to 1 switch
+ /Dev1/port0/line29:31 leads to 3 switches
+ """
+ # Determine DO lines to use. This defines the number of switches for this module.
+ assert isinstance(self._channel, str), 'ConfigOption "channel" must be str type'
+ match = re.match(r'(.*?dev\d/port\d/line)(\d+)(?::(\d+))?', self._channel, re.IGNORECASE)
+ match_pfi = re.match(r'(.*?dev\d/pfi)(\d+)(?::(\d+))?', self._channel, re.IGNORECASE)
+ assert match is not None or match_pfi is not None, \
+ 'channel string invalid. Valid examples: "/Dev1/port0/line29:31", "/Dev1/PFI10:13"'
+ if match is not None and match.groups()[2] is None:
+ self._channels = (match.group(),)
+ elif match_pfi is not None and match_pfi.groups()[2] is None:
+ self._channels = (match_pfi.group(),)
+ elif match is not None:
+ first, last = sorted(int(ch) for ch in match.groups()[1:])
+ prefix = match.groups()[0]
+ self._channels = tuple('{0}{1:d}'.format(prefix, ii) for ii in range(first, last + 1))
+ elif match_pfi is not None:
+ first, last = sorted(int(ch) for ch in match_pfi.groups()[1:])
+ prefix = match_pfi.groups()[0]
+ self._channels = tuple('{0}{1:d}'.format(prefix, ii) for ii in range(first, last + 1))
+ else:
+ self.log.error(f'channel does not have the required format: "{self._channel}"')
+
+ # Determine available switches and states
+ if self._switches is None:
+ self._switches = {str(ii): ('Off', 'On') for ii in range(1, len(self._channels) + 1)}
+ self._switches = self._chk_refine_available_switches(self._switches)
+
+ if self._hardware_name is None:
+ self._hardware_name = 'NICard' + str(self._channel).replace('/', ' ')
+
+ # 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):
+ """ Disconnect from hardware on deactivation.
+ """
+ pass
+
+ @property
+ def name(self):
+ """ Name of the hardware as string.
+
+ The name can either be defined as ConfigOption (name) or it defaults to the name of the hardware module.
+
+ @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"}
+ """
+ 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"}
+ """
+ avail_states = self.available_states
+ assert isinstance(state_dict,
+ dict), f'Property "state" must be dict type. Received: {type(state_dict)}'
+ assert all(switch in avail_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())}'
+
+ if state_dict:
+ with self.lock:
+ new_states = self._states.copy()
+ new_states.update(state_dict)
+ with nidaqmx.Task('NISwitchTask' + self.name.replace(':', ' ')) as switch_task:
+ binary = list()
+ for channel_index, (switch, state) in enumerate(new_states.items()):
+ switch_task.do_channels.add_do_chan(self._channels[channel_index])
+ if self._inverted_states:
+ binary.append(avail_states[switch][0] == state)
+ else:
+ binary.append(avail_states[switch][0] != state)
+ switch_task.write(binary, auto_start=True)
+ time.sleep(self._switch_time)
+ self._states = new_states
+
+ 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._states, f'Invalid switch name: "{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}
+
+ def _chk_refine_available_switches(self, switch_dict):
+ """ See SwitchInterface class for details
+
+ @param dict switch_dict:
+ @return dict:
+ """
+ refined = super()._chk_refine_available_switches(switch_dict)
+ num = len(self._channels)
+ assert len(refined) == num, f'Exactly {num} 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
diff --git a/hardware/switches/flipmirror.py b/hardware/switches/flipmirror.py
index 4950e95f1e..8ec2e81331 100644
--- a/hardware/switches/flipmirror.py
+++ b/hardware/switches/flipmirror.py
@@ -23,162 +23,156 @@
import time
from core.module import Base
from core.configoption import ConfigOption
-from core.util.mutex import Mutex
+from core.statusvariable import StatusVar
+from core.util.mutex import RecursiveMutex
from interface.switch_interface import SwitchInterface
class FlipMirror(Base, SwitchInterface):
- """ This class is implements communication with the Radiant Dyes flip mirror driver
- through pyVISA.
+ """ This class is implements communication with the Radiant Dyes flip mirror driver using pyVISA
Example config for copy-paste:
flipmirror_switch:
module.Class: 'switches.flipmirror.FlipMirror'
interface: 'ASRL1::INSTR'
-
+ name: 'Flipmirror Switch' # optional
+ switch_time: 2 # optional
+ remember_states: False # optional
+ switch_name: 'Detection' # optional
+ switch_states: ['Spectrometer', 'APD'] # optional
"""
- serial_interface = ConfigOption('interface', 'ASRL1::INSTR', missing='warn')
-
- def __init__(self, config, **kwargs):
- """ Creae flip mirror control module
-
- @param object manager: reference to module manager
- @param str name: unique module name
- @param dict config; configuration parameters in a dict
- @param dict kwargs: aditional parameters in a dict
- """
- super().__init__(config=config, **kwargs)
- self.lock = Mutex()
+ # ConfigOptions to give the single switch and its states custom names
+ _switch_name = ConfigOption(name='switch_name', default='1', missing='nothing')
+ _switch_states = ConfigOption(name='switch_states', default=['Down', 'Up'], missing='nothing')
+ # optional name of the hardware
+ _hardware_name = ConfigOption(name='name', default='Flipmirror 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')
+ # switch_time to wait after setting the states for the solenoids to react
+ _switch_time = ConfigOption(name='switch_time', default=2.0, missing='nothing')
+ # name of the serial interface where the hardware is connected.
+ # Use e.g. the Keysight IO connections expert to find the device.
+ serial_interface = ConfigOption('interface', 'ASRL1::INSTR', missing='error')
+
+ # 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.lock = RecursiveMutex()
+ self._resource_manager = None
+ self._instrument = None
+ self._switches = dict()
def on_activate(self):
""" Prepare module, connect to hardware.
"""
- self.rm = visa.ResourceManager()
- self.inst = self.rm.open_resource(
- self.serial_interface,
- baud_rate=115200,
- write_termination='\r\n',
- read_termination='\r\n',
- timeout=10,
- send_end=True
+ assert isinstance(self._switch_name, str), 'ConfigOption "switch_name" must be str type'
+ assert len(self._switch_states) == 2, 'ConfigOption "switch_states" must be len 2 iterable'
+ self._switches = self._chk_refine_available_switches(
+ {self._switch_name: self._switch_states}
)
+ self._resource_manager = visa.ResourceManager()
+ self._instrument = self._resource_manager.open_resource(
+ self.serial_interface,
+ baud_rate=115200,
+ write_termination='\r\n',
+ read_termination='\r\n',
+ timeout=10,
+ send_end=True
+ )
+
+ # 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):
""" Disconnect from hardware on deactivation.
"""
- self.inst.close()
- self.rm.close()
+ self._instrument.close()
+ self._resource_manager.close()
- def getNumberOfSwitches(self):
- """ Gives the number of switches connected to this hardware.
+ @property
+ def name(self):
+ """ Name of the hardware as string.
- @return int: number of swiches on this hardware
+ @return str: The name of the hardware
"""
- return 1
+ return self._hardware_name
- def getSwitchState(self, switchNumber):
- """ Gives state of switch.
+ @property
+ def available_states(self):
+ """ Names of the states as a dict of tuples.
- @param int switchNumber: number of switch
+ 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 bool: True if vertical, False if horizontal, None on error
+ @return dict: Available states per switch in the form {"switch": ("state1", "state2")}
"""
- with self.lock:
- pos = self.inst.ask('GP1')
- if pos == 'H1':
- return False
- elif pos == 'V1':
- return True
- else:
- return None
+ return self._switches.copy()
- def getCalibration(self, switchNumber, state):
- """ Get calibration parameter for switch.
+ @property
+ def states(self):
+ """ The current states the hardware is in.
- @param int switchNumber: number of switch for which to get calibration parameter
- @param str switchState: state ['On', 'Off'] for which to get calibration parameter
+ The states of the system as a dict consisting of switch names as keys and state names as values.
- @return str: calibration parameter fir switch and state.
-
- In this case, the calibration parameter is a integer number that says where the
- horizontal and vertical position of the flip mirror is in the 16 bit PWM range of the motor driver.
- The number is returned as a string, not as an int, and needs to be converted.
- """
- with self.lock:
- try:
- if state == 'On':
- answer = self.inst.ask('GVT1')
- else:
- answer = self.inst.ask('GHT1')
- result = int(answer.split('=')[1])
- except:
- result = -1
- return result
-
- def setCalibration(self, switchNumber, state, value):
- """ Set calibration parameter for switch.
-
- @param int switchNumber: number of switch for which to get calibration parameter
- @param str switchState: state ['On', 'Off'] for which to get calibration parameter
- @param int value: calibration parameter to be set.
-
- @return bool: True if success, False on error
+ @return dict: All the current states of the switches in a state dict of the form {"switch": "state"}
"""
with self.lock:
- try:
- answer = self.inst.ask('SHT1 {0}'.format(int(value)))
- if answer != 'OK1':
- return False
- except:
- return False
- return True
+ response = self._instrument.query('GP1').strip().upper()
+ assert response in {'H1', 'V1'}, f'Unexpected hardware return value: "{response}"'
+ switch, avail_states = next(iter(self.available_states.items()))
+ self._states = {switch: avail_states[int(response == 'V1')]}
+ return self._states.copy()
- def switchOn(self, switchNumber):
- """ Turn the flip mirror to vertical position.
+ @states.setter
+ def states(self, state_dict):
+ """ The setter for the states of the hardware.
- @param int switchNumber: number of switch to be switched
+ 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.
- @return bool: True if suceeds, False otherwise
+ @param dict state_dict: state dict of the form {"switch": "state"}
"""
- with self.lock:
- try:
- answer = self.inst.ask('SV1')
- if answer != 'OK1':
- return False
- time.sleep(self.getSwitchTime(switchNumber))
- self.log.info('{0} switch {1}: On'.format(
- self._name, switchNumber))
- except:
- return False
- return True
-
- def switchOff(self, switchNumber):
- """ Turn the flip mirror to horizontal position.
-
- @param int switchNumber: number of switch to be switched
-
- @return bool: True if suceeds, False otherwise
+ 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())}'
+
+ if state_dict:
+ with self.lock:
+ switch, state = next(iter(state_dict.items()))
+ down = self.available_states[switch][0] == state
+ answer = self._instrument.query('SH1' if down else 'SV1', delay=self._switch_time)
+ assert answer == 'OK1', \
+ f'setting of state "{state}" in switch "{switch}" failed with return value "{answer}"'
+ self._states = {switch: state}
+ self.log.debug('{0}-{1}: {2}'.format(self.name, switch, state))
+
+ 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
"""
- with self.lock:
- try:
- answer = self.inst.ask('SH1')
- if answer != 'OK1':
- return False
- time.sleep(self.getSwitchTime(switchNumber))
- self.log.info('{0} switch {1}: Off'.format(
- self._name, switchNumber))
- except:
- return False
- return True
-
-
- def getSwitchTime(self, switchNumber):
- """ Give switching time for switch.
+ assert switch in self.available_states, f'Invalid switch name: "{switch}"'
+ return self.states[switch]
- @param int switchNumber: number of switch
+ def set_state(self, switch, state):
+ """ Query state of single switch by name
- @return float: time needed for switch state change
+ @param str switch: name of the switch to change
+ @param str state: name of the state to set
"""
- return 2.0
+ self.states = {switch: state}
diff --git a/hardware/switches/hbridge.py b/hardware/switches/hbridge.py
index 6b2c79f400..c0ab4a40af 100644
--- a/hardware/switches/hbridge.py
+++ b/hardware/switches/hbridge.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
-Control custom board with 4 H bridges.
+Control the Radiant Dyes flip mirror driver through the serial interface.
Qudi is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -23,7 +23,8 @@
import time
from core.module import Base
from core.configoption import ConfigOption
-from core.util.mutex import Mutex
+from core.statusvariable import StatusVar
+from core.util.mutex import RecursiveMutex
from interface.switch_interface import SwitchInterface
@@ -32,138 +33,160 @@ class HBridge(Base, SwitchInterface):
Example config for copy-paste:
- flipmirror_switch:
+ h_bridge_switch:
module.Class: 'switches.hbridge.HBridge'
interface: 'ASRL1::INSTR'
-
+ name: 'HBridge Switch' # optional
+ switch_time: 0.5 # optional
+ remember_states: False # optional
+ switches: # optional
+ One: ['Spectrometer', 'APD']
+ Two: ['Spectrometer', 'APD']
+ Three: ['Spectrometer', 'APD']
+ Four: ['Spectrometer', 'APD']
"""
- serial_interface = ConfigOption('interface', 'ASRL1::INSTR', missing='warn')
-
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
- self.lock = Mutex()
+ # ConfigOptions
+
+ # customize all 4 available switches in config. Each switch needs a tuple of 2 state names.
+ _switches = ConfigOption(name='switches',
+ default={str(ii): ('Off', 'On') for ii in range(1, 5)},
+ missing='nothing')
+ # optional name of the hardware
+ _hardware_name = ConfigOption(name='name', default='HBridge 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')
+ # switch_time to wait after setting the states for the solenoids to react
+ _switch_time = ConfigOption(name='switch_time', default=0.5, missing='nothing')
+ # name of the serial interface where the hardware is connected.
+ # Use e.g. the Keysight IO connections expert to find the device.
+ serial_interface = ConfigOption('interface', 'ASRL1::INSTR', missing='error')
+
+ # 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.lock = RecursiveMutex()
+ self._resource_manager = None
+ self._instrument = None
def on_activate(self):
- """ Activate module.
+ """ Prepare module, connect to hardware.
"""
- self.rm = visa.ResourceManager()
- self.inst = self.rm.open_resource(
- self.serial_interface,
- baud_rate=9600,
- write_termination='\r\n',
- read_termination='\r\n',
- timeout=10,
- send_end=True
+ self._switches = self._chk_refine_available_switches(self._switches)
+
+ self._resource_manager = visa.ResourceManager()
+ self._instrument = self._resource_manager.open_resource(
+ self.serial_interface,
+ baud_rate=9600,
+ write_termination='\r\n',
+ read_termination='\r\n',
+ timeout=10,
+ send_end=True
)
+ # 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 module.
+ """ Disconnect from hardware on deactivation.
"""
- self.inst.close()
+ self._instrument.close()
+ self._resource_manager.close()
- def getNumberOfSwitches(self):
- """ Gives the number of switches connected to this hardware.
+ @property
+ def name(self):
+ """ Name of the hardware as string.
- @return int: number of switches
+ @return str: The name of the hardware
"""
- return 4
+ return self._hardware_name
- def getSwitchState(self, switchNumber):
- """ Gives state of switch.
+ @property
+ def available_states(self):
+ """ Names of the states as a dict of tuples.
- @param int switchNumber: number of switch
+ 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 bool: True if on, False if off, None on error
+ @return dict: Available states per switch in the form {"switch": ("state1", "state2")}
"""
- with self.lock:
- pos = self.inst.ask('STATUS')
- ret = list()
- for i in pos.split():
- ret.append(int(i))
- return ret[switchNumber]
+ return self._switches.copy()
- def getCalibration(self, switchNumber, state):
- """ Get calibration parameter for switch.
-
- @param int switchNumber: number of switch for which to get calibration parameter
- @param str switchState: state ['On', 'Off'] for which to get calibration parameter
-
- @return str: calibration parameter fir switch and state.
-
- In this case, the calibration parameter is the time for which current is
- applied to the coil/motor for switching.
+ @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"}
"""
- return 0
+ with self.lock:
+ binary = tuple(int(pos == '1') for pos in self.inst.ask('STATUS').strip().split())
+ avail_states = self.available_states
+ self._states = {switch: states[binary[index]] for index, (switch, states) in
+ enumerate(avail_states.items())}
+ return self._states.copy()
- def setCalibration(self, switchNumber, state, value):
- """ Set calibration parameter for switch.
+ @states.setter
+ def states(self, state_dict):
+ """ The setter for the states of the hardware.
- @param int switchNumber: number of switch for which to get calibration parameter
- @param str switchState: state ['On', 'Off'] for which to get calibration parameter
- @param int value: calibration parameter to be set.
+ 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.
- @return bool: True if success, False on error
+ @param dict state_dict: state dict of the form {"switch": "state"}
"""
- pass
-
- def switchOn(self, switchNumber):
- """ Extend coil or move motor.
+ assert isinstance(state_dict, dict), \
+ f'Property "state" must be dict type. Received: {type(state_dict)}'
- @param int switchNumber: number of switch to be switched
-
- @return bool: True if suceeds, False otherwise
- """
- coilnr = int(switchNumber) + 1
- if 0 < int(coilnr) < 5:
+ if state_dict:
with self.lock:
- try:
- answer = self.inst.ask('P{0}=1'.format(coilnr))
- if answer != 'P{0}=1'.format(coilnr):
- return False
- time.sleep(self.getSwitchTime(switchNumber))
- self.log.info('{0} switch {1}: On'.format(
- self._name, switchNumber))
- except:
- return False
- return True
- else:
- self.log.error('You are trying to use non-existing output no {0}'
- ''.format(coilnr))
+ for switch, state in state_dict.items():
+ self.set_state(switch, state)
- def switchOff(self, switchNumber):
- """ Retract coil ore move motor.
+ def get_state(self, switch):
+ """ Query state of single switch by name
- @param int switchNumber: number of switch to be switched
-
- @return bool: True if suceeds, False otherwise
+ @param str switch: name of the switch to query the state for
+ @return str: The current switch state
"""
- coilnr = int(switchNumber) + 1
- if 0 < int(coilnr) < 5:
- with self.lock:
- try:
- answer = self.inst.ask('P{0}=0'.format(coilnr))
- if answer != 'P{0}=0'.format(coilnr):
- return False
- time.sleep(self.getSwitchTime(switchNumber))
- self.log.info('{0} switch {1}: Off'.format(
- self._name, switchNumber))
- except:
- return False
- return True
- else:
- self.log.error('You are trying to use non-existing output no {0}'
- ''.format(coilnr))
-
- def getSwitchTime(self, switchNumber):
- """ Give switching time for switch.
-
- @param int switchNumber: number of switch
+ assert switch in self.available_states, 'Invalid switch name "{0}"'.format(switch)
+ return self.states[switch]
- @return float: time needed for switch state change
+ def set_state(self, switch, state):
+ """ Query state of single switch by name
- Coils typically switch faster than 0.5s, but safety first!
+ @param str switch: name of the switch to change
+ @param str state: name of the state to set
"""
- return 0.5
+ avail_states = self.available_states
+ assert switch in avail_states, f'Invalid switch name: "{switch}"'
+ assert state in avail_states[switch], f'Invalid state name "{state}" for switch "{switch}"'
+ with self.lock:
+ switch_index = self.switch_names.index(switch) + 1
+ state_index = avail_states[switch].index(state) + 1
+ cmd = 'P{0:d}={1:d}'.format(switch_index, state_index)
+ answer = self._instrument.ask(cmd)
+ assert answer == cmd, \
+ f'setting of state "{state}" in switch "{switch}" failed with return value "{answer}"'
+ time.sleep(self._switch_time)
+
+ @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) == 4, 'Exactly 4 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
diff --git a/hardware/switches/ok_fpga/ok_s6_switch.py b/hardware/switches/ok_fpga/ok_s6_switch.py
deleted file mode 100644
index 60008d4930..0000000000
--- a/hardware/switches/ok_fpga/ok_s6_switch.py
+++ /dev/null
@@ -1,261 +0,0 @@
-# -*- coding: utf-8 -*-
-
-"""
-This file contains the Qudi hardware module for the FPGA (Opal Kelly XEM6310) based software
-defined 8-channel CMOS switch.
-
-Qudi is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-Qudi is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with Qudi. If not, see .
-
-Copyright (c) the Qudi Developers. See the COPYRIGHT.txt file at the
-top-level directory of this distribution and at
-"""
-
-import os
-import okfrontpanel as ok
-from core.module import Base
-from core.configoption import ConfigOption
-from core.util.modules import get_main_dir
-from core.util.mutex import Mutex
-from interface.switch_interface import SwitchInterface
-
-
-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'
-
- """
-
- # config options
- _serial = ConfigOption('fpga_serial', missing='error')
- # possible type options: XEM6310_LX150, XEM6310_LX45
- _fpga_type = ConfigOption('fpga_type', default='XEM6310_LX45', missing='warn')
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- self._fpga = None
- self.threadlock = Mutex()
- self._switch_status = dict()
- self._connected = False
-
- def on_activate(self):
- """ Connect and configure the access to the FPGA.
- """
- # Create an instance of the Opal Kelly FrontPanel. The Frontpanel is a
- # c dll which was wrapped with SWIG for Windows type systems to be
- # accessed with python 3.4. You have to ensure to use the python 3.4
- # version to be able to run the Frontpanel wrapper:
- self._fpga = ok.FrontPanel()
-
- # TTL output status of the 8 channels
- self._switch_status = {chnl: False for chnl in range(8)}
-
- self._connected = False
-
- # Sanity check for fpga_type ConfigOption
- self._fpga_type = self._fpga_type.upper()
- if self._fpga_type not in ('XEM6310_LX45', 'XEM6310_LX150'):
- self.log.error('Unsupported FPGA type "{0}" specified in config. Valid options are '
- '"XEM6310_LX45" and "XEM6310_LX150".\nAborting module activation.'
- ''.format(self._fpga_type))
- return
-
- # connect to the FPGA module
- self._connect()
- return
-
- def on_deactivate(self):
- """ Deactivate the FPGA.
- """
- if self._connected:
- self.reset()
- del self._fpga
- self._connected = False
- return
-
- def _connect(self):
- """
- Connect host PC to FPGA module with the specified serial number.
- """
- # check if a FPGA is connected to this host PC. That method is used to
- # determine also how many devices are available.
- if not self._fpga.GetDeviceCount():
- self.log.error('No FPGA connected to host PC or FrontPanel.exe is running.')
- return -1
-
- # open a connection to the FPGA with the specified serial number
- self._fpga.OpenBySerial(self._serial)
-
- # upload the proper hardware switch configuration bitfile to the FPGA
- 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
-
- # Load on the FPGA a configuration file (bit file).
- self._fpga.ConfigureFPGA(os.path.join(get_main_dir(), 'thirdparty', 'qo_fpga',
- bitfile_name))
-
- # Check if the upload was successful and the Opal Kelly FrontPanel is enabled on the FPGA
- if not self._fpga.IsFrontPanelEnabled():
- self.log.error('Opal Kelly FrontPanel is not enabled in FPGA')
- return -1
-
- self._fpga.SetWireInValue(0x00, 0x00000000)
- self._fpga.UpdateWireIns()
-
- self._switch_status = {chnl: False for chnl in range(8)}
- self._connected = True
- return 0
-
- def getNumberOfSwitches(self):
- """ There are 8 TTL channels on the OK FPGA.
- Chan PIN
- ----------
- Ch1 B14
- Ch2 B16
- Ch3 B12
- Ch4 C7
- Ch5 D15
- Ch6 D10
- Ch7 D9
- Ch8 D11
-
- @return int: number of switches
- """
- return 8
-
- def getSwitchState(self, channel):
- """ Gives state of switch.
-
- @param int channel: number of switch channel
-
- @return bool: True if on, False if off, None on error
- """
- if channel not in self._switch_status:
- self.log.error('FPGA switch only accepts channel numbers 0..7. Asked for channel {0}.'
- ''.format(channel))
- return None
- self._get_all_states()
- return self._switch_status[channel]
-
- def switchOn(self, channel):
- with self.threadlock:
- if channel not in self._switch_status:
- self.log.error('FPGA switch only accepts channel numbers 0..7. Asked for channel '
- '{0}.'.format(channel))
- return
-
- # determine new channels status
- new_state = self._switch_status.copy()
- new_state[channel] = True
-
- # encode channel states
- chnl_state = 0
- for chnl in list(new_state):
- if new_state[chnl]:
- chnl_state += int(2 ** chnl)
-
- # apply changes in hardware
- self._fpga.SetWireInValue(0x00, chnl_state)
- self._fpga.UpdateWireIns()
-
- # get new state from hardware
- actual_state = self._get_all_states()
- if new_state != actual_state:
- self.log.error('Setting of channel states in hardware failed.')
- return
-
- def switchOff(self, channel):
- with self.threadlock:
- if channel not in self._switch_status:
- self.log.error('FPGA switch only accepts channel numbers 0..7. Asked for channel '
- '{0}.'.format(channel))
- return
-
- # determine new channels status
- new_state = self._switch_status.copy()
- new_state[channel] = False
-
- # encode channel states
- chnl_state = 0
- for chnl in list(new_state):
- if new_state[chnl]:
- chnl_state += int(2 ** chnl)
-
- # apply changes in hardware
- self._fpga.SetWireInValue(0x00, chnl_state)
- self._fpga.UpdateWireIns()
-
- # get new state from hardware
- actual_state = self._get_all_states()
- if new_state != actual_state:
- self.log.error('Setting of channel states in hardware failed.')
- return
-
- def reset(self):
- """
- Reset TTL outputs to zero
- """
- with self.threadlock:
- if not self._connected:
- return
- self._fpga.SetWireInValue(0x00, 0)
- self._fpga.UpdateWireIns()
- self._switch_status = {chnl: False for chnl in range(8)}
- return
-
- def getCalibration(self, switchNumber, state):
- return -1
-
- def setCalibration(self, switchNumber, state, value):
- return True
-
- def getSwitchTime(self, switchNumber):
- """ Give switching time for switch.
-
- @param int switchNumber: number of switch
-
- @return float: time needed for switch state change
- """
- return 100.0e-3
-
- def _get_all_states(self):
- self._fpga.UpdateWireOuts()
- new_state = int(self._fpga.GetWireOutValue(0x20))
- for chnl in list(self._switch_status):
- if new_state & (2 ** chnl) != 0:
- self._switch_status[chnl] = True
- else:
- self._switch_status[chnl] = False
- return self._switch_status
diff --git a/hardware/switches/ok_s6_switch.py b/hardware/switches/ok_s6_switch.py
new file mode 100644
index 0000000000..3ed715d1d5
--- /dev/null
+++ b/hardware/switches/ok_s6_switch.py
@@ -0,0 +1,258 @@
+# -*- coding: utf-8 -*-
+
+"""
+This file contains the Qudi hardware module for the FPGA (Opal Kelly XEM6310) based software
+defined 8-channel CMOS switch.
+
+Qudi is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Qudi is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Qudi. If not, see .
+
+Copyright (c) the Qudi Developers. See the COPYRIGHT.txt file at the
+top-level directory of this distribution and at
+"""
+
+import os
+import okfrontpanel as ok
+from core.module import Base
+from core.configoption import ConfigOption
+from core.statusvariable import StatusVar
+from core.util.modules import get_main_dir
+from core.util.mutex import RecursiveMutex
+from interface.switch_interface import SwitchInterface
+
+
+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: # 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
diff --git a/hardware/switches/osw12.py b/hardware/switches/osw12.py
index 59e6b00588..e066ce2301 100644
--- a/hardware/switches/osw12.py
+++ b/hardware/switches/osw12.py
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
"""
+Control for a Thorlabs OWS12 MEMS Fiber-Optic Switch through the serial interface.
+
Qudi is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
@@ -18,108 +20,151 @@
"""
import visa
+import time
from core.module import Base
from core.configoption import ConfigOption
+from core.util.mutex import Mutex
from interface.switch_interface import SwitchInterface
-class Main(Base, SwitchInterface):
- """ This class is implements communication with Thorlabs OSW12(22) fibered switch
-
- Example config for copy-paste:
-
- fibered_switch:
- module.Class: 'switches.osw12.Main'
- interface: 'ASRL1::INSTR'
+class OSW12(Base, SwitchInterface):
+ """ This class is implements communication with Thorlabs OSW12(22) fibered switch.
Description of the hardware provided by Thorlabs:
Thorlabs offers a line of bidirectional fiber optic switch kits that include a MEMS optical switch with an
- integrated control circuit that offers a USB 2.0 interface for easy integration into your optical system.
- Choose from 1x2 or 2x2 MEMS modules with any of the following operating wavelengths:
+ integrated control circuit that offers a USB 2.0 interface for easy integration into your optical system.
+ Choose from 1x2 or 2x2 MEMS modules with any of the following operating wavelengths:
480 - 650 nm, 600 - 800 nm, 750 - 950 nm, 800 - 1000 nm, 970 - 1170 nm, or 1280 - 1625 nm.
These bidirectional switches have low insertion loss and excellent repeatability.
- """
- interface = ConfigOption('interface', 'ASRL1::INSTR', missing='error')
+ Example config for copy-paste:
- _rm = None
- _inst = None
+ fibered_switch:
+ module.Class: 'switches.osw12.OSW12'
+ interface: 'ASRL1::INSTR'
+ name: 'MEMS Fiber-Optic Switch' # optional
+ switch_name: 'Detection' # optional
+ switch_states: ['Off', 'On'] # optional
+ """
+
+ # ConfigOptions to give the single switch and its states custom names
+ _switch_name = ConfigOption(name='switch_name', default='1', missing='nothing')
+ _switch_states = ConfigOption(name='switch_states', default=['Off', 'On'], missing='nothing')
+ # optional name of the hardware
+ _hardware_name = ConfigOption(name='name', default='MEMS Fiber-Optic Switch', missing='nothing')
+ # name of the serial interface where the hardware is connected.
+ # Use e.g. the Keysight IO connections expert to find the device.
+ serial_interface = ConfigOption('interface', 'ASRL1::INSTR')
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.lock = Mutex()
+ self._resource_manager = None
+ self._instrument = None
+ self._switches = dict()
def on_activate(self):
- """ Module activation method """
- self._rm = visa.ResourceManager()
- try:
- self._inst = self._rm.open_resource(self.interface, baud_rate=115200, write_termination='\n',
- read_termination='\r\n')
- except visa.VisaIOError:
- self.log.error('Could not connect to OSW device')
+ """ Prepare module, connect to hardware.
+ """
+ assert isinstance(self._switch_name, str), 'ConfigOption "switch_name" must be str type'
+ assert len(self._switch_states) == 2, 'ConfigOption "switch_states" must be len 2 iterable'
+ self._switches = self._chk_refine_available_switches(
+ {self._switch_name: self._switch_states}
+ )
+
+ self._resource_manager = visa.ResourceManager()
+ self._instrument = self._resource_manager.open_resource(
+ self.serial_interface,
+ baud_rate=115200,
+ write_termination='\n',
+ read_termination='\r\n',
+ timeout=10,
+ send_end=True
+ )
def on_deactivate(self):
- """ Disconnect from hardware on deactivation. """
- self._inst.close()
- self._rm.close()
+ """ Disconnect from hardware on deactivation.
+ """
+ self._instrument.close()
+ self._resource_manager.close()
- def getNumberOfSwitches(self):
- """ Gives the number of switches connected to this hardware.
+ @property
+ def name(self):
+ """ Name of the hardware as string.
- @return int: number of swiches on this hardware
+ @return str: The name of the hardware
"""
- return 1
+ return self._hardware_name
- def getSwitchState(self, switchNumber=0):
- """ Get the state of the switch.
+ @property
+ def available_states(self):
+ """ Names of the states as a dict of tuples.
- @param int switchNumber: index of switch
+ 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 bool: True if 1, False if 2
- """
- state = self._inst.query('S?\n')
- if state == '1':
- return True
- elif state == '2':
- return False
- else:
- self.log.error('Hardware returned {} as switch state.'.format(state))
-
- def getCalibration(self, switchNumber, state):
- """ Get calibration parameter for switch.
-
- Function not used by this module
+ @return dict: Available states per switch in the form {"switch": ("state1", "state2")}
"""
- return 0
+ return self._switches.copy()
- def setCalibration(self, switchNumber, state, value):
- """ Set calibration parameter for switch.
+ @property
+ def states(self):
+ """ The current states the hardware is in as state dictionary with switch names as keys and
+ state names as values.
- Function not used by this module
+ @return dict: All the current states of the switches in the form {"switch": "state"}
"""
- return True
+ with self.lock:
+ return {switch: self.get_state(switch) for switch in self.available_states}
- def switchOn(self, switchNumber):
- """ Set the state to on (channel 1)
+ @states.setter
+ def states(self, state_dict):
+ """ The setter for the states of the hardware.
- @param int switchNumber: number of switch to be switched
+ 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.
- @return bool: True if succeeds, False otherwise
+ @param dict state_dict: state dict of the form {"switch": "state"}
"""
- self._inst.write('S 1')
- return True
+ assert isinstance(state_dict, dict), 'Parameter "state_dict" must be dict type'
+ with self.lock:
+ for switch, state in state_dict.items():
+ self.set_state(switch, state)
- def switchOff(self, switchNumber):
- """ Set the state to off (channel 2)
+ def get_state(self, switch):
+ """ Query state of single switch by name
- @param int switchNumber: number of switch to be switched
-
- @return bool: True if suceeds, False otherwise
+ @param str switch: name of the switch to query the state for
+ @return str: The current switch state
"""
- self._inst.write('S 2')
- return True
-
- def getSwitchTime(self, switchNumber):
- """ Give switching time for switch.
+ avail_states = self.available_states
+ assert switch in avail_states, 'Invalid switch name "{0}"'.format(switch)
+
+ with self.lock:
+ for attempt in range(3):
+ try:
+ response = self._instrument.query('S?').strip()
+ except visa.VisaIOError:
+ self.log.debug('Hardware query raised VisaIOError, trying again...')
+ else:
+ assert response in {'1', '2'}, f'Unexpected return value "{response}"'
+ return avail_states[switch][int(response == '1')]
+ raise Exception('Hardware did not respond after 3 attempts. Visa error')
+
+ 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
+ """
+ avail_states = self.available_states
+ assert switch in avail_states, f'Invalid switch name: "{switch}"'
+ assert state in avail_states[switch], f'Invalid state name "{state}" for switch "{switch}"'
- @param int switchNumber: number of switch
+ with self.lock:
+ direction = avail_states[switch].index(state)
+ self._instrument.write('S {0:d}'.format(1 if direction else 2))
+ time.sleep(0.1)
- @return float: time needed for switch state change
- """
- return 1e-3 # max. 1 ms; typ. 0.5 ms
+ # FIXME: For some reason first returned value is not updated yet, let's clear it.
+ _ = self.states
diff --git a/hardware/switches/switch_dummy.py b/hardware/switches/switch_dummy.py
index 88de08fd88..93a5217c2f 100644
--- a/hardware/switches/switch_dummy.py
+++ b/hardware/switches/switch_dummy.py
@@ -19,71 +19,94 @@
top-level directory of this distribution and at
"""
-
from core.module import Base
from interface.switch_interface import SwitchInterface
-import time
+from core.configoption import ConfigOption
+from core.statusvariable import StatusVar
class SwitchDummy(Base, SwitchInterface):
- """ Methods to control slow laser switching devices.
+ """ Methods to control slow switching devices.
Example config for copy-paste:
switch_dummy:
module.Class: 'switches.switch_dummy.SwitchDummy'
-
+ name: 'First' # optional
+ remember_states: True # optional
+ switches:
+ one: ['down', 'up']
+ two: ['down', 'up']
+ three: ['low', 'middle', 'high']
"""
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
+ # ConfigOptions
+ # customize available switches in config. Each switch needs a tuple of at least 2 state names.
+ _switches = ConfigOption(name='switches', missing='error')
+ # optional name of the hardware
+ _hardware_name = ConfigOption(name='name', default=None, 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=True, missing='nothing')
- self.switchState = [False, False, False]
- self.switchCalibration = dict()
- self.switchCalibration['On'] = [0.9, 0.8, 0.88]
- self.switchCalibration['Off'] = [0.15, 0.3, 0.2]
+ # StatusVariable for remembering the last state of the hardware
+ _states = StatusVar(name='states', default=None)
def on_activate(self):
- pass
+ """ Activate the module and fill status variables.
+ """
+ self._switches = self._chk_refine_available_switches(self._switches)
+
+ # Choose config name for this module if no name is given in ConfigOptions
+ if self._hardware_name is None:
+ self._hardware_name = self._name
+
+ # 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}
+ else:
+ self._states = {switch: states[0] for switch, states in self._switches.items()}
def on_deactivate(self):
+ """ Deactivate the module and clean up.
+ """
pass
- def getNumberOfSwitches(self):
- """ Gives the number of switches connected to this hardware.
- """
- return len(self.switchState)
+ @property
+ def name(self):
+ """ Name of the hardware as string.
- def getSwitchState(self, switchNumber):
- """
+ @return str: The name of the hardware
"""
- return self.switchState[switchNumber]
+ return self._hardware_name
- def getCalibration(self, switchNumber, state):
- """
- """
- return self.switchCalibration[state][switchNumber]
+ @property
+ def available_states(self):
+ """ Names of the states as a dict of tuples.
- def setCalibration(self, switchNumber, state, value):
- """
- """
- self.switchCalibration[state][switchNumber] = value
+ 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.
- def switchOn(self, switchNumber):
- """
+ @return dict: Available states per switch in the form {"switch": ("state1", "state2")}
"""
- self.switchState[switchNumber] = True
- time.sleep(self.getSwitchTime(switchNumber))
- self.log.info('{0} switch {1}: On'.format(self._name, switchNumber))
- return self.switchState[switchNumber]
+ return self._switches.copy()
- def switchOff(self, switchNumber):
- """
+ 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
"""
- self.switchState[switchNumber] = False
- time.sleep(self.getSwitchTime(switchNumber))
- self.log.info('{0} switch {1}: Off'.format(self._name, switchNumber))
- return self.switchState[switchNumber]
+ assert switch in self.available_states, f'Invalid switch name: "{switch}"'
+ return self._states[switch]
- def getSwitchTime(self, switchNumber):
- return 0.5
+ 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
+ """
+ avail_states = self.available_states
+ assert switch in avail_states, f'Invalid switch name: "{switch}"'
+ assert state in avail_states[switch], f'Invalid state name "{state}" for switch "{switch}"'
+ self._states[switch] = state
diff --git a/interface/switch_interface.py b/interface/switch_interface.py
index f0dc832e89..a4e3cb0c9d 100644
--- a/interface/switch_interface.py
+++ b/interface/switch_interface.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
"""
-Control the Radiant Dyes flip mirror driver through the serial interface.
-
Qudi is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
@@ -20,84 +18,116 @@
top-level directory of this distribution and at
"""
-from core.interface import abstract_interface_method
+from core.interface import abstract_interface_method, interface_method
from core.meta import InterfaceMetaclass
class SwitchInterface(metaclass=InterfaceMetaclass):
- """ Methods to control slow (mechanical) laser switching devices.
+ """ Methods to control slow (mechanical) switching devices.
- Warning: This interface use CamelCase. This is should not be done in future versions. See more info here :
- documentation/programming_style.md
+ Getter and setter functions to control single switches need to be implemented by the hardware
+ module.
+ Automatically implements Python properties to access and set the switch states based on the
+ single switch getter and setter method.
"""
+ @property
@abstract_interface_method
- def getNumberOfSwitches(self):
- """ Gives the number of switches connected to this hardware.
+ def name(self):
+ """ Name of the hardware as string.
- @return int: number of swiches on this hardware
+ @return str: The name of the hardware
"""
pass
+ @property
@abstract_interface_method
- def getSwitchState(self, switchNumber):
- """ Gives state of switch.
+ def available_states(self):
+ """ Names of the states as a dict of tuples.
- @param int switchNumber: number of switch
+ 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 bool: True if on, False if off, None on error
+ @return dict: Available states per switch in the form {"switch": ("state1", "state2")}
"""
pass
@abstract_interface_method
- def getCalibration(self, switchNumber, state):
- """ Get calibration parameter for switch.
-
- @param int switchNumber: number of switch for which to get calibration parameter
- @param str switchState: state ['On', 'Off'] for which to get calibration parameter
+ def get_state(self, switch):
+ """ Query state of single switch by name
- @return str: calibration parameter fir switch and state.
+ @param str switch: name of the switch to query the state for
+ @return str: The current switch state
"""
pass
@abstract_interface_method
- def setCalibration(self, switchNumber, state, value):
- """ Set calibration parameter for switch.
+ def set_state(self, switch, state):
+ """ Query state of single switch by name
- @param int switchNumber: number of switch for which to get calibration parameter
- @param str switchState: state ['On', 'Off'] for which to get calibration parameter
- @param int value: calibration parameter to be set.
-
- @return bool: True if suceeds, False otherwise
+ @param str switch: name of the switch to change
+ @param str state: name of the state to set
"""
pass
- @abstract_interface_method
- def switchOn(self, switchNumber):
- """ Switch on.
+ # Non-abstract default implementations below
- @param int switchNumber: number of switch to be switched
+ @property
+ @interface_method
+ def number_of_switches(self):
+ """ Number of switches provided by the hardware.
- @return bool: True if suceeds, False otherwise
+ @return int: number of switches
"""
- pass
+ return len(self.available_states)
- @abstract_interface_method
- def switchOff(self, switchNumber):
- """ Switch off.
+ @property
+ @interface_method
+ def switch_names(self):
+ """ Names of all available switches as tuple.
- @param int switchNumber: number of switch to be switched
+ @return str[]: Tuple of strings of available switch names.
+ """
+ return tuple(self.available_states)
- @return bool: True if suceeds, False otherwise
+ @property
+ @interface_method
+ 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"}
"""
- pass
+ return {switch: self.get_state(switch) for switch in self.available_states}
- @abstract_interface_method
- def getSwitchTime(self, switchNumber):
- """ Give switching time for switch.
+ @states.setter
+ def states(self, state_dict):
+ """ The setter for the states of the hardware.
- @param int switchNumber: number of switch
-s
- @return float: time needed for switch state change
+ 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"}
"""
- pass
+ assert isinstance(state_dict, dict), 'Parameter "state_dict" must be dict type'
+ for switch, state in state_dict.items():
+ self.set_state(switch, state)
+
+ @staticmethod
+ def _chk_refine_available_switches(switch_dict):
+ """ Perform some general checking of the configured available switches and their possible
+ states. When implementing a hardware module, you can overwrite this method to include
+ custom checks, but make sure to call this implementation first via super().
+
+ @param dict switch_dict: available switches in a dict like {"switch1": ["state1", "state2"]}
+ @return dict: The refined switch dict to replace the dict passed as argument
+ """
+ assert isinstance(switch_dict, dict), 'switch_dict must be a dict of tuples'
+ assert all((isinstance(sw, str) and sw) for sw in
+ switch_dict), 'Switch name must be non-empty string'
+ assert all(len(states) > 1 for states in
+ switch_dict.values()), 'State tuple must contain at least 2 states'
+ assert all(all((s and isinstance(s, str)) for s in states) for states in
+ switch_dict.values()), 'Switch states must be non-empty strings'
+ # Convert state lists to tuples in order to restrict mutation
+ return {switch: tuple(states) for switch, states in switch_dict.items()}
diff --git a/logic/fitmethods/decaylikemethods.py b/logic/fitmethods/decaylikemethods.py
index 2318a47d27..dc4814c067 100644
--- a/logic/fitmethods/decaylikemethods.py
+++ b/logic/fitmethods/decaylikemethods.py
@@ -20,11 +20,11 @@
top-level directory of this distribution and at
"""
-
import numpy as np
from lmfit.models import Model
from scipy.ndimage import filters
+
############################################################################
# #
# Defining Exponential Models #
@@ -52,12 +52,13 @@ def make_barestretchedexponentialdecay_model(self, prefix=None):
lmfit.model.CompositeModel.
object lmfit.parameter.Parameters params:
- It is basically an OrderedDict, so a dictionary, with keys
+ It is basically a dictionary, with keys
denoting the parameters as string names and values which are
lmfit.parameter.Parameter (without s) objects, keeping the
information about the current value.
"""
+
def barestretchedexponentialdecay_function(x, beta, lifetime):
""" Function of a bare exponential decay.
@@ -66,13 +67,13 @@ def barestretchedexponentialdecay_function(x, beta, lifetime):
@return: bare exponential decay function: in order to use it as a model
"""
- return np.exp(-np.power(x/lifetime, beta))
+ return np.exp(-np.power(x / lifetime, beta))
if not isinstance(prefix, str) and prefix is not None:
self.log.error('The passed prefix <{0}> of type {1} is not a string and'
- 'cannot be used as a prefix and will be ignored for now.'
- 'Correct that!'.format(prefix, type(prefix)))
+ 'cannot be used as a prefix and will be ignored for now.'
+ 'Correct that!'.format(prefix, type(prefix)))
model = Model(barestretchedexponentialdecay_function,
independent_vars='x')
else:
@@ -83,6 +84,7 @@ def barestretchedexponentialdecay_function(x, beta, lifetime):
return model, params
+
##############################
# Single exponential decay #
##############################
@@ -106,6 +108,7 @@ def make_bareexponentialdecay_model(self, prefix=None):
return bare_exp_decay, params
+
def make_decayexponential_model(self, prefix=None):
""" Create a exponential decay model with an amplitude and offset.
@@ -120,15 +123,8 @@ def make_decayexponential_model(self, prefix=None):
bare_exp_model, params = self.make_bareexponentialdecay_model(prefix=prefix)
-
-
-
amplitude_model, params = self.make_amplitude_model(prefix=prefix)
-
-
-
-
constant_model, params = self.make_constant_model(prefix=prefix)
exponentialdecay_model = amplitude_model * bare_exp_model + constant_model
@@ -136,6 +132,7 @@ def make_decayexponential_model(self, prefix=None):
return exponentialdecay_model, params
+
#################################
# Stretched exponential decay #
#################################
@@ -161,6 +158,37 @@ def make_decayexponentialstretched_model(self, prefix=None):
return stre_exp_decay_offset, params
+
+#################################
+# Biexponential decay #
+#################################
+
+def make_biexponential_model(self, prefix=None):
+ """ Create a exponential model with an amplitude and offset.
+
+ @param str prefix: optional string, which serves as a prefix for all
+ parameters used in this model. That will prevent
+ name collisions if this model is used in a composite
+ way.
+
+ @return tuple: (object model, object params), for more description see in
+ the method make_barestretchedexponential_model.
+ """
+
+ exp0_model, params = self.make_bareexponentialdecay_model(prefix='e0_')
+ amp0_model, params = self.make_amplitude_model(prefix='e0_')
+
+ exp1_model, params = self.make_bareexponentialdecay_model(prefix='e1_')
+ amp1_model, params = self.make_amplitude_model(prefix='e1_')
+
+ constant_model, params = self.make_constant_model(prefix=prefix)
+
+ exponential_model = amp0_model * exp0_model + amp1_model * exp1_model + constant_model
+ params = exponential_model.make_params()
+
+ return exponential_model, params
+
+
############################################################################
# #
# Fit methods and their estimators #
@@ -177,7 +205,7 @@ def make_decayexponential_fit(self, x_axis, data, estimator, units=None, add_par
@param numpy.array x_axis: 1D axis values
@param numpy.array data: 1D data, should have the same dimension as x_axis.
@param Parameters or dict add_params: optional, additional parameters of
- type lmfit.parameter.Parameters, OrderedDict or dict for the fit
+ type lmfit.parameter.Parameters, dict for the fit
which will be used instead of the values from the estimator.
@return object result: lmfit.model.ModelFit object, all parameters
@@ -196,25 +224,24 @@ def make_decayexponential_fit(self, x_axis, data, estimator, units=None, add_par
except:
result = exponentialdecay.fit(data, x=x_axis, params=params, **kwargs)
self.log.warning('The exponentialdecay with offset fit did not work. '
- 'Message: {}'.format(str(result.message)))
-
+ 'Message: {}'.format(str(result.message)))
if units is None:
units = ['arb. unit', 'arb. unit']
- result_str_dict = dict() #create result string for gui
+ result_str_dict = dict() # create result string for gui
result_str_dict['Amplitude'] = {'value': result.params['amplitude'].value,
'error': result.params['amplitude'].stderr,
- 'unit': units[1]} #amplitude
+ 'unit': units[1]} # amplitude
result_str_dict['Lifetime'] = {'value': result.params['lifetime'].value,
- 'error': result.params['lifetime'].stderr,
- 'unit': units[0]} #lifetime
+ 'error': result.params['lifetime'].stderr,
+ 'unit': units[0]} # lifetime
result_str_dict['Offset'] = {'value': result.params['offset'].value,
- 'error': result.params['offset'].stderr,
- 'unit': units[1]} #offset
+ 'error': result.params['offset'].stderr,
+ 'unit': units[1]} # offset
result.result_str_dict = result_str_dict
@@ -240,7 +267,7 @@ def estimate_decayexponential(self, x_axis, data, params):
# calculation of offset, take the last 10% from the end of the data
# and perform the mean from those.
- offset = data[-max(1, int(len(x_axis)/10)):].mean()
+ offset = data[-max(1, int(len(x_axis) / 10)):].mean()
# substraction of offset, check whether
if data[0] < data[-1]:
@@ -253,7 +280,6 @@ def estimate_decayexponential(self, x_axis, data, params):
if data_level.min() <= 0:
data_level = data_level - data_level.min()
-
# remove all the data that can be smaller than or equals to std.
# when the data is smaller than std, it is beyond resolution
# which is not helpful to our fitting.
@@ -270,7 +296,7 @@ def estimate_decayexponential(self, x_axis, data, params):
# linear fit, see linearmethods.py
linear_result = self.make_linear_fit(x_axis=x_axis[0:i], data=data_level_log, estimator=self.estimate_linear)
- params['lifetime'].set(value=-1/linear_result.params['slope'].value, min=min_lifetime)
+ params['lifetime'].set(value=-1 / linear_result.params['slope'].value, min=min_lifetime)
# amplitude can be positive of negative
if data[0] < data[-1]:
@@ -280,13 +306,14 @@ def estimate_decayexponential(self, x_axis, data, params):
except:
self.log.warning('Lifetime too small in estimate_exponentialdecay, beyond resolution!')
- params['lifetime'].set(value=x_axis[i]-x_axis[0], min=min_lifetime)
+ params['lifetime'].set(value=x_axis[i] - x_axis[0], min=min_lifetime)
params['amplitude'].set(value=data_level[0])
params['offset'].set(value=offset)
return error, params
+
#############################################
# stretched exponential decay with offset #
#############################################
@@ -299,7 +326,7 @@ def make_decayexponentialstretched_fit(self, x_axis, data, estimator, units=None
@param object estimator: Pointer to the estimator method
@param list units: List containing the ['horizontal', 'vertical'] units as strings
@param Parameters or dict add_params: optional, additional parameters of
- type lmfit.parameter.Parameters, OrderedDict or dict for the fit
+ type lmfit.parameter.Parameters, dict for the fit
which will be used instead of the values from the estimator.
@return object result: lmfit.model.ModelFit object, all parameters
@@ -318,33 +345,34 @@ def make_decayexponentialstretched_fit(self, x_axis, data, estimator, units=None
except:
result = stret_exp_decay_offset.fit(data, x=x_axis, params=params, **kwargs)
self.log.warning('The double exponentialdecay with offset fit did not work. '
- 'Message: {}'.format(str(result.message)))
+ 'Message: {}'.format(str(result.message)))
if units is None:
units = ['arb. unit', 'arb. unit']
- result_str_dict = dict() #create result string for gui
+ result_str_dict = dict() # create result string for gui
result_str_dict['Amplitude'] = {'value': result.params['amplitude'].value,
'error': result.params['amplitude'].stderr,
- 'unit': units[1]} #amplitude
+ 'unit': units[1]} # amplitude
result_str_dict['Lifetime'] = {'value': result.params['lifetime'].value,
- 'error': result.params['lifetime'].stderr,
- 'unit': units[0]} #lifetime
+ 'error': result.params['lifetime'].stderr,
+ 'unit': units[0]} # lifetime
result_str_dict['Offset'] = {'value': result.params['offset'].value,
- 'error': result.params['offset'].stderr,
- 'unit': units[1]} #offset
+ 'error': result.params['offset'].stderr,
+ 'unit': units[1]} # offset
result_str_dict['Beta'] = {'value': result.params['beta'].value,
- 'error': result.params['beta'].stderr,
- 'unit': ''} #Beta (exponent of exponential exponent)
+ 'error': result.params['beta'].stderr,
+ 'unit': ''} # Beta (exponent of exponential exponent)
result.result_str_dict = result_str_dict
return result
+
def estimate_decayexponentialstretched(self, x_axis, data, params):
""" Provide an estimation for initial values for a stretched exponential decay with offset.
@@ -369,16 +397,16 @@ def estimate_decayexponentialstretched(self, x_axis, data, params):
# calculation of offset, take the last 10% from the end of the data
# and perform the mean from those.
- offset = data_smoothed[-max(1, int(len(x_axis)/10)):].mean()
+ offset = data_smoothed[-max(1, int(len(x_axis) / 10)):].mean()
# substraction of the offset and correction of the decay behaviour
# (decay to a bigger value or decay to a smaller value)
if data_smoothed[0] < data_smoothed[-1]:
data_smoothed = offset - data_smoothed
- ampl_sign=-1
+ ampl_sign = -1
else:
data_smoothed = data_smoothed - offset
- ampl_sign=1
+ ampl_sign = 1
if data_smoothed.min() <= 0:
data_smoothed = data_smoothed - data_smoothed.min()
@@ -395,17 +423,162 @@ def estimate_decayexponentialstretched(self, x_axis, data, params):
poly_coef = np.polyfit(x_axis[0:stop_index], data_level_log, deg=2)
# obtain the values from the polynomical fit
- lifetime = 1/np.sqrt(abs(poly_coef[0]))
+ lifetime = 1 / np.sqrt(abs(poly_coef[0]))
amplitude = np.exp(poly_coef[2])
# Include all the estimated fit parameter:
- params['amplitude'].set(value=amplitude*ampl_sign)
+ params['amplitude'].set(value=amplitude * ampl_sign)
params['offset'].set(value=offset)
- min_lifetime = 2 * (x_axis[1]-x_axis[0])
+ min_lifetime = 2 * (x_axis[1] - x_axis[0])
params['lifetime'].set(value=lifetime, min=min_lifetime)
# as an arbitrary starting point:
params['beta'].set(value=2, min=0)
return error, params
+
+
+#############################################
+# biexponential function with offset #
+#############################################
+
+
+def make_biexponential_fit(self, x_axis, data, estimator,
+ units=None,
+ add_params=None, **kwargs):
+ """ Perform a biexponential fit on the provided data.
+
+ @param numpy.array x_axis: 1D axis values
+ @param numpy.array data: 1D data, should have the same dimension as x_axis.
+ @param method estimator: Pointer to the estimator method
+ @param list units: List containing the ['horizontal', 'vertical'] units as strings
+ @param Parameters or dict add_params: optional, additional parameters of
+ type lmfit.parameter.Parameters, dict for the fit
+ which will be used instead of the values from the estimator.
+
+ @return object model: lmfit.model.ModelFit object, all parameters
+ provided about the fitting, like: success,
+ initial fitting values, best fitting values, data
+ with best fit with given axis,...
+ """
+ if units is None:
+ units = ['arb. unit', 'arb. unit']
+
+ model, params = self.make_biexponential_model()
+
+ error, params = estimator(x_axis, data, params)
+
+ params = self._substitute_params(initial_params=params,
+ update_params=add_params)
+ try:
+ result = model.fit(data, x=x_axis, params=params, **kwargs)
+ except:
+ result = model.fit(data, x=x_axis, params=params, **kwargs)
+ self.log.warning('The double gaussian dip fit did not work: {0}'.format(
+ result.message))
+
+ # Write the parameters to allow human-readable output to be generated
+ result_str_dict = dict()
+
+ result_str_dict['1st amplitude'] = {'value': result.params['e0_amplitude'].value,
+ 'error': result.params['e0_amplitude'].stderr,
+ 'unit': units[1]} # amplitude
+
+ result_str_dict['1st lifetime'] = {'value': result.params['e0_lifetime'].value,
+ 'error': result.params['e0_lifetime'].stderr,
+ 'unit': units[0]} # lifetime
+
+ result_str_dict['1st beta'] = {'value': result.params['e0_beta'].value,
+ 'error': result.params['e0_beta'].stderr,
+ 'unit': ''} # Beta (exponent of exponential exponent)
+
+ result_str_dict['2nd amplitude'] = {'value': result.params['e1_amplitude'].value,
+ 'error': result.params['e1_amplitude'].stderr,
+ 'unit': units[1]} # amplitude
+
+ result_str_dict['2nd lifetime'] = {'value': result.params['e1_lifetime'].value,
+ 'error': result.params['e1_lifetime'].stderr,
+ 'unit': units[0]} # lifetime
+
+ result_str_dict['2nd beta'] = {'value': result.params['e1_beta'].value,
+ 'error': result.params['e1_beta'].stderr,
+ 'unit': ''} # Beta (exponent of exponential exponent)
+
+ result_str_dict['offset'] = {'value': result.params['offset'].value,
+ 'error': result.params['offset'].stderr,
+ 'unit': units[1]} # offset
+
+ result.result_str_dict = result_str_dict
+ return result
+
+
+def estimate_biexponential(self, x_axis, data, params):
+ """ Estimation of the initial values for an biexponential function.
+
+ @param numpy.array x_axis: 1D axis values
+ @param numpy.array data: 1D data, should have the same dimension as x_axis.
+ @param lmfit.Parameters params: object includes parameter dictionary which
+ can be set
+
+ @return tuple (error, params):
+
+ Explanation of the return parameter:
+ int error: error code (0:OK, -1:error)
+ Parameters object params: set parameters of initial values
+ """
+
+ error = self._check_1D_input(x_axis=x_axis, data=data, params=params)
+
+ # calculation of offset, take the last 10% from the end of the data
+ # and perform the mean from those.
+ offset = data[-max(1, int(len(x_axis) / 10)):].mean()
+
+ # substraction of offset, check whether
+ if data[0] < data[-1]:
+ data_level = offset - data
+ else:
+ data_level = data - offset
+
+ # check if the data level contain still negative values and correct
+ # the data level therefore. Otherwise problems in the logarithm appear.
+ if data_level.min() <= 0:
+ data_level = data_level - data_level.min()
+
+ # remove all the data that can be smaller than or equals to std.
+ # when the data is smaller than std, it is beyond resolution
+ # which is not helpful to our fitting.
+ for i in range(0, len(x_axis)):
+ if data_level[i] <= data_level.std():
+ break
+
+ # values and bound of parameter.
+ ampl = data[-max(1, int(len(x_axis) / 10)):].std()
+ min_lifetime = 1e-16
+
+ try:
+ data_level_log = np.log(data_level[0:i])
+
+ # linear fit, see linearmethods.py
+ linear_result = self.make_linear_fit(x_axis=x_axis[0:i], data=data_level_log, estimator=self.estimate_linear)
+ params['e0_lifetime'].set(value=-1 / linear_result.params['slope'].value, min=min_lifetime)
+ params['e1_lifetime'].set(value=-1 / linear_result.params['slope'].value, min=min_lifetime)
+
+ # amplitude can be positive of negative
+ if data[0] < data[-1]:
+ params['e0_amplitude'].set(value=-np.exp(linear_result.params['offset'].value), max=-ampl)
+ params['e1_amplitude'].set(value=-np.exp(linear_result.params['offset'].value), max=-ampl)
+ else:
+ params['e0_amplitude'].set(value=np.exp(linear_result.params['offset'].value), min=ampl)
+ params['e1_amplitude'].set(value=np.exp(linear_result.params['offset'].value), min=ampl)
+ except:
+ self.log.warning('Lifetime too small in estimate_exponential, beyond resolution!')
+
+ params['e0_lifetime'].set(value=x_axis[i] - x_axis[0], min=min_lifetime)
+ params['e1_lifetime'].set(value=x_axis[i] - x_axis[0], min=min_lifetime)
+ params['e0_amplitude'].set(value=data_level[0])
+ params['e1_amplitude'].set(value=data_level[0])
+
+ params['offset'].set(value=offset)
+
+ return error, params
diff --git a/logic/interfuse/switch_combiner_interfuse.py b/logic/interfuse/switch_combiner_interfuse.py
new file mode 100644
index 0000000000..7a2dff4469
--- /dev/null
+++ b/logic/interfuse/switch_combiner_interfuse.py
@@ -0,0 +1,188 @@
+# -*- coding: utf-8 -*-
+
+"""
+Combine two hardware switches into one.
+
+Qudi is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Qudi is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Qudi. If not, see .
+
+Copyright (c) the Qudi Developers. See the COPYRIGHT.txt file at the
+top-level directory of this distribution and at
+"""
+
+from core.module import Base
+from interface.switch_interface import SwitchInterface
+from core.configoption import ConfigOption
+from core.connector import Connector
+
+
+class SwitchCombinerInterfuse(Base, SwitchInterface):
+ """ Methods to control slow (mechanical) laser switching devices.
+ This interfuse in particular combines two switches into one.
+ """
+
+ # connectors for the switches to be combined
+ switch1 = Connector(interface='SwitchInterface')
+ switch2 = Connector(interface='SwitchInterface')
+
+ # optional name of the combined hardware
+ _hardware_name = ConfigOption(name='name', default=None, missing='nothing')
+
+ # if extend_hardware_name is True the switch names will be extended by the hardware name
+ # of the individual switches in front.
+ _extend_hardware_name = ConfigOption(name='extend_hardware_name',
+ default=False,
+ missing='nothing')
+
+ def on_activate(self):
+ """ Activate the module and fill status variables.
+ """
+ if self._hardware_name is None:
+ self._hardware_name = self._name
+
+ def on_deactivate(self):
+ """ Deactivate the module and clean up.
+ """
+ pass
+
+ @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")}
+ """
+ if self._extend_hardware_name:
+ new_dict = {f'{self.switch1().name}.{switch}': states
+ for switch, states in self.switch1().available_states.items()}
+ new_dict.update({f'{self.switch2().name}.{switch}': states
+ for switch, states in self.switch2().available_states.items()})
+ else:
+ new_dict = {**self.switch1().available_states, **self.switch2().available_states}
+ return new_dict
+
+ @property
+ def number_of_switches(self):
+ """ Number of switches provided by the hardware.
+
+ @return int: number of switches
+ """
+ return self.switch1().number_of_switches + self.switch2().number_of_switches
+
+ @property
+ def switch_names(self):
+ """ Names of all available switches as tuple.
+
+ @return str[]: Tuple of strings of available switch names.
+ """
+ return tuple(self.available_states)
+
+ @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"}
+ """
+ if self._extend_hardware_name:
+ hw_name = self.switch1().name
+ new_dict = {
+ f'{hw_name}.{switch}': states for switch, states in self.switch1().states.items()
+ }
+ hw_name = self.switch2().name
+ new_dict.update(
+ {f'{hw_name}.{switch}': states for switch, states in self.switch2().states.items()}
+ )
+ else:
+ new_dict = {**self.switch1().states, **self.switch2().states}
+ return new_dict
+
+ @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)}'
+ states1 = dict()
+ states2 = dict()
+ hardware1 = self.switch1()
+ hardware2 = self.switch2()
+ for switch, state in state_dict.items():
+ if self._extend_hardware_name:
+ if switch.startswith(f'{hardware2.name}.'):
+ states2[switch[len(hardware2.name) + 1:]] = state
+ elif switch.startswith(f'{hardware1.name}.'):
+ states1[switch[len(hardware1.name) + 1:]] = state
+ else:
+ if switch in hardware2.available_states:
+ states2[switch] = state
+ else:
+ states1[switch] = state
+ if states1:
+ hardware1.states = states1
+ if states2:
+ hardware2.states = states2
+
+ 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, f'Invalid switch name: "{switch}"'
+ if self._extend_hardware_name:
+ hardware = self.switch2()
+ if switch.startswith(f'{hardware.name}.'):
+ return hardware.get_state(switch[len(hardware.name) + 1:])
+ hardware = self.switch1()
+ if switch.startswith(f'{hardware.name}.'):
+ return hardware.get_state(switch[len(hardware.name) + 1:])
+ else:
+ hardware = self.switch2()
+ if switch in hardware.available_states:
+ return hardware.get_state(switch)
+ return self.switch1().get_state(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
+ """
+ if self._extend_hardware_name:
+ hardware = self.switch2()
+ if switch.startswith(f'{hardware.name}.'):
+ return hardware.set_state(switch[len(hardware.name) + 1:], state)
+ hardware = self.switch1()
+ if switch.startswith(f'{hardware.name}.'):
+ return hardware.set_state(switch[len(hardware.name) + 1:], state)
+ else:
+ hardware = self.switch2()
+ if switch in hardware.available_states:
+ return hardware.set_state(switch, state)
+ return self.switch1().set_state(switch, state)
diff --git a/logic/odmr_logic.py b/logic/odmr_logic.py
index 9d3467feaa..a4bd80011d 100644
--- a/logic/odmr_logic.py
+++ b/logic/odmr_logic.py
@@ -839,13 +839,17 @@ def do_fit(self, fit_function=None, x_data=None, y_data=None, channel_index=0, f
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.frequency_lists[fit_range]
- x_data_full_length = np.zeros(len(self.final_freq_list))
- # how to insert the data at the right position?
- start_pos = np.where(np.isclose(self.final_freq_list, self.mw_starts[fit_range]))[0][0]
- x_data_full_length[start_pos:(start_pos + len(x_data))] = x_data
- y_args = np.array([ind_list[0] for ind_list in np.argwhere(x_data_full_length)])
- y_data = self.odmr_plot_y[channel_index][y_args]
+ if fit_range >= 0:
+ x_data = self.frequency_lists[fit_range]
+ x_data_full_length = np.zeros(len(self.final_freq_list))
+ # how to insert the data at the right position?
+ start_pos = np.where(np.isclose(self.final_freq_list, self.mw_starts[fit_range]))[0][0]
+ x_data_full_length[start_pos:(start_pos + len(x_data))] = x_data
+ y_args = np.array([ind_list[0] for ind_list in np.argwhere(x_data_full_length)])
+ y_data = self.odmr_plot_y[channel_index][y_args]
+ else:
+ x_data = self.final_freq_list
+ 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)
diff --git a/logic/pid_logic.py b/logic/pid_logic.py
index d061d88995..b14ae6a320 100644
--- a/logic/pid_logic.py
+++ b/logic/pid_logic.py
@@ -24,14 +24,24 @@
from core.connector import Connector
from core.statusvariable import StatusVar
+from core.configoption import ConfigOption
from core.util.mutex import Mutex
from logic.generic_logic import GenericLogic
from qtpy import QtCore
class PIDLogic(GenericLogic):
- """
- Control a process via software PID.
+ """ Logic module to monitor and control a PID process
+
+ Example config:
+
+ pidlogic:
+ module.Class: 'pid_logic.PIDLogic'
+ timestep: 0.1
+ connect:
+ controller: 'softpid'
+ savelogic: 'savelogic'
+
"""
# declare connectors
@@ -40,7 +50,7 @@ class PIDLogic(GenericLogic):
# status vars
bufferLength = StatusVar('bufferlength', 1000)
- timestep = StatusVar(default=100)
+ timestep = ConfigOption('timestep', 100e-3) # timestep in seconds
# signals
sigUpdateDisplay = QtCore.Signal()
@@ -64,7 +74,7 @@ def on_activate(self):
self.enabled = False
self.timer = QtCore.QTimer()
self.timer.setSingleShot(True)
- self.timer.setInterval(self.timestep)
+ self.timer.setInterval(self.timestep * 1000) # in ms
self.timer.timeout.connect(self.loop)
def on_deactivate(self):
@@ -80,7 +90,7 @@ def startLoop(self):
""" Start the data recording loop.
"""
self.enabled = True
- self.timer.start(self.timestep)
+ self.timer.start(self.timestep * 1000) # in ms
def stopLoop(self):
""" Stop the data recording loop.
@@ -96,7 +106,7 @@ def loop(self):
self.history[2, -1] = self._controller.get_setpoint()
self.sigUpdateDisplay.emit()
if self.enabled:
- self.timer.start(self.timestep)
+ self.timer.start(self.timestep * 1000) # in ms
def getSavingState(self):
""" Return whether we are saving data
diff --git a/logic/switch_logic.py b/logic/switch_logic.py
index c4f1ac260b..d22c63225c 100644
--- a/logic/switch_logic.py
+++ b/logic/switch_logic.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
-Aggregate multiple switches.
+Interact with switches.
Qudi is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -20,40 +20,177 @@
"""
from logic.generic_logic import GenericLogic
-from collections import OrderedDict
+from core.connector import Connector
+from core.configoption import ConfigOption
+from core.util.mutex import RecursiveMutex
+from qtpy import QtCore
class SwitchLogic(GenericLogic):
- """ Logic module aggregating multiple hardware switches.
+ """ Logic module for interacting with the hardware switches.
+ This logic has the same structure as the SwitchInterface but supplies additional functionality:
+ - switches can either be manipulated by index or by their names
+ - signals are generated on state changes
+
+ switchlogic:
+ module.Class: 'switch_logic.SwitchLogic'
+ watchdog_interval: 1 # optional
+ autostart_watchdog: True # optional
+ connect:
+ switch:
"""
- def __init__(self, config, **kwargs):
- """ Create logic object
+ # connector for one switch, if multiple switches are needed use the SwitchCombinerInterfuse
+ switch = Connector(interface='SwitchInterface')
- @param dict config: configuration in a dict
- @param dict kwargs: additional parameters as a dict
- """
- super().__init__(config=config, **kwargs)
+ _watchdog_interval = ConfigOption(name='watchdog_interval', default=1.0, missing='nothing')
+ _autostart_watchdog = ConfigOption(name='autostart_watchdog', default=False, missing='nothing')
+
+ sigSwitchesChanged = QtCore.Signal(dict)
+ sigWatchdogToggled = QtCore.Signal(bool)
+
+ # directly wrapped attributes from hardware module
+ __wrapped_hw_attributes = frozenset({'switch_names', 'number_of_switches', 'available_states'})
- # dynamic number of 'in' connectors depending on config
- if 'connect' in config:
- for connector in config['connect']:
- self.connectors[connector] = OrderedDict()
- self.connectors[connector]['class'] = 'SwitchInterface'
- self.connectors[connector]['object'] = None
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self._thread_lock = RecursiveMutex()
+
+ self._watchdog_active = False
+ self._watchdog_interval_ms = 0
+ self._old_states = dict()
def on_activate(self):
- """ Prepare logic module for work.
+ """ Activate module
"""
- self.switches = dict()
- for connector in self.connectors:
- hwname = self.get_connector(connector)._name
- self.switches[hwname] = dict()
- for i in range(self.get_connector(connector).getNumberOfSwitches()):
- self.switches[hwname][i] = self.get_connector(connector)
+ self._old_states = self.states
+ self._watchdog_interval_ms = int(round(self._watchdog_interval * 1000))
+
+ if self._autostart_watchdog:
+ self._watchdog_active = True
+ QtCore.QMetaObject.invokeMethod(self, '_watchdog_body', QtCore.Qt.QueuedConnection)
+ else:
+ self._watchdog_active = False
def on_deactivate(self):
- """ Deactivate modeule.
+ """ Deactivate module
+ """
+ self._watchdog_active = False
+
+ def __getattr__(self, item):
+ if item in self.__wrapped_hw_attributes:
+ return getattr(self.switch(), item)
+ raise AttributeError(f'SwitchLogic has no attribute with name "{item}"')
+
+ @property
+ def device_name(self):
+ """ Name of the connected hardware switch as string.
+
+ @return str: The name of the connected hardware switch
+ """
+ return self.switch().name
+
+ @property
+ def watchdog_active(self):
+ return self._watchdog_active
+
+ @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._thread_lock:
+ try:
+ states = self.switch().states
+ except:
+ self.log.exception(f'Error during query of all switch states.')
+ states = dict()
+ return states
+
+ @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"}
+ """
+ with self._thread_lock:
+ try:
+ self.switch().states = state_dict
+ except:
+ self.log.exception('Error while trying to set switch states.')
+
+ states = self.states
+ if states:
+ self.sigSwitchesChanged.emit({switch: states[switch] for switch in state_dict})
+
+ 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
+ """
+ with self._thread_lock:
+ try:
+ state = self.switch().get_state(switch)
+ except:
+ self.log.exception(f'Error while trying to query state of switch "{switch}".')
+ state = None
+ return state
+
+ @QtCore.Slot(str, str)
+ 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
+ """
+ with self._thread_lock:
+ try:
+ self.switch().set_state(switch, state)
+ except:
+ self.log.exception(
+ f'Error while trying to set switch "{switch}" to state "{state}".'
+ )
+ curr_state = self.get_state(switch)
+ if curr_state is not None:
+ self.sigSwitchesChanged.emit({switch: curr_state})
+
+ @QtCore.Slot(bool)
+ def toggle_watchdog(self, enable):
"""
- self.switches = dict()
+ @param bool enable:
+ """
+ enable = bool(enable)
+ with self._thread_lock:
+ if enable != self._watchdog_active:
+ self._watchdog_active = enable
+ self.sigWatchdogToggled.emit(enable)
+ if enable:
+ QtCore.QMetaObject.invokeMethod(self,
+ '_watchdog_body',
+ QtCore.Qt.QueuedConnection)
+
+ @QtCore.Slot()
+ def _watchdog_body(self):
+ """ Helper function to regularly query the states from the hardware.
+
+ This function is called by an internal signal and queries the hardware regularly to fire
+ the signal sig_switch_updated, if the hardware changed its state without notifying the logic.
+ The timing of the watchdog is set by the ConfigOption watchdog_interval in seconds.
+ """
+ with self._thread_lock:
+ if self._watchdog_active:
+ curr_states = self.states
+ diff_state = {switch: state for switch, state in curr_states.items() if
+ state != self._old_states[switch]}
+ self._old_states = curr_states
+ if diff_state:
+ self.sigSwitchesChanged.emit(diff_state)
+ QtCore.QTimer.singleShot(self._watchdog_interval_ms, self._watchdog_body)
diff --git a/qtwidgets/loading_indicator.py b/qtwidgets/loading_indicator.py
new file mode 100644
index 0000000000..2cdb300461
--- /dev/null
+++ b/qtwidgets/loading_indicator.py
@@ -0,0 +1,134 @@
+# -*- coding: utf-8 -*-
+
+"""
+This file contains custom QWidgets to show (animated) loading indicators.
+
+Qudi is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Qudi is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Qudi. If not, see .
+
+Copyright (c) the Qudi Developers. See the COPYRIGHT.txt file at the
+top-level directory of this distribution and at
+"""
+
+from qtpy import QtWidgets, QtCore, QtGui
+
+
+class CircleLoadingIndicator(QtWidgets.QWidget):
+ """ Simple circular loading indicator.
+ You can customize cycle period, indicator arc length and width.
+ Animation will automatically start (stop) upon showing (hiding) the widget.
+ The widget can be arbitrarily resized but the actual indicator will always maintain 1:1 aspect
+ ratio and will be centered.
+ The color of the indicator is chosen to be the current palette highlight color.
+
+ Indicator length must be specified as integer value in 1/16th of a degree.
+ Indicator width ratio can be any value 0 < x <= 0.5
+ """
+
+ def __init__(self, *args, cycle_time=1.2, indicator_length=960, indicator_width_ratio=0.2,
+ **kwargs):
+ """
+ @param float cycle_time: The animation time in seconds for a full cycle
+ @param int indicator_length: Length of the indicator arc in 1/16th of a degree
+ @param float indicator_width_ratio: Ratio of the indicator arc width WRT widget size
+ """
+ assert cycle_time > 0, 'cycle_time must be larger than 0'
+ assert 0 < indicator_length < 5760, 'indicator_length must be >0 and <5760'
+ assert 0 < indicator_width_ratio <= 0.5, 'indicator_width_ratio must be >0 and <=0.5'
+ super().__init__(*args, **kwargs)
+ self.setMinimumSize(6, 6)
+ self.setMouseTracking(False)
+ self.setFocusPolicy(QtCore.Qt.NoFocus)
+
+ # Fixed init parameters
+ self._indicator_length = indicator_length
+ self._cycle_time_ms = int(round(1000 * cycle_time))
+ self._indicator_width_ratio = indicator_width_ratio
+
+ # property value (angle in 1/16th of a degree) for current indicator position.
+ # 0 means 3 o'clock.
+ self._indicator_position = 0
+
+ # misc parameters
+ self.__animation = None
+ self.__pen = QtGui.QPen(self.palette().highlight().color())
+ self.__pen.setCapStyle(QtCore.Qt.RoundCap)
+ self.__draw_rect = None
+ self.__update_draw_size()
+ self.__size_hint = None
+ self.__update_size_hint()
+
+ @QtCore.Property(int)
+ def indicator_position(self):
+ return self._indicator_position
+
+ @indicator_position.setter
+ def indicator_position(self, value):
+ self._indicator_position = value
+ self.update()
+
+ def sizeHint(self):
+ return self.__size_hint
+
+ def resizeEvent(self, event):
+ super().resizeEvent(event)
+ self.__update_draw_size()
+ self.__update_size_hint()
+
+ def paintEvent(self, event):
+ # Set up painter
+ p = QtGui.QPainter(self)
+ p.setRenderHint(QtGui.QPainter.Antialiasing, True)
+ p.setBrush(QtCore.Qt.NoBrush)
+ self.__pen.setColor(self.palette().highlight().color()) # in case the palette has changed
+ p.setPen(self.__pen)
+
+ # draw indicator
+ p.drawArc(self.__draw_rect, self._indicator_position, self._indicator_length)
+
+ def showEvent(self, ev):
+ super().showEvent(ev)
+ if self.__animation is None:
+ self.__animation = QtCore.QPropertyAnimation(self, b'indicator_position', self)
+ self.__animation.setDuration(self._cycle_time_ms)
+ self.__animation.setStartValue(0)
+ self.__animation.setEndValue(-5760)
+ self.__animation.setLoopCount(-1)
+ self.__animation.start()
+
+ def hideEvent(self, ev):
+ if self.__animation is not None:
+ self.__animation.stop()
+ self.__animation = None
+ super().hideEvent(ev)
+
+ def __update_draw_size(self):
+ width = self.width()
+ height = self.height()
+ if height > width:
+ x_offset = 0
+ y_offset = (height - width) // 2
+ base_size = width
+ else:
+ x_offset = (width - height) // 2
+ y_offset = 0
+ base_size = height
+ line_width = max(1, int(round(base_size * self._indicator_width_ratio)))
+ margin = max(1, line_width // 2)
+ size = base_size - 2 * margin
+ self.__draw_rect = QtCore.QRect(x_offset + margin, y_offset + margin, size, size)
+ self.__pen.setWidth(line_width)
+
+ def __update_size_hint(self):
+ self.__size_hint = QtCore.QSize(min(self.width(), self.height()),
+ min(self.width(), self.height()))
diff --git a/qtwidgets/scientific_spinbox.py b/qtwidgets/scientific_spinbox.py
index df85b5bb85..8fb04d6bac 100644
--- a/qtwidgets/scientific_spinbox.py
+++ b/qtwidgets/scientific_spinbox.py
@@ -233,6 +233,7 @@ def __init__(self, *args, **kwargs):
self._dynamic_precision = True
self._assumed_unit_prefix = None # To assume one prefix. This is only used if no prefix would be out of range
self._is_valid = True # A flag property to check if the current value is valid.
+ self.disable_wheel = False
self.validator = FloatValidator()
self.lineEdit().textEdited.connect(self.update_value)
self.update_display()
@@ -924,6 +925,17 @@ def stepEnabled(self):
"""
return self.StepUpEnabled | self.StepDownEnabled
+ def wheelEvent(self, event):
+ """
+ Overwriting wheel event, such that with the class variable disable_wheel = True the stepping with the mouse
+ wheel is turned off and the wheel event is passed to the parent widget.
+ :param event:
+ """
+ if self.disable_wheel:
+ event.ignore()
+ else:
+ super().wheelEvent(event)
+
def stepBy(self, steps):
"""
This method is incrementing the value of the SpinBox when the user triggers a step
@@ -1022,6 +1034,7 @@ def __init__(self, *args, **kwargs):
self.__minimalStep = 1
self.__cached_value = None # a temporary variable for restore functionality
self._dynamic_stepping = True
+ self.disable_wheel = False
self.validator = IntegerValidator()
self.lineEdit().textEdited.connect(self.update_value)
self.update_display()
@@ -1332,6 +1345,17 @@ def focusOutEvent(self, event):
super().focusOutEvent(event)
return
+ def wheelEvent(self, event):
+ """
+ Overwriting wheel event, such that with the class variable disable_wheel = True the stepping with the mouse
+ wheel is turned off and the wheel event is passed to the parent widget.
+ :param event:
+ """
+ if self.disable_wheel:
+ event.ignore()
+ else:
+ super().wheelEvent(event)
+
def validate(self, text, position):
"""
Access method to the validator. See IntegerValidator class for more information.
diff --git a/qtwidgets/toggle_switch.py b/qtwidgets/toggle_switch.py
new file mode 100644
index 0000000000..c086914a71
--- /dev/null
+++ b/qtwidgets/toggle_switch.py
@@ -0,0 +1,185 @@
+# -*- coding: utf-8 -*-
+"""
+This file contains a touch-like toggle switch.
+
+Qudi is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Qudi is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Qudi. If not, see .
+
+Copyright (c) the Qudi Developers. See the COPYRIGHT.txt file at the
+top-level directory of this distribution and at
+"""
+
+from qtpy import QtWidgets, QtCore, QtGui
+
+
+class ToggleSwitch(QtWidgets.QAbstractButton):
+ """ A mobile/touch inspired toggle switch to switch between two states.
+ Using default settings, this switch will resize horizontally. If you want a fixed size switch,
+ please set the horizontal size policy to Fixed.
+
+ """
+
+ def __init__(self, parent=None, off_state=None, on_state=None, thumb_track_ratio=1):
+ assert off_state is None or isinstance(off_state, str), 'off_state must be str or None'
+ assert on_state is None or isinstance(on_state, str), 'on_state must be str or None'
+ super().__init__(parent=parent)
+
+ # remember state names
+ if off_state is None and on_state is None:
+ self._state_names = None
+ else:
+ self._state_names = (off_state, on_state)
+
+ # Get default track height from QLineEdit sizeHint if thumb_track_ratio <= 1
+ # If thumb_track_ratio > 1 the QLineEdit height will serve as thumb diameter
+ if thumb_track_ratio > 1:
+ self._thumb_radius = int(round(QtWidgets.QLineEdit().sizeHint().height() / 2))
+ self._track_radius = max(1, int(round(self._thumb_radius / thumb_track_ratio)))
+ self._text_font = QtWidgets.QLabel().font()
+ else:
+ self._track_radius = int(round(QtWidgets.QLineEdit().sizeHint().height() / 2))
+ self._thumb_radius = max(1, int(round(self._track_radius * thumb_track_ratio)))
+ self._track_margin = max(0, self._thumb_radius - self._track_radius)
+ self._thumb_origin = max(self._thumb_radius, self._track_radius)
+
+ # Determine appearance from current palette depending on thumb style
+ palette = self.palette()
+ if thumb_track_ratio > 1:
+ self._track_colors = (palette.dark(), palette.highlight())
+ self._thumb_colors = (palette.light(), palette.highlight())
+ self._text_colors = (palette.text().color(), palette.highlightedText().color())
+ self._track_opacity = 0.5
+ else:
+ self._track_colors = (palette.dark(), palette.highlight())
+ self._thumb_colors = (palette.light(), palette.highlightedText())
+ self._text_colors = (palette.text().color(), palette.highlightedText().color())
+ self._track_opacity = 1
+ self._text_font = QtGui.QFont()
+ # self._text_font.setBold(True)
+ self._text_font.setPixelSize(1.5 * self._track_radius)
+
+ # property value for current thumb position
+ self._thumb_position = self._thumb_origin
+
+ self.setCheckable(True)
+ self.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
+ if self._state_names is None or thumb_track_ratio > 1:
+ self._text_width = 0
+ else:
+ metrics = QtGui.QFontMetrics(self._text_font)
+ self._text_width = max(metrics.width(f' {text} ') for text in self._state_names if text)
+ self._size_hint = QtCore.QSize(
+ 4 * self._track_radius + 2 * self._track_margin + self._text_width,
+ 2 * self._track_radius + 2 * self._track_margin
+ )
+ self.setMinimumSize(self._size_hint)
+
+ @property
+ def current_state(self):
+ return self._state_names[int(self.isChecked())] if self._state_names else None
+
+ @property
+ def _thumb_end(self):
+ return self.width() - self._thumb_origin if self.isChecked() else self._thumb_origin
+
+ @property
+ def _track_color(self):
+ return self._track_colors[int(self.isChecked())]
+
+ @property
+ def _thumb_color(self):
+ return self._thumb_colors[int(self.isChecked())]
+
+ @property
+ def _text_color(self):
+ return self._text_colors[int(self.isChecked())]
+
+ @QtCore.Property(int)
+ def thumb_position(self):
+ return self._thumb_position
+
+ @thumb_position.setter
+ def thumb_position(self, value):
+ self._thumb_position = value
+ self.update()
+
+ def sizeHint(self):
+ return self._size_hint
+
+ def setChecked(self, checked):
+ super().setChecked(checked)
+ self._thumb_position = self._thumb_end
+
+ def resizeEvent(self, event):
+ super().resizeEvent(event)
+ self.thumb_position = self._thumb_end
+
+ def paintEvent(self, event):
+ # Set up painter
+ p = QtGui.QPainter(self)
+ p.setRenderHint(QtGui.QPainter.Antialiasing, True)
+ p.setPen(QtCore.Qt.NoPen)
+ track_opacity = self._track_opacity
+ if self.isEnabled():
+ track_brush = self._track_color
+ thumb_brush = self._thumb_color
+ text_color = self._text_color
+ else:
+ palette = self.palette()
+ track_opacity *= 0.8
+ track_brush = palette.shadow()
+ thumb_brush = palette.mid()
+ text_color = palette.shadow().color()
+
+ # draw track
+ p.setBrush(track_brush)
+ p.setOpacity(track_opacity)
+ p.drawRoundedRect(self._track_margin,
+ self.height()/2 - self._track_radius,
+ self.width() - 2 * self._track_margin,
+ 2 * self._track_radius,
+ self._track_radius,
+ self._track_radius)
+ # draw text if necessary
+ state_str = self.current_state
+ if state_str is not None and self._track_margin == 0:
+ p.setPen(text_color)
+ p.setOpacity(1.0)
+ p.setFont(self._text_font)
+ p.drawText(self._track_margin,
+ self.height() / 2 - self._track_radius,
+ self.width() - 2 * self._track_margin,
+ 2 * self._track_radius,
+ QtCore.Qt.AlignCenter,
+ state_str)
+ # draw thumb
+ p.setPen(QtCore.Qt.NoPen)
+ p.setBrush(thumb_brush)
+ p.setOpacity(1.0)
+ p.drawEllipse(self._thumb_position - self._thumb_radius,
+ self.height()/2 - self._thumb_radius,
+ 2 * self._thumb_radius,
+ 2 * self._thumb_radius)
+
+ def mouseReleaseEvent(self, event):
+ super().mouseReleaseEvent(event)
+ if event.button() == QtCore.Qt.LeftButton:
+ anim = QtCore.QPropertyAnimation(self, b'thumb_position', self)
+ anim.setDuration(200)
+ anim.setStartValue(self._thumb_position)
+ anim.setEndValue(self._thumb_end)
+ anim.start()
+
+ def enterEvent(self, event):
+ self.setCursor(QtCore.Qt.PointingHandCursor)
+ super().enterEvent(event)