diff --git a/python/src/instruments/srs/srs830.py b/python/src/instruments/srs/srs830.py index 511ca1a..7cded8b 100644 --- a/python/src/instruments/srs/srs830.py +++ b/python/src/instruments/srs/srs830.py @@ -1,141 +1,179 @@ #!/usr/bin/python -# Filename: srs830.py +# -*- coding: utf-8 -*- +## +# srs830.py: Driver for the SRS830 lock-in amplifier. +## +# © 2013 Steven Casagrande (scasagrande@galvant.ca). +# +# This file is a part of the GPIBUSB adapter project. +# Licensed under the AGPL version 3. +## +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +## -# Original author: Steven Casagrande (stevencasagrande@gmail.com) -# 2012 +## FEATURES #################################################################### -# This work is released under the Creative Commons Attribution-Sharealike 3.0 license. -# See http://creativecommons.org/licenses/by-sa/3.0/ or the included license/LICENSE.TXT file for more information. +from __future__ import division -# Attribution requirements can be found in license/ATTRIBUTION.TXT +## IMPORTS ##################################################################### -from instruments.abstract_instruments import Instrument import math import time -class SRS830(Instrument): - def __init__(self, port, address,timeout_length): - super(SRS830, self).__init__(self,port,address,timeout_length) - self.write('OUTX 1') # Set the device responce port to GPIB +from flufl.enum import Enum +from flufl.enum._enum import EnumValue +import quantities as pq + +from instruments.generic_scpi import SCPIInstrument +import instruments.abstract_instruments.gi_gpib as gw +import instruments.abstract_instruments.serialwrapper as sw +from instruments.util_fns import assume_units + +## ENUMS ####################################################################### + +class SRS830FreqSource(Enum): + external = 0 + internal = 1 - # Set Frequency Source - def setFreqSource(self,source): - ''' - Function sets teh frequency source to either the internal - reference clock, or an external reference. +class SRS830InputShield(Enum): + floating = 0 + grounded = 1 - source: {EXTernal|INTernal},string - ''' - if not isinstance(source,str): - raise Exception('Parameter must be a string.') - - source = source.lower() - - valid1 = ['ext','int'] - valid2 = ['external','internal'] - if source in valid1: - source = valid1.index(source) - elif source in valid2: - source = valid2.index(source) +class SRS830Coupling(Enum): + ac = 0 + dc = 1 + +class SRS830BufferMode(Enum): + one_shot = 0 + loop = 1 + +## CONSTANTS ################################################################### + +VALID_SAMPLE_RATES = [2.0**n for n in xrange(-4, 10)] + + +## CLASSES ##################################################################### + +class SRS830(SCPIInstrument): + def __init__(self, port, address, timeout_length, outx_mode=None): + super(SRS830, self).__init__(self,port,address,timeout_length) + if outx_mode is 1: + self.sendcmd('OUTX 1') + elif outx_mode is 2: + self.sendcmd('OUTX 2') else: - raise Exception('Only "external" and "internal" are valid freq sources.') - - self.write( 'FREQ ' + str(source) ) + if isinstance(self._file, gw.GPIBWarapper): + self.sendcmd('OUTX 1') + elif isinstance(self._file, sw.SerialWrapper): + self.sendcmd('OUTX 2') + else: + print 'OUTX command has not been set. Instrument behavour is '\ + 'unknown.' - # Set Frequency - def setFreq(self,freq): - ''' - Function sers the internal reference frequency. This command only - works if the lock-in is set to use the internal reference. - - freq: Desired frequency. Rounded to 5 digits or 0.0001Hz, whichever is larger. - freq: ,float - ''' - if not isinstance(freq,int) and not isinstance(freq,float): - raise Exception('Freq parameter must be an integer or a float.') - - # Ensure that lock-in is not set to external freq ref - if( int(self.query('FMOD?')) == 0 ): - raise Exception('SRS830 set to external freq source, cannot change internal freq.') - - self.write( 'FREQ ' + str(freq) ) + ## PROPERTIES ## - # Set Phase - def setPhase(self,phase): + @property + def freq_source(self): + return SRS830FreqSource[self.query('FMOD?')] + @freq_source.setter + def freq_source(self, newval): + if not isinstance(newval, EnumValue) or + (newval.enum is not SRS830FreqSource): + raise TypeError("Frequency source setting must be a " + "SRS830FreqSource value, got {} " + "instead.".format(type(newval))) + self.sendcmd('FMOD {}'.format(newval.value)) + + @property + def freq(self): + return pq.Quantity(float(self.query('FREQ?')),pq.hertz) + @freq.setter + def freq(self, newval): + newval = float(assume_units(newval, pq.Hz).rescale(pq.Hz).magnitude) + + self.sendcmd('FREQ {}'.format(newval)) + + @property + def phase(self): ''' Function sets the phase of the internal reference signal. phase: Desired phase phase = <-360...+729.99>,float ''' - if not isinstance(phase,int) and not isinstance(phase,float): - raise Exception('Phase parameter must be an integer or a float.') - - if ( (phase >= 730) or (phase < -360) ): - raise Exception('Phase must be -360 <= phase < +730 .') - - self.write( 'PHAS ' + str(phase) ) + return pq.Quantity(float(self.query('PHAS?')), pq.degrees) + phase.setter + def phase(self, newval): + newval = float(assume_units(newval, pq.degree) + .rescale(pq.degree).magnitude) + if (newval >= 730) or (newval <- 360): + raise ValueError('Phase must be -360 <= phase < +730') + self.sendcmd('PHAS {}'.format(newval)) - # Set Amplitude - def setAmplitude(self,amplitude): + @property + def amplitude(self): ''' Function sets the amplitude of the internal reference signal. amplitude: Desired peak-to-peak voltage amplitude = <0.004...5>,float ''' - if not isinstance(amplitude,int) and not isinstance(amplitude,float): - raise Exception('Amplitude parameter must be an integer or a float.') - - if ( (amplitude > 5) or (amplitude < 0.004) ): - raise Exception('Amplitude must be +0.004 <= amplitude <= +5 .') - - self.write( 'SLVL ' + str(amplitude) ) - - # Set Input Shield Grounding - def setInputGround(self,grounding): + return pq.Quantity(float(self.query('SLVL?')), pq.volt) + @amplitude.setter + def amplitude(self, newval): + newval = float(assume_units(newval, pq.volt).rescale(pq.volt).magnitude) + if ((newval > 5) or (newval < 0.004)): + raise ValueError('Amplitude must be +0.004 <= amplitude <= +5 .') + self.sendcmd('SLVL {}'.format(newval)) + + @property + def input_shield_grounding(self): ''' Function sets the input shield grounding to either 'float' or 'ground' grounding: Desired input shield grounding grounding = {float|ground},string ''' - if not isinstance(grounding,str): - raise Exception('Parameter "grounding" must be a string.') - - grounding = grounding.lower() - - valid = ['float','ground'] - if grounding in valid: - grounding = str( valid.index(grounding) ) - else: - raise Exception('Only "float" and "ground" are valid grounding input ground settings.') - - self.write( 'IGND ' + grounding ) + return SRS830InputShield[self.query('IGND?')] + @input_shield_grounding.setter + def input_shield_grounding(self, newval): + if not isinstance(newval, EnumValue) or + (newval.enum is not SRS830InputShield): + raise TypeError("Input shield grounding setting must be a " + "SRS830InputShield value, got {} " + "instead.".format(type(newval))) + self.sendcmd('IGND {}'.format(newval.value)) - # Set Input Coupling - def setInputCoupling(self,coupling): + @property + def coupling(self): ''' Function sets the input coupling to either 'ac' or 'dc' coupling: Desired input coupling mode coupling = {ac|dc},string ''' - if not isinstance(coupling,str): - raise Exception('Parameter "coupling" must be a string.') - - coupling = coupling.lower() - - valid = ['ac','dc'] - if coupling in valid: - coupling = str( valid.index(coupling) ) - else: - raise Exception('Only "ac" and "dc" are valid input coupling settings.') + return SRS830Coupling[self.query('ICPL?')] + @coupling.setter + def coupling(self, newval): + if not isinstance(newval, EnumValue) or + (newval.enum is not SRS830Coupling): + raise TypeError("Input coupling setting must be a " + "SRS830Coupling value, got {} " + "instead.".format(type(newval))) + self.sendcmd('ICPL {}'.format(newval.value)) - self.write( 'ICPL ' + coupling ) - - # Set Data Sample Rate - def setSampleRate(self,sampleRate): + @property + def sample_rate(self): ''' Function sets the data sampling rate of the lock-in @@ -144,99 +182,123 @@ def setSampleRate(self,sampleRate): This means 2^n, n={-4...+9} sampleRate = {,TRIGGER} ''' - if isinstance(sampleRate,str): - sampleRate = sampleRate.lower() - - valid = [0.0625,0.125,0.25,0.5,1,2,4,8,16,32,64,128,256,512,'trigger'] - if sampleRate in valid: - sampleRate = str( valid.index(sampleRate) ) - else: - raise Exception('Data sample rate parameter can only be 2^n, n={-4..+9} or "trigger".') + return pq.Quantity(float(self.query('SRAT?')), pq.Hz) + @sample_rate.setter + def sample_rate(self, newval): + if isinstance(newval, str): + newval = newval.lower() + if newval == 'trigger': + self.sendcmd('SRAT 14') - self.write( 'SRAT ' + sampleRate ) + if newval in VALID_SAMPLE_RATES: + self.sendcmd('SRAT {}'.format(VALID_SAMPLE_RATES.index(newval))) + else: + raise ValueError('Valid samples rates given by {} and "trigger".' + .format(VALID_SAMPLE_RATES)) - # Set End of Buffer Mode - def setEndOfBufferMode(self,mode): + @property + def buffer_mode(self): ''' Function sets the end of buffer mode mode: Desired end of buffer mode mode = {1SHOT,LOOP},string ''' + return SRS830BufferMode[self.query('SEND?')] + @buffer_mode.setter + def buffer_mode(self, newval): + if not isinstance(newval, EnumValue) or + (newval.enum is not SRS830BufferMode): + raise TypeError("Input coupling setting must be a " + "SRS830BufferMode value, got {} " + "instead.".format(type(newval))) + self.sendcmd('SEND {}'.format(newval.value)) + + @property + def num_data_points(self): + ''' + Function checks number of data sets in SRS830 buffer. + Returns an integer. + ''' + return int( self.query('SPTS?') ) + + @property + def data_transfer(self): + ''' + % Function used to turn the data transfer from the lockin on or off + % + % mode: + % mode = {ON|OFF},string + ''' + return int(self.query('FAST?')) == 2 + @data_transfer.setter + def data_transfer(self, newval): + self.sendcmd('FAST {}'.format(2 if newval else 0)) + + ## AUTO- METHODS ## + + def auto_offset(self,mode): + ''' + % Function sets a specific channel mode to auto offset. This is the + % same as pressing the auto offset key on the display. + % It sets the offset of the mode specified to zero. + % + % mode: Mode who's offset will be set to zero. + % mode = {X|Y|R},string + ''' if not isinstance(mode,str): raise Exception('Parameter "mode" must be a string.') mode = mode.lower() - valid = ['1shot','loop'] + valid = ['x','y','r'] if mode in valid: - mode = str( valid.index(mode) ) + mode = str( valid.index(mode) + 1 ) else: - raise Exception('Only "1shot" and "loop" are valid end of buffer modes.') + raise Exception('Only "x" , "y" and "r" are valid modes ' + 'for setting the auto offset.') - self.write( 'SEND ' + mode ) + self.write( 'AOFF ' + mode ) - # Set Channel Display - def setChannelDisplay(self,channel,display,ratio): + def auto_phase(self): ''' - % Function sets the display of the two channels. - % Channel 1 can display X, R, X Noise, Aux In 1, Aux In 2 - % Channel 2 can display Y, Theta, Y Noise, Aux In 3, Aux In 4 - % - % Channel 1 can have ratio of None, Aux In 1, Aux In 2 - % Channel 2 can have ratio of None, Aux In 3, Aux In 4 - % - % channel = {CH1|CH2|1|2},string/int - % display = {X|Y|R|THETA|XNOISE|YNOISE|AUX1|AUX2|AUX3|AUX4},string - % ratio = {NONE|AUX1|AUX2|AUX3|AUX4},string + % Function sets the lock-in to auto phase. + % This does the same thing as pushing the auto phase button. + % Do not send this message again without waiting the correct amount + % of time for the lock-in to finish. ''' - if not isinstance(channel,str) and not isinstance(channel,int): - raise Exception('Parameter "channel" must be a string or integer.') - if not isinstance(display,str): - raise Exception('Parameter "display" must be a string.') - if not isinstance(ratio,str): - raise Exception('Parameter "ratio" must be a string.') - - if type(channel) == type(str()): - channel = channel.lower() - display = display.lower() - ratio = ratio.lower() - - if channel == 'ch1' or channel == '1' or channel == 1: - channel = '1' - valid = ['x','r','xnoise','aux1','aux2'] - if display in valid: - display = str( valid.index(display) ) - else: - raise Exception('Only "x" , "r" , "xnoise" , "aux1" and "aux2" are valid displays for channel 1.') - - valid = ['none','aux1','aux2'] - if ratio in valid: - ratio = str( valid.index(ratio) ) - else: - raise Exception('Only "none" , "aux1" and "aux2" are valid ratios for channel 1.') + self.write('APHS') - elif channel == 'ch2' or channel == '2' or channel == 2: - channel = '2' - valid = ['y','theta','ynoise','aux3','aux4'] - if display in valid: - display = str( valid.index(display) ) - else: - raise Exception('Only "y" , "theta" , "ynoise" , "aux3" and "aux4" are valid displays for channel 2.') - - valid = ['none','aux3','aux4'] - if ratio in valid: - ratio = str( valid.index(ratio) ) - else: - raise Exception('Only "none" , "aux3" and "aux4" are valid ratios for channel 2.') + ## META-METHODS ## + + def init(self, sample_rate, buffer_mode): + ''' + Wrapper function to prepare the srs830 for measurement. + Sets both the data sampling rate and the end of buffer mode - else: - raise Exception('Only "ch1" and "ch2" are valid channels.') + sampleRate: The sampling rate in Hz, or the string "trigger". + When specifing the rate in Hz, acceptable values are integer + powers of 2. This means 2^n, n={-4...+9}. + sampleRate = {|TRIGGER} - self.write( 'DDEF %s,%s,%s' % (channel,display,ratio) ) + mode = {1SHOT|LOOP},string + ''' + self.clear_data_buffer() + self.sample_rate = sample_rate + self.buffer_mode = buffer_mode - # Set the Channel Offset and Expand - def setOffsetExpand(self,mode,offset,expand): + def start_data_transfer(self): + ''' + Wrapper function to start the actual data transfer. + Sets the transfer mode to FAST2, and triggers the data transfer + to start after a delay of 0.5 seconds. + ''' + self.data_transfer('on') # FIXME + self.start_scan() + + ## OTHER METHODS ## + + def set_offset_expand(self,mode,offset,expand): ''' % Function sets the channel offset and expand parameters. % Offset is a percentage, and expand is given as a multiplication @@ -261,7 +323,8 @@ def setOffsetExpand(self,mode,offset,expand): if mode in valid: mode = valid.index(mode) + 1 else: - raise Exception('Only "x" , "y" and "r" are valid modes for setting the offset & expand.') + raise Exception('Only "x" , "y" and "r" are valid modes for ' + 'setting the offset & expand.') if type(offset) != type(int()) or type(offset) != type(float()): raise Exception('Offset parameter must be an integer or a float.') @@ -279,105 +342,23 @@ def setOffsetExpand(self,mode,offset,expand): self.write( 'OEXP %s,%s,%s' % (mode,offset,expand) ) - # Enable Auto Offset - def autoOffset(self,mode): - ''' - % Function sets a specific channel mode to auto offset. This is the - % same as pressing the auto offset key on the display. - % It sets the offset of the mode specified to zero. - % - % mode: Mode who's offset will be set to zero. - % mode = {X|Y|R},string - ''' - if not isinstance(mode,str): - raise Exception('Parameter "mode" must be a string.') - - mode = mode.lower() - - valid = ['x','y','r'] - if mode in valid: - mode = str( valid.index(mode) + 1 ) - else: - raise Exception('Only "x" , "y" and "r" are valid modes for setting the auto offset.') - - self.write( 'AOFF ' + mode ) - - # Enable Auto Phase - def autoPhase(self): - ''' - % Function sets the lock-in to auto phase. - % This does the same thing as pushing the auto phase button. - % Do not send this message again without waiting the correct amount - % of time for the lock-in to finish. - ''' - self.write('APHS') - - # Set Data Transfer on/off - def dataTransfer(self,mode): - ''' - % Function used to turn the data transfer from the lockin on or off - % - % mode: - % mode = {ON|OFF},string - ''' - if not isinstance(mode,str): - raise Exception('Parameter "mode" must be a string.') - - mode = mode.lower() - - if mode == 'off': - mode = '0' - elif mode == 'on': - mode = '2' - else: - raise Exception('Only "on" and "off" are valid parameters for setDataTransfer.') - - self.write( 'FAST ' + mode ) - # Start Scan - def startScan(self): + + def start_scan(self): ''' % After setting the data transfer on via the dataTransfer function, % this is used to start the scan. The scan starts after a delay of % 0.5 seconds. ''' self.write('STRD') - - # Pause Data Capture + def pause(self): ''' Has the instrument pause data capture. ''' self.write('PAUS') - - # Start Data Transfer (wrapper) - def init(self,sampleRate,EoBMode): - ''' - Wrapper function to prepare the srs830 for measurement. - Sets both the data sampling rate and the end of buffer mode - - sampleRate: The sampling rate in Hz, or the string "trigger". - When specifing the rate in Hz, acceptable values are integer - powers of 2. This means 2^n, n={-4...+9}. - sampleRate = {|TRIGGER} - - mode = {1SHOT|LOOP},string - ''' - self.clearDataBuffer() - self.setSampleRate(sampleRate) - self.setEndOfBufferMode(EoBMode) - - # Take Data Snapshot (wrapper) - def startDataTransfer(self): - ''' - Wrapper function to start the actual data transfer. - Sets the transfer mode to FAST2, and triggers the data transfer - to start after a delay of 0.5 seconds. - ''' - self.dataTransfer('on') - self.startScan() - - def dataSnap(self,mode1,mode2): + + def data_snap(self,mode1,mode2): ''' Function takes a snapshot of the current parameters are defined by variables mode1 and mode2. @@ -415,8 +396,7 @@ def dataSnap(self,mode1,mode2): result = self.query( 'SNAP? %s,%s' % (mode1,mode2) ) return map( float, result.split(',') ) - # Read Data Buffer - def readDataBuffer(self,channel): + def read_data_buffer(self,channel): ''' Function reads the entire data buffer for a specific channel. Transfer is done in ASCII mode. Although binary would be faster, @@ -447,46 +427,89 @@ def readDataBuffer(self,channel): # calling method return map( float, self.query( 'TRCA?%s,0,%s' % (channel,N) ).split(',') ) - # Check number of data sets in buffer - def numDataPoints(self): - ''' - Function checks number of data sets in SRS830 buffer. - Returns an integer. - ''' - return int( self.query('SPTS?') ) - - # Clear data (channel) buffer - def clearDataBuffer(self): + def clear_data_buffer(self): ''' Clears the data buffer of the SRS830. ''' self.write('REST') - # Take measurement (wrapper function) - def takeMeasurement(self,sampleRate,numSamples): - numSamples = float( numSamples ) + def take_measurement(self, sample_rate, num_samples): + numSamples = float(num_samples) if numSamples > 16383: - raise Exception('Number of samples cannot exceed 16383.') + raise ValueError('Number of samples cannot exceed 16383.') - sampleTime = math.ceil( numSamples/sampleRate ) + sample_time = math.ceil( num_samples/sample_rate ) - self.init(sampleRate,'1shot') - self.startDataTransfer() + self.init(sample_rate, SRS830BufferMode['one_shot']) + self.start_data_transfer() - print 'Sampling will take ' + sampleTime + ' seconds.' - time.sleep(sampleTime) + print 'Sampling will take ' + sample_time + ' seconds.' + time.sleep(sample_time) self.pause() - print 'Sampling complete, reading channel 1.' - ch1 = self.readDataBuffer('ch1') - - print 'Reading channel 2.' - ch2 = self.readDataBuffer('ch2') + ch1 = self.read_data_buffer('ch1') + ch2 = self.read_data_buffer('ch2') return [ch1,ch2] + + def set_channel_display(self,channel,display,ratio): + ''' + % Function sets the display of the two channels. + % Channel 1 can display X, R, X Noise, Aux In 1, Aux In 2 + % Channel 2 can display Y, Theta, Y Noise, Aux In 3, Aux In 4 + % + % Channel 1 can have ratio of None, Aux In 1, Aux In 2 + % Channel 2 can have ratio of None, Aux In 3, Aux In 4 + % + % channel = {CH1|CH2|1|2},string/int + % display = {X|Y|R|THETA|XNOISE|YNOISE|AUX1|AUX2|AUX3|AUX4},string + % ratio = {NONE|AUX1|AUX2|AUX3|AUX4},string + ''' + if not isinstance(channel,str) and not isinstance(channel,int): + raise Exception('Parameter "channel" must be a string or integer.') + if not isinstance(display,str): + raise Exception('Parameter "display" must be a string.') + if not isinstance(ratio,str): + raise Exception('Parameter "ratio" must be a string.') + + if type(channel) == type(str()): + channel = channel.lower() + display = display.lower() + ratio = ratio.lower() + + if channel == 'ch1' or channel == '1' or channel == 1: + channel = '1' + valid = ['x','r','xnoise','aux1','aux2'] + if display in valid: + display = str( valid.index(display) ) + else: + raise Exception('Only "x" , "r" , "xnoise" , "aux1" and "aux2" are valid displays for channel 1.') - + valid = ['none','aux1','aux2'] + if ratio in valid: + ratio = str( valid.index(ratio) ) + else: + raise Exception('Only "none" , "aux1" and "aux2" are valid ratios for channel 1.') + + elif channel == 'ch2' or channel == '2' or channel == 2: + channel = '2' + valid = ['y','theta','ynoise','aux3','aux4'] + if display in valid: + display = str( valid.index(display) ) + else: + raise Exception('Only "y" , "theta" , "ynoise" , "aux3" and "aux4" are valid displays for channel 2.') + + valid = ['none','aux3','aux4'] + if ratio in valid: + ratio = str( valid.index(ratio) ) + else: + raise Exception('Only "none" , "aux3" and "aux4" are valid ratios for channel 2.') + + else: + raise Exception('Only "ch1" and "ch2" are valid channels.') + + self.write( 'DDEF %s,%s,%s' % (channel,display,ratio) )