diff --git a/CHANGES b/CHANGES index 1ed61e8..84ad999 100644 --- a/CHANGES +++ b/CHANGES @@ -2,10 +2,11 @@ Lantz Changelog =============== -0.4 (unreleased) +0.3.1 (unreleased) ---------------- -- Nothing changed yet. +- Added drivers: SR844 Lock-In +- Support for PyQt5 0.3 (2015-02-05) diff --git a/lantz/drivers/stanford/sr844.py b/lantz/drivers/stanford/sr844.py new file mode 100644 index 0000000..9e3ba21 --- /dev/null +++ b/lantz/drivers/stanford/sr844.py @@ -0,0 +1,447 @@ +# -*- coding: utf-8 -*- +""" + lantz.drivers.stanford.sr844 + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2016 by Lantz Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +import numpy as np + +from lantz import Action, Feat, DictFeat, ureg, Q_ +from lantz.drivers.legacy.serial import SerialDriver +from lantz.errors import InstrumentError + +from collections import OrderedDict +import numpy as np +import logging +from lib.logger import get_all_caller + +display_output = OrderedDict([('X', '0'), + ('Y', '0'), + ('X/Y', '0'), + ('Rv', '1'), + ('Theta', '1'), + ('Rv/Theta', '1'), + ('RdB', '2'), + ('YnoiseV', '2'), + ('Xnoise', '3'), + ('YnoisedB', '3'), + ('AUX', '4')]) + + +class _SR844(object): + def __init__(self,port): + super().__init__(port) + super().initialize() # Automatically open the port + self.TIMEOUT = 20 + self.sens = np.array([1e-7,3e-7,1e-6,3e-6,1e-5,3e-5,1e-4,3e-4,1e-3,3e-3,1e-2,3e-2,1e-1,3e-1,1]) #Units in Volt + self.timecnst = np.array([1e-4,3e-4,1e-3,3e-3,1e-2,3e-2,1e-1,3e-1,1e-0,3e-0,1,3,1e2,3e2,1,1e3,3e3]) #Units in seconds + self.timecnstfilt = np.array([0,6,8,12,18,24]) #Units in dB/oct + self.sample_rates = np.array([62.5e-3,125e-3,250e-3,500e-3,1,2,4,8,16,32,64,128,256,512,0]) #Units in Hz + + @Feat() + def idn(self): + return self.query ('*IDN?') + + @Feat(units='degrees', limits=(-360, 360, 0.01)) + def reference_phase_shift(self): + """Phase shift of the reference. + """ + return self.query('PHAS?') + + @reference_phase_shift.setter + def reference_phase_shift(self, value): + self.send('PHAS{:.2f}'.format(value)) + + @Feat(values={True: '1', False: '0'}) + def reference_internal(self): + """Reference source. + """ + return self.query('FMOD?') + + @reference_internal.setter + def reference_internal(self, value): + self.send('FMOD {}'.format(value)) + + @Feat(units='Hz', limits=(5e4, 2e8)) + def frequency(self): + """Reference frequency. + """ + return self.query('FREQ?') + + @frequency.setter + def frequency(self, value): + self.send('FREQ{:.5f}'.format(value)) + + @Feat(values={True: '0', False: '1'}) + def harmonic(self): + """Detection harmonic. + (detect at F, i=0) or ON, (detect at 2F, i=1). + """ + return self.query('HARM?') + + @harmonic.setter + def harmonic(self, value): + self.send('HARM {}'.format(value)) + + # Signal input + @Feat(values={'High': '0', 'Normal': '1', 'Low': '2'}) + def wide_reserve_mode(self): + return self.query('WRSV ?') + + @wide_reserve_mode.setter + def wide_reserve_mode(self,value): + self.send('WRSV {}'.format(value)) + + # GAIN and TIME CONSTANT COMMANDS. + + @Feat(values={False: '0', True: '1'}) + def rel_mode(self): + """REL mode. + """ + return self.query('RMOD?') + + @rel_mode.setter + def rel_mode(self, value): + self.send('RMOD {}'.format(value)) + + @Feat(units='dB') + def filter_db_per_oct(self): + """Time constant. + """ + return self.timecnstfilt[int(self.query('OFSL?'))] + + @filter_db_per_oct.setter + def filter_db_per_oct(self, value): + #if value=0 then no filter is set + option = np.abs(self.timecnstfilt-value).argmin() + if (self.timecnstfilt==value).any(): + print('Value %s is not a option pick nearest' %value) + print('Nearest value is %s' %self.sens[option]) + #self.logger.warning('Value %s is not a option pick nearest' %value) + #self.logger.warning('Nearest value is %s' %self.sens[option]) + self.send('OFSL %s' %option) + + @Feat(units='V') + def sensitivity(self): + return self.sens[int(self.query('SENS ?'))] + + @sensitivity.setter + def sensitivity(self,value): + print(value) + option = np.abs(self.sens-value).argmin() + if not (self.sens==value).any(): + print('Value %s is not a option pick nearest' %value) + print('Nearest value is %s' %self.sens[option]) + #self.logger.warning('Value %s is not a option pick nearest' %value) + #self.logger.warning('Nearest value is %s' %self.sens[option]) + self.send('SENS %s' %option) + + @Feat(units='s') + def timeconstant(self): + return self.timecnst[int(self.query('OFLT ?'))] + + @timeconstant.setter + def timeconstant(self,value): + option = np.abs(self.timecnst-value).argmin() + if not (self.timecnst==value).any(): + print('Value %s is not a option pick nearest' %value) + print('Nearest value is %s' %self.timecnst[option]) + #self.logger.warning('Value %s is not a option pick nearest' %value) + #self.logger.warning('Nearest value is %s' %self.sens[option]) + self.send('OFLT %s' %option) + + @Feat(values={'High': 0, 'Normal': 1, 'Low':2}) + def close_reserve_mode(self): + return self.query('CRSV ?') + + @wide_reserve_mode.setter + def close_reserve_mode(self,value): + self.send('CRSV {}'.format(value)) + + ## DISPLAY and OUTPUT COMMANDS + + @DictFeat(keys={1, 2}, values=display_output) + def display(self, channel): + """Front panel output source. + """ + return self.query('DDEF? {}'.format(channel)) + + @display.setter + def display(self, channel, value): + self.send('DDEF {}, {}'.format(channel, value)) + + @DictFeat(keys={1, 2}, values=OrderedDict([('Display', 0), ('X', 1), ('Y', 1), ('X/Y', 1)])) + def front_output(self, channel): + """Front panel output source. + """ + return int(self.query('FPOP? {}'.format(channel))) + + @front_output.setter + def front_output(self, channel, value): + self.send('FPOP {}, {}'.format(channel, value)) + + # OEXP + + # AOFF is below. + + ## AUX INPUT and OUTPUT COMMANDS + + @DictFeat(keys={1, 2}, units='volt') + def analog_input(self, key): + """Input voltage in the auxiliary analog input. + """ + self.query('AOUX? {}'.format(key)) + + @DictFeat(None, keys={1, 2}, units='volt', limits=(-10.5, 10.5)) + def analog_output(self, key): + """Ouput voltage in the auxiliary analog output. + """ + self.query('AUXV? {}'.format(key)) + + @analog_output.setter + def analog_output(self, key, value): + self.query('AUXV {}, {}'.format(key, value)) + + + ## SETUP COMMANDS + + remote = Feat(None, values={True: 0, False: 1}) + + @remote.setter + def remote(self, value): + """Lock Front panel. + """ + self.query('OVRM {}'.format(value)) + + @Feat(values={True: '1', False: '0'}) + def key_click_enabled(self): + """Key click + """ + return self.query('KCLK?') + + @key_click_enabled.setter + def key_click_enabled(self, value): + return self.send('KCLK {}'.format(value)) + + @Feat(values={True: '1', False: '0'}) + def alarm_enabled(self): + """Key click + """ + return self.query('ALRM?') + + @alarm_enabled.setter + def alarm_enabled(self, value): + return self.send('ALRM {}'.format(value)) + + @Action(limits=(1, 9)) + def recall_state(self, location): + """Recalls instrument state in specified non-volatile location. + + :param location: non-volatile storage location. + """ + self.send('RSET {}'.format(location)) + + @Action(limits=(1, 9)) + def save_state(self, location): + """Saves instrument state in specified non-volatile location. + + Previously stored state in location is overwritten (no error is generated). + :param location: non-volatile storage location. + """ + self.send('SSET {}'.format(location)) + + + ## AUTO FUNCTIONS + + def wait_bit1(self): + pass + + @Action() + def auto_gain_async(self): + """Equivalent to press the Auto Gain key in the front panel. + Might take some time if the time constant is long. + Does nothing if the constant is greater than 1 second. + """ + self.send('AGAN') + + @Action() + def auto_gain(self): + self.auto_gain_async() + self.wait_bit1() + + @Action() + def auto_reserve_async(self): + """Equivalent to press the Auto Reserve key in the front panel. + Might take some time if the time constant is long. + """ + self.send('ARSV') + + @Action() + def auto_reserve(self): + self.auto_reserve_async() + self.wait_bit1() + + @Action() + def auto_phase_async(self): + """Equivalent to press the Auto Phase key in the front panel. + Might take some time if the time constant is long. + Does nothing if the phase is unstable. + """ + self.send('APHS') + + @Action() + def auto_phase(self): + self.auto_phase_async() + self.wait_bit1() + + @DictFeat(keys={1, 2}, values={'X': 0, 'Rv':1,'RdB':2,'y': 0}) + def auto_offset_async(self, channel_name): + """Automatically offset a given channel to zero. + Is equivalent to press the Auto Offset Key in the front panel. + + :param channel_name: the name of the channel. + """ + self.send('AOFF {}'.format(channel_name)) + + @Action() + def auto_offset(self): + self.auto_offset_async() + self.wait_bit1() + + + ## DATA STORAGE COMMANDS + + @Feat(units = 'Hz') + def sample_rate(self): + """Sample rate. + """ + return self.query('SRAT?') + + @sample_rate.setter + def sample_rate(self, value): + self.send('SRAT {}'.format(value)) + + @Feat(values={True: '0', False: '1'}) + def single_shot(self): + """End of buffer mode. + + If loop mode (single_shot = False), make sure to pause data storage + before reading the data to avoid confusion about which point is the + most recent. + """ + return self.query('SEND?') + + @single_shot.setter + def single_shot(self, value): + self.send('SEND {}'.format(value)) + + @Action() + def trigger(self): + """Software trigger. + """ + self.send('TRIG') + + @Feat(values={True: '0', False: '1'}) + def trigger_start_mode(self): + """The TSTR command sets or queries the Trigger Scan Mode to On (1) or Off (0). + When Trigger Scan Mode is On (i=1), an external or software trigger starts the + scan. This mode is only applicable for fixed data sample rates set by SRAT + 0-13.""" + self.query('TSTR?') + + @trigger_start_mode.setter + def trigger_start_mode(self, value): + self.send('TSTR {}'.format(value)) + + @Action() + def start_data_storage(self): + """Start or resume data storage + """ + self.send('STRT') + + @Action() + def pause_data_storage(self): + """Pause data storage + """ + self.send('PAUS') + + @Action() + def reset_data_storage(self): + """Reset data buffers. The command can be sent at any time - + any storage in progress, paused or not. will be reset. The command + will erase the data buffer. + """ + self.send('REST') + + + ## DATA TRANSFER COMMANDS + + @DictFeat(keys={'X', 'Y', 'Rv', 'RdB', 'Theta', 1, 2}) + def analog_value(self, key): + i = {'X': '1', 'Y': '2', 'Rv': '3', 'RdB': '4', 'Theta': '5'} + if key in i.keys(): + return self.query('OUTP? {}'.format(i[key])) + else: + return self.query('OUTR? {}'.format(key)) + + @Action() + def measure(self, channels): + """ The SNAP? command returns the values of up to six parameters at a single + instant. The SNAP? command requires at least two and at most six parameters + """ + if 2<=len(channels)<=6: + d = {'X': '1', 'Y': '2', 'Rv': '3', 'RdB': '4','Theta': '5', + 'AUX1': '6', 'AUX2': '7', 'RefFreq': '8', 'Ch1': '9', + 'Ch2': '10'} + channels = ','.join(d[ch] for ch in channels) + return self.query('SNAP? {}'.format(channels)) + else: + self.logger.error('Length {} is out of range(2,7)'.format(len(channels))) + + # OAUX See above + + @Feat() + def buffer_length(self): + return self.query('SPTS?') + + @Action() + def read_buffer(self, channel, start=0, length=None, format='A'): + """Queries points stored in the Channel buffer + + :param channel: Number of the channel (1, 2). + :param start: Index of the buffer to start. + :param length: Number of points to read. + Defaults to the number of points in the buffer. + :param format: Transfer format + 'a': ASCII (slow) + 'b': IEEE Binary (fast) - NOT IMPLEMENTED + 'c': Non-IEEE Binary (fastest) - NOT IMPLEMENTED + """ + + cmd = 'TRCA' + if not length: + length = self.buffer_length + self.send('{}? {},{},{}'.format(cmd, channel, start, length)) + if cmd == 'TRCA': + data = self.recv() + return np.fromstring(data, sep=',') * ureg.volt + else: + raise ValueError('{} transfer format is not implemented'.format(format)) + + # Fast + # STRD + +class SR844GPIB(_SR844, ): + + RECV_TERMINATION = '\r' + SEND_TERMINATION = '\n' + +class SR844Serial(_SR844, SerialDriver): + + RECV_TERMINATION = '\r' + SEND_TERMINATION = '\n' + + diff --git a/lantz/utils/qt_loaders.py b/lantz/utils/qt_loaders.py index 88c608e..61aa352 100644 --- a/lantz/utils/qt_loaders.py +++ b/lantz/utils/qt_loaders.py @@ -25,7 +25,8 @@ from distutils.version import LooseVersion # Available APIs. -QT_API_PYQT = 'pyqt' +QT_API_PYQT = 'pyqt5' +QT_API_PYQT4 = 'pyqt' QT_API_PYQTv1 = 'pyqtv1' QT_API_PYQT_DEFAULT = 'pyqtdefault' # don't set SIP explicitly QT_API_PYSIDE = 'pyside' @@ -83,7 +84,12 @@ def commit_api(api): if api == QT_API_PYSIDE: ID.forbid('PyQt4') + ID.forbid('PyQt5') + elif api in (QT_API_PYQT4, QT_API_PYQTv1): + ID.forbid('PyQt5') + ID.forbid('PySide') else: + ID.forbid('PyQt4') ID.forbid('PySide') @@ -99,16 +105,18 @@ def loaded_api(): """ if 'PyQt4.QtCore' in sys.modules: if qtapi_version() == 2: - return QT_API_PYQT + return QT_API_PYQT4 else: return QT_API_PYQTv1 + elif 'PyQt5.QtCore' in sys.modules: + return QT_API_PYQT elif 'PySide.QtCore' in sys.modules: return QT_API_PYSIDE return None def has_binding(api): - """Safely check for PyQt4 or PySide, without importing + """Safely check for PyQt[45] or PySide, without importing submodules Parameters @@ -128,9 +136,10 @@ def has_binding(api): return True module_name = {QT_API_PYSIDE: 'PySide', - QT_API_PYQT: 'PyQt4', + QT_API_PYQT: 'PyQt5', + QT_API_PYQT4: 'PyQt4', QT_API_PYQTv1: 'PyQt4', - QT_API_PYQT_DEFAULT: 'PyQt4'} + QT_API_PYQT_DEFAULT: 'PyQt5'} module_name = module_name[api] import importlib @@ -174,7 +183,7 @@ def can_import(api): current = loaded_api() if api == QT_API_PYQT_DEFAULT: - return current in [QT_API_PYQT, QT_API_PYQTv1, None] + return current in [QT_API_PYQT, None] else: return current in [api, None] @@ -215,6 +224,52 @@ def import_pyqt4(version=2): api = QT_API_PYQTv1 if version == 1 else QT_API_PYQT return QtCore, QtGui, QtSvg, api +def import_pyqt5(): + """ + Import PyQt5 + + ImportErrors raised within this function are non-recoverable + """ + + from PyQt5 import QtGui, QtCore, QtSvg, QtWidgets + + # Alias PyQt-specific functions for PySide compatibility. + QtCore.Signal = QtCore.pyqtSignal + QtCore.Slot = QtCore.pyqtSlot + + from PyQt5.uic import loadUi + QtGui.loadUi = loadUi + + class QtGuiWrapper: + def __init__(self, QtGui, QtWidgets): + self.__QtGui = QtGui + self.__QtWidgets = QtWidgets + + def __getattr__(self, name): + if name.startswith('_'): + return object.__getattr__(self, name) + + try: + return getattr(self.__QtWidgets, name) + except AttributeError: + return getattr(self.__QtWidgets, name) + + def __setattr__(self, name, value): + if name.startswith('_'): + return object.__setattr__(self, name, value) + + if hasattr(self.__QtWidgets, name): + setattr(self.__QtWidgets, name, value) + else: + setattr(self.__QtGui, name, value) + + def __hasattr__(self, name): + return hasattr(self.__QtWidgets, name) or hasattr(self.__QtGui, name) or object.__hasattr__(self, name) + + myQtGui = QtGuiWrapper(QtGui, QtWidgets) + + api = QT_API_PYQT + return QtCore, myQtGui, QtSvg, api def import_pyside(): """ @@ -347,9 +402,10 @@ def load_qt(api_options): an incompatible library has already been installed) """ loaders = {QT_API_PYSIDE: import_pyside, - QT_API_PYQT: import_pyqt4, + QT_API_PYQT: import_pyqt5, + QT_API_PYQT4: import_pyqt4, QT_API_PYQTv1: partial(import_pyqt4, version=1), - QT_API_PYQT_DEFAULT: partial(import_pyqt4, version=None), + QT_API_PYQT_DEFAULT: import_pyqt5, QT_MOCK: import_qtmock } diff --git a/setup.py b/setup.py index 2091f50..daa2536 100644 --- a/setup.py +++ b/setup.py @@ -47,7 +47,7 @@ def read(filename): setup(name='Lantz', - version='0.4.dev0', + version='0.3.1b1+qt5', license='BSD', description='Instrumentation framework', long_description=long_description,