Source code for labdrivers.srs.sr830

import logging

import visa

# create a logger object for this module
logger = logging.getLogger(__name__)
# added so that log messages show up in Jupyter notebooks
logger.addHandler(logging.StreamHandler())


[docs]class Sr830: """Interface to a Stanford Research Systems 830 lock in amplifier.""" def __init__(self, gpib_addr): """Create an instance of the Sr830 object. :param gpib_addr: GPIB address of the SR830 """ try: # the pyvisa manager we'll use to connect to the GPIB resources self.resource_manager = visa.ResourceManager() except OSError: logger.exception("\n\tCould not find the VISA library. Is the VISA driver installed?\n\n") self._gpib_addr = gpib_addr self._instrument = None self._instrument = self.resource_manager.open_resource("GPIB::%d" % self._gpib_addr) @property def sync_filter(self): """ The state of the sync filter (< 200 Hz). """ return self._instrument.query_ascii_values('SYNC?')[0] @sync_filter.setter def sync_filter(self, value): if isinstance(value, bool): self._instrument.query_ascii_values('SYNC {}'.format(int(value))) else: raise RuntimeError('Sync filter input expects [True|False].') @property def low_pass_filter_slope(self): """ The low pass filter slope in units of dB/octave. The choices are: i slope(dB/oct) --- ------------- 0 6 1 12 2 18 3 24 """ response = self._instrument.query_ascii_values('OSFL?')[0] slope = {'0': '6 dB/oct', '1': '12 dB/oct', '2': '18 dB/oct', '3': '24 dB/oct'} return slope[response] @low_pass_filter_slope.setter def low_pass_filter_slope(self, value): """ Sets the low pass filter slope. :param value: The slope in units of dB/oct. """ if value in (6, 12, 18, 24): slope = {6: '0', 12: '1', 18: '2', 24: '3'} self._instrument.query_ascii_values('OSFL {}'.format(slope[value])) else: raise RuntimeError('Low pass filter slope only accepts [6|12|18|24].') @property def reserve(self): """ The reserve mode of the SR830. """ reserve = {'0': 'high', '1': 'normal', '2': 'low noise'} response = self._instrument.query_ascii_values('RMOD?')[0] return reserve[response] @reserve.setter def reserve(self, value): if isinstance(value, str): mode = value.lower() elif isinstance(value, int): mode = value else: raise RuntimeError('Reserve expects a string or integer argument.') modes_dict = {'hi': 0, 'high': 0, 'high reserve': 0, 0: 0, 'normal': 1, 1: 1, 'lo': 2, 'low': 2, 'low noise': 2, 2: 2} if mode in modes_dict.keys(): self._instrument.query_ascii_values('RMOD {}'.format(mode)) else: raise RuntimeError('Incorrect key for reserve.') @property def frequency(self): """ The frequency of the output signal. """ return self._instrument.query_ascii_values('FREQ?')[0] @frequency.setter def frequency(self, value): if 0.001 <= value <= 102000: self._instrument.write("FREQ {}".format(value)) else: raise RuntimeError('Valid frequencies are between 0.001 Hz and 102 kHz.') # INPUT and FILTER @property def input(self): """ The input on the SR830 machine. Possible values: 0: A 1: A-B 2: I (1 MOhm) 3: I (100 MOhm) """ return self._instrument.query_ascii_values('ISRC?')[0] @input.setter def input(self, input_value): input_ = {'0': 0, 0: 0, 'A': 0, '1': 1, 1: 1, 'A-B': 1, 'DIFFERENTIAL': 1, '2': 2, 2: 2, 'I1': 2, 'I1M': 2, 'I1MOHM': 2, '3': 3, 3: 3, 'I100': 3, 'I100M': 3, 'I100MOHM': 3} if isinstance(input_value, str): query = input_value.upper().replace('(', '').replace(')', '').replace(' ', '') else: query = input_value if query in input_.keys(): command = input_[query] self._instrument.write("ISRC {}".format(command)) else: raise RuntimeError('Unexpected input for SR830 input command.') @property def input_shield_grounding(self): """Tells whether the shield is floating or grounded.""" response = self._instrument.query_ascii_values("IGND?")[0] return {'0': 'Float', '1': 'Ground'}[response] @input_shield_grounding.setter def input_shield_grounding(self, ground_type): ground_types = {'float': '0', 'floating': '0', '0': '0', 'ground': '1', 'grounded': '1', '1': '1'} if ground_type.lower() in ground_types.keys(): self._instrument.write("IGND {}".format(ground_type.lower())) else: raise RuntimeError('Improper input grounding shield type.') @property def phase(self): """ The phase of the output relative to the input. """ return self._instrument.query_ascii_values('PHAS?')[0] @phase.setter def phase(self, value): if (isinstance(value, float) or isinstance(value, int)) and -360.0 <= value <= 729.99: self._instrument.write("PHAS {}".format(value)) else: raise RuntimeError('Given phase is out of range for the SR830. Should be between -360.0 and 729.99.') @property def amplitude(self): """ The amplitude of the voltage output. """ return self._instrument.query_ascii_values('SLVL?')[0] @amplitude.setter def amplitude(self, value): if 0.004 <= value <= 5.0: self._instrument.write("SLVL {}".format(value)) else: raise RuntimeError('Given amplitude is out of range. Expected 0.004 to 5.0 V.') @property def time_constant(self): """ The time constant of the SR830. """ time_constant = {0: '10 us', 10: '1 s', 1: '30 us', 11: '3 s', 2: '100 us', 12: '10 s', 3: '300 us', 13: '30 s', 4: '1 ms', 14: '100 s', 5: '3 ms', 15: '300 s', 6: '10 ms', 16: '1 ks', 7: '30 ms', 17: '3 ks', 8: '100 ms', 18: '10 ks', 9: '300 ms', 19: '30 ks'} const_index = self._instrument.query_ascii_values('OFLT?')[0] return time_constant[const_index] @time_constant.setter def time_constant(self, value): if value.lower() == 'increment': if self.time_constant + 1 <= 19: self.time_constant += 1 elif value.lower() == 'decrement': if self.time_constant - 1 >= 0: self.time_constant -= 1 elif 0 <= value <= 19: self._instrument.write("SENS {}".format(value)) else: raise RuntimeError('Time constant index must be between 0 and 19 (inclusive).') @property def sensitivity(self): """Voltage/current sensitivity for inputs.""" sensitivity = {0: "2 nV/fA", 13: "50 uV/pA", 1: "5 nV/fA", 14: "100 uV/pA", 2: "10 nV/fA", 15: "200 uV/pA", 3: "20 nV/fA", 16: "500 uV/pA", 4: "50 nV/fA", 17: "1 mV/nA", 5: "100 nV/fA", 18: "2 mV/nA", 6: "200 nV/fA", 19: "5 mV/nA", 7: "500 nV/fA", 20: "10 mV/nA", 8: "1 uV/pA", 21: "20 mV/nA", 9: "2 uV/pA", 22: "50 mV/nA", 10: "5 uV/pA", 23: "100 mV/nA", 11: "10 uV/pA", 24: "200 mV/nA", 12: "20 uV/pA", 25: "500 mV/nA", 26: "1 V/uA"} sens_index = self._instrument.query_ascii_values('SENS?')[0] return sensitivity[sens_index] @sensitivity.setter def sensitivity(self, value): if isinstance(value, int) and 0 <= value <= 26: self._instrument.write("SENS {}".format(value)) else: raise RuntimeError("Invalid input for sensitivity.")
[docs] def set_display(self, channel, display, ratio=0): """Set the display of the amplifier. Display options are: (for channel 1) (for channel 2) 0: X 0: Y 1: R 1: Theta 2: X Noise 2: Y Noise 3: Aux in 1 3: Aux in 3 4: Aux in 2 4: Aux in 4 Ratio options are (i.e. divide output by): 0: none 0: none 1: Aux in 1 1: Aux in 3 2: Aux in 2 2: Aux in 4 Args: channel (int): which channel to modify (1 or 2) display (int): what to display ratio (int, optional): display the output as a ratio """ self._instrument.write("DDEF {}, {}, {}".format(channel, display, ratio))
[docs] def get_display(self, channel): """Get the display configuration of the amplifier. Display options are: (for channel 1) (for channel 2) 0: X 0: Y 1: R 1: Theta 2: X Noise 2: Y Noise 3: Aux in 1 3: Aux in 3 4: Aux in 2 4: Aux in 4 Args: channel (int): which channel to return the configuration for Returns: int: the parameter being displayed by the amplifier """ return self._instrument.query_ascii_values("DDEF? {}".format(channel))
[docs] def single_output(self, value): """Get the current value of a single parameter. Possible parameter values are: 1: X 2: Y 3: R 4: Theta Returns: float: the value of the specified parameter """ return self._instrument.query_ascii_values("OUTP? {}".format(value))[0]
[docs] def multiple_output(self, *values): """Queries the SR830 for multiple output. See below for possibilities. Possible parameters are: 1: X 2: Y 3: R 4: Theta 5: Aux in 1 6: Aux in 2 7: Aux in 3 8: Aux in 4 9: Reference frequency 10: CH1 display 11: CH2 display :param values: A variable number of arguments to obtain output :return: """ command_string = "SNAP?" + " {}," * len(values) return self._instrument.query_ascii_values(command_string.format(*values))
[docs] def auto_gain(self): """ Mimics pressing the Auto Gain button. Does nothing if the time constant is more than 1 second. """ self._instrument.query_ascii_values("AGAN")
[docs] def auto_reserve(self): """ Mimics pressing the Auto Reserve button. """ self._instrument.query_ascii_values("ARSV")
[docs] def auto_phase(self): """ Mimics pressing the Auto Phase button. """ self._instrument.query_ascii_values("APHS")
[docs] def auto_offset(self, parameter): """ Automatically offsets the given voltage parameter. :param parameter: A string from ['x'|'y'|'r'], case insensitive. """ self._instrument.query_ascii_values("AOFF {}".format(parameter.upper()))
# Data storage commands @property def data_sample_rate(self): """Data sample rate, which can be 62.5 mHz, 512 Hz, or Trigger. Expected strings: 62.5, 62.5 mhz, 62.5mhz, mhz, 0, 512, 512hz, 512 hz, hz, 13, trig, trigger, 14.""" rate_dict = {'0': '62.5 mHz', '13': '512 Hz', '14': 'Trigger'} response = self._instrument.query_ascii_values("SRAT?")[0] return rate_dict[response] @data_sample_rate.setter def data_sample_rate(self, rate): rate_dict = {'62.5': '0', '0': '0', '62.5mhz': '0', 'mhz': '0', '512': '13', '13': '13', '512hz': '13', 'hz': '13', 'trig': '14', '14': '14', 'trigger': '14'} rate_value = str(rate).lower().replace(' ', '') if rate_value in rate_dict.keys(): self._instrument.write("SRAT {}".format(rate_value)) else: raise RuntimeError('Sample rate input not recognized.') @property def data_scan_mode(self): """Data scan mode, which is either a 1-shot or a loop. Expected strings: 1-shot, 1 shot, 1shot, loop.""" scan_modes = {'0': '1-shot', '1': 'loop'} response = self._instrument.query_ascii_values("SEND?")[0] return scan_modes[response] @data_scan_mode.setter def data_scan_mode(self, scan_mode): scan_modes = {'1shot': '0', 'loop': '1'} mode = scan_mode.replace('-', '').replace(' ', '') self._instrument.write("SEND {}".format(scan_modes[mode])) @property def trigger_starts_scan(self): """Determines if a Trigger starts scan mode.""" response = self._instrument.query_ascii_values("TSTR?")[0] return {'0': False, '1': True}[response] @trigger_starts_scan.setter def trigger_starts_scan(self, starts): starts_value = int(bool(starts)) self._instrument.write("TSTR {}".format(starts_value))
[docs] def trigger(self): """Sends a software trigger.""" self._instrument.write("TRIG")
[docs] def start_scan(self): """Starts or continues a scan.""" self._instrument.write("STRT")
[docs] def pause_scan(self): """Pauses a scan.""" self._instrument.write("PAUS")
[docs] def reset_scan(self): """Resets a scan and releases all stored data.""" self._instrument.write("REST")