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