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 - - - - - - - - - - - 0 - 0 - 466 - 18 - - - - - Menu - - - - - - - - - ../../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)