Source code for qcodes.instrument_drivers.stanford_research.SR830

from __future__ import annotations

import time
from functools import partial
from typing import TYPE_CHECKING, Any, ClassVar

import numpy as np

from qcodes.instrument import VisaInstrument, VisaInstrumentKWArgs
from qcodes.parameters import (
    ArrayParameter,
    Parameter,
    ParameterWithSetpoints,
    ParamRawDataType,
)
from qcodes.validators import Arrays, ComplexNumbers, Enum, Ints, Numbers, Strings

if TYPE_CHECKING:
    from collections.abc import Iterable

    from typing_extensions import Unpack


class ChannelTrace(ParameterWithSetpoints):
    """
    Parameter class for the two channel buffers
    """

    def __init__(self, name: str, channel: int, **kwargs: Any) -> None:
        """
        Args:
            name: The name of the parameter
            channel: The relevant channel (1 or 2). The name should
                match this.
            **kwargs: kwargs are forwarded to base class.
        """
        super().__init__(name, **kwargs)

        self._valid_channels = (1, 2)

        if channel not in self._valid_channels:
            raise ValueError('Invalid channel specifier. SR830 only has '
                             'channels 1 and 2.')

        if not isinstance(self.root_instrument, SR830):
            raise ValueError('Invalid parent instrument. ChannelBuffer '
                             'can only live on an SR830.')

        self.channel = channel
        self.update_unit()

    def update_unit(self) -> None:
        assert isinstance(self.root_instrument, SR830)
        params = self.root_instrument.parameters
        if params[f'ch{self.channel}_ratio'].get() != 'none':
            self.unit = '%'
        else:
            disp = params[f'ch{self.channel}_display'].get()
            if disp == 'Phase':
                self.unit = 'deg'
            else:
                self.unit = 'V'
            self.label = disp

    def get_raw(self) -> ParamRawDataType:
        """
        Get command. Returns numpy array
        """
        assert isinstance(self.root_instrument, SR830)
        N = self.root_instrument.buffer_npts()
        if N == 0:
            raise ValueError('No points stored in SR830 data buffer.'
                             ' Can not poll anything.')

        # poll raw binary data
        self.root_instrument.write(f'TRCL ? {self.channel}, 0, {N}')
        rawdata = self.root_instrument.visa_handle.read_raw()

        # parse it
        realdata = np.frombuffer(rawdata, dtype="<i2")
        numbers = realdata[::2] * 2.0 ** (realdata[1::2] - 124)

        return numbers


class ChannelBuffer(ArrayParameter):
    """
    Parameter class for the two channel buffers

    Currently always returns the entire buffer
    TODO (WilliamHPNielsen): Make it possible to query parts of the buffer.
    The instrument natively supports this in its TRCL call.
    """

    def __init__(self, name: str, instrument: SR830, channel: int) -> None:
        """
        Args:
            name: The name of the parameter
            instrument: The parent instrument
            channel: The relevant channel (1 or 2). The name should
                should match this.
        """
        self._valid_channels = (1, 2)

        if channel not in self._valid_channels:
            raise ValueError('Invalid channel specifier. SR830 only has '
                             'channels 1 and 2.')

        if not isinstance(instrument, SR830):
            raise ValueError('Invalid parent instrument. ChannelBuffer '
                             'can only live on an SR830.')

        super().__init__(
            name,
            shape=(1,),  # dummy initial shape
            unit="V",  # dummy initial unit
            setpoint_names=("Time",),
            setpoint_labels=("Time",),
            setpoint_units=("s",),
            docstring="Holds an acquired (part of the) data buffer of one channel.",
            instrument=instrument,
        )

        self.channel = channel

    def prepare_buffer_readout(self) -> None:
        """
        Function to generate the setpoints for the channel buffer and
        get the right units
        """
        assert isinstance(self.instrument, SR830)
        N = self.instrument.buffer_npts()  # problem if this is zero?
        # TODO (WilliamHPNielsen): what if SR was changed during acquisition?
        SR = self.instrument.buffer_SR()
        if SR == 'Trigger':
            self.setpoint_units = ('',)
            self.setpoint_names = ('trig_events',)
            self.setpoint_labels = ('Trigger event number',)
            self.setpoints = (tuple(np.arange(0, N)),)
        else:
            dt = 1/SR
            self.setpoint_units = ('s',)
            self.setpoint_names = ('Time',)
            self.setpoint_labels = ('Time',)
            self.setpoints = (tuple(np.linspace(0, N*dt, N)),)

        self.shape = (N,)

        params = self.instrument.parameters
        # YES, it should be: comparing to the string 'none' and not
        # the None literal
        if params[f'ch{self.channel}_ratio'].get() != 'none':
            self.unit = '%'
        else:
            disp = params[f'ch{self.channel}_display'].get()
            if disp == 'Phase':
                self.unit = 'deg'
            else:
                self.unit = 'V'

        if self.channel == 1:
            self.instrument._buffer1_ready = True
        else:
            self.instrument._buffer2_ready = True

    def get_raw(self) -> ParamRawDataType:
        """
        Get command. Returns numpy array
        """
        assert isinstance(self.instrument, SR830)
        if self.channel == 1:
            ready = self.instrument._buffer1_ready
        else:
            ready = self.instrument._buffer2_ready

        if not ready:
            raise RuntimeError('Buffer not ready. Please run '
                               'prepare_buffer_readout')
        N = self.instrument.buffer_npts()
        if N == 0:
            raise ValueError('No points stored in SR830 data buffer.'
                             ' Can not poll anything.')

        # poll raw binary data
        self.instrument.write(f"TRCL ? {self.channel}, 0, {N}")
        rawdata = self.instrument.visa_handle.read_raw()

        # parse it
        realdata = np.frombuffer(rawdata, dtype="<i2")
        numbers = realdata[::2] * 2.0 ** (realdata[1::2] - 124)
        if self.shape[0] != N:
            raise RuntimeError(
                f"SR830 got {N} points in buffer expected {self.shape[0]}"
            )
        return numbers


[docs] class SR830(VisaInstrument): """ QCoDeS driver for the Stanford Research Systems SR830 Lock-in Amplifier. """ _VOLT_TO_N: ClassVar[dict[float | int, int]] = { 2e-9: 0, 5e-9: 1, 10e-9: 2, 20e-9: 3, 50e-9: 4, 100e-9: 5, 200e-9: 6, 500e-9: 7, 1e-6: 8, 2e-6: 9, 5e-6: 10, 10e-6: 11, 20e-6: 12, 50e-6: 13, 100e-6: 14, 200e-6: 15, 500e-6: 16, 1e-3: 17, 2e-3: 18, 5e-3: 19, 10e-3: 20, 20e-3: 21, 50e-3: 22, 100e-3: 23, 200e-3: 24, 500e-3: 25, 1: 26, } _N_TO_VOLT: ClassVar[dict[int, float | int]] = {v: k for k, v in _VOLT_TO_N.items()} _CURR_TO_N: ClassVar[dict[float, int]] = { 2e-15: 0, 5e-15: 1, 10e-15: 2, 20e-15: 3, 50e-15: 4, 100e-15: 5, 200e-15: 6, 500e-15: 7, 1e-12: 8, 2e-12: 9, 5e-12: 10, 10e-12: 11, 20e-12: 12, 50e-12: 13, 100e-12: 14, 200e-12: 15, 500e-12: 16, 1e-9: 17, 2e-9: 18, 5e-9: 19, 10e-9: 20, 20e-9: 21, 50e-9: 22, 100e-9: 23, 200e-9: 24, 500e-9: 25, 1e-6: 26, } _N_TO_CURR: ClassVar[dict[int, float]] = {v: k for k, v in _CURR_TO_N.items()} _VOLT_ENUM = Enum(*_VOLT_TO_N.keys()) _CURR_ENUM = Enum(*_CURR_TO_N.keys()) _INPUT_CONFIG_TO_N: ClassVar[dict[str, int]] = { 'a': 0, 'a-b': 1, 'I 1M': 2, 'I 100M': 3, } _N_TO_INPUT_CONFIG: ClassVar[dict[int, str]] = { v: k for k, v in _INPUT_CONFIG_TO_N.items() } def __init__(self, name: str, address: str, **kwargs: Unpack[VisaInstrumentKWArgs]): super().__init__(name, address, **kwargs) # Reference and phase self.phase: Parameter = self.add_parameter( "phase", label="Phase", get_cmd="PHAS?", get_parser=float, set_cmd="PHAS {:.2f}", unit="deg", vals=Numbers(min_value=-360, max_value=729.99), ) """Parameter phase""" self.reference_source: Parameter = self.add_parameter( "reference_source", label="Reference source", get_cmd="FMOD?", set_cmd="FMOD {}", val_mapping={ "external": 0, "internal": 1, }, vals=Enum("external", "internal"), ) """Parameter reference_source""" self.frequency: Parameter = self.add_parameter( "frequency", label="Frequency", get_cmd="FREQ?", get_parser=float, set_cmd="FREQ {:.4f}", unit="Hz", vals=Numbers(min_value=1e-3, max_value=102e3), ) """Parameter frequency""" self.ext_trigger: Parameter = self.add_parameter( "ext_trigger", label="External trigger", get_cmd="RSLP?", set_cmd="RSLP {}", val_mapping={ "sine": 0, "TTL rising": 1, "TTL falling": 2, }, ) """Parameter ext_trigger""" self.harmonic: Parameter = self.add_parameter( "harmonic", label="Harmonic", get_cmd="HARM?", get_parser=int, set_cmd="HARM {:d}", vals=Ints(min_value=1, max_value=19999), ) """Parameter harmonic""" self.amplitude: Parameter = self.add_parameter( "amplitude", label="Amplitude", get_cmd="SLVL?", get_parser=float, set_cmd="SLVL {:.3f}", unit="V", vals=Numbers(min_value=0.004, max_value=5.000), ) """Parameter amplitude""" # Input and filter self.input_config: Parameter = self.add_parameter( "input_config", label="Input configuration", get_cmd="ISRC?", get_parser=self._get_input_config, set_cmd="ISRC {}", set_parser=self._set_input_config, vals=Enum(*self._INPUT_CONFIG_TO_N.keys()), ) """Parameter input_config""" self.input_shield: Parameter = self.add_parameter( "input_shield", label="Input shield", get_cmd="IGND?", set_cmd="IGND {}", val_mapping={ "float": 0, "ground": 1, }, ) """Parameter input_shield""" self.input_coupling: Parameter = self.add_parameter( "input_coupling", label="Input coupling", get_cmd="ICPL?", set_cmd="ICPL {}", val_mapping={ "AC": 0, "DC": 1, }, ) """Parameter input_coupling""" self.notch_filter: Parameter = self.add_parameter( "notch_filter", label="Notch filter", get_cmd="ILIN?", set_cmd="ILIN {}", val_mapping={ "off": 0, "line in": 1, "2x line in": 2, "both": 3, }, ) """Parameter notch_filter""" # Gain and time constant self.sensitivity: Parameter = self.add_parameter( name="sensitivity", label="Sensitivity", get_cmd="SENS?", set_cmd="SENS {:d}", get_parser=self._get_sensitivity, set_parser=self._set_sensitivity, ) """Parameter sensitivity""" self.reserve: Parameter = self.add_parameter( "reserve", label="Reserve", get_cmd="RMOD?", set_cmd="RMOD {}", val_mapping={ "high": 0, "normal": 1, "low noise": 2, }, ) """Parameter reserve""" self.time_constant: Parameter = self.add_parameter( "time_constant", label="Time constant", get_cmd="OFLT?", set_cmd="OFLT {}", unit="s", val_mapping={ 10e-6: 0, 30e-6: 1, 100e-6: 2, 300e-6: 3, 1e-3: 4, 3e-3: 5, 10e-3: 6, 30e-3: 7, 100e-3: 8, 300e-3: 9, 1: 10, 3: 11, 10: 12, 30: 13, 100: 14, 300: 15, 1e3: 16, 3e3: 17, 10e3: 18, 30e3: 19, }, ) """Parameter time_constant""" self.filter_slope: Parameter = self.add_parameter( "filter_slope", label="Filter slope", get_cmd="OFSL?", set_cmd="OFSL {}", unit="dB/oct", val_mapping={ 6: 0, 12: 1, 18: 2, 24: 3, }, ) """Parameter filter_slope""" self.sync_filter: Parameter = self.add_parameter( "sync_filter", label="Sync filter", get_cmd="SYNC?", set_cmd="SYNC {}", val_mapping={ "off": 0, "on": 1, }, ) """Parameter sync_filter""" def parse_offset_get(s: str) -> tuple[float, int]: parts = s.split(',') return float(parts[0]), int(parts[1]) # TODO: Parameters that can be set with multiple arguments # For the OEXP command for example two arguments are needed self.X_offset: Parameter = self.add_parameter( "X_offset", get_cmd="OEXP? 1", get_parser=parse_offset_get ) """Parameter X_offset""" self.Y_offset: Parameter = self.add_parameter( "Y_offset", get_cmd="OEXP? 2", get_parser=parse_offset_get ) """Parameter Y_offset""" self.R_offset: Parameter = self.add_parameter( "R_offset", get_cmd="OEXP? 3", get_parser=parse_offset_get ) """Parameter R_offset""" # Aux input/output for i in [1, 2, 3, 4]: self.add_parameter(f'aux_in{i}', label=f'Aux input {i}', get_cmd=f'OAUX? {i}', get_parser=float, unit='V') self.add_parameter(f'aux_out{i}', label=f'Aux output {i}', get_cmd=f'AUXV? {i}', get_parser=float, set_cmd=f'AUXV {i}, {{}}', unit='V') # Setup self.output_interface: Parameter = self.add_parameter( "output_interface", label="Output interface", get_cmd="OUTX?", set_cmd="OUTX {}", val_mapping={ "RS232": "0\n", "GPIB": "1\n", }, ) """Parameter output_interface""" # Data transfer self.X: Parameter = self.add_parameter( "X", get_cmd="OUTP? 1", get_parser=float, unit="V" ) """Parameter X""" self.Y: Parameter = self.add_parameter( "Y", get_cmd="OUTP? 2", get_parser=float, unit="V" ) """Parameter Y""" self.R: Parameter = self.add_parameter( "R", get_cmd="OUTP? 3", get_parser=float, unit="V" ) """Parameter R""" self.P: Parameter = self.add_parameter( "P", get_cmd="OUTP? 4", get_parser=float, unit="deg" ) """Parameter P""" self.complex_voltage: Parameter = self.add_parameter( "complex_voltage", label="Voltage", get_cmd=self._get_complex_voltage, unit="V", docstring="Complex voltage parameter " "calculated from X, Y phase using " "Z = X +j*Y", vals=ComplexNumbers(), ) """Complex voltage parameter calculated from X, Y phase using Z = X +j*Y""" # Data buffer settings self.buffer_SR: Parameter = self.add_parameter( "buffer_SR", label="Buffer sample rate", get_cmd="SRAT ?", set_cmd=self._set_buffer_SR, unit="Hz", val_mapping={ 62.5e-3: 0, 0.125: 1, 0.250: 2, 0.5: 3, 1: 4, 2: 5, 4: 6, 8: 7, 16: 8, 32: 9, 64: 10, 128: 11, 256: 12, 512: 13, "Trigger": 14, }, get_parser=int, ) """Parameter buffer_SR""" self.buffer_acq_mode: Parameter = self.add_parameter( "buffer_acq_mode", label="Buffer acquistion mode", get_cmd="SEND ?", set_cmd="SEND {}", val_mapping={"single shot": 0, "loop": 1}, get_parser=int, ) """Parameter buffer_acq_mode""" self.buffer_trig_mode: Parameter = self.add_parameter( "buffer_trig_mode", label="Buffer trigger start mode", get_cmd="TSTR ?", set_cmd="TSTR {}", val_mapping={"ON": 1, "OFF": 0}, get_parser=int, ) """Parameter buffer_trig_mode""" self.buffer_npts: Parameter = self.add_parameter( "buffer_npts", label="Buffer number of stored points", get_cmd="SPTS ?", get_parser=int, ) """Parameter buffer_npts""" self.sweep_setpoints: GeneratedSetPoints = self.add_parameter( "sweep_setpoints", parameter_class=GeneratedSetPoints, vals=Arrays(shape=(self.buffer_npts.get,)), ) """Parameter sweep_setpoints""" # Channel setup for ch in range(1, 3): # detailed validation and mapping performed in set/get functions self.add_parameter(f'ch{ch}_ratio', label=f'Channel {ch} ratio', get_cmd=partial(self._get_ch_ratio, ch), set_cmd=partial(self._set_ch_ratio, ch), vals=Strings()) self.add_parameter(f'ch{ch}_display', label=f'Channel {ch} display', get_cmd=partial(self._get_ch_display, ch), set_cmd=partial(self._set_ch_display, ch), vals=Strings()) self.add_parameter(f'ch{ch}_databuffer', channel=ch, parameter_class=ChannelBuffer) self.add_parameter(f'ch{ch}_datatrace', channel=ch, vals=Arrays(shape=(self.buffer_npts.get,)), setpoints=(self.sweep_setpoints,), parameter_class=ChannelTrace) # Auto functions self.add_function('auto_gain', call_cmd='AGAN') self.add_function('auto_reserve', call_cmd='ARSV') self.add_function('auto_phase', call_cmd='APHS') self.add_function('auto_offset', call_cmd='AOFF {0}', args=[Enum(1, 2, 3)]) # Interface self.add_function('reset', call_cmd='*RST') self.add_function('disable_front_panel', call_cmd='OVRM 0') self.add_function('enable_front_panel', call_cmd='OVRM 1') self.add_function('send_trigger', call_cmd='TRIG', docstring=("Send a software trigger. " "This command has the same effect as a " "trigger at the rear panel trigger" " input.")) self.add_function('buffer_start', call_cmd='STRT', docstring=("The buffer_start command starts or " "resumes data storage. buffer_start" " is ignored if storage is already in" " progress.")) self.add_function('buffer_pause', call_cmd='PAUS', docstring=("The buffer_pause command pauses data " "storage. If storage is already paused " "or reset then this command is ignored.")) self.add_function('buffer_reset', call_cmd='REST', docstring=("The buffer_reset command resets the data" " buffers. The buffer_reset command can " "be sent at any time - any storage in " "progress, paused or not, will be reset." " This command will erase the data " "buffer.")) # Initialize the proper units of the outputs and sensitivities self.input_config() # start keeping track of buffer setpoints self._buffer1_ready = False self._buffer2_ready = False self.connect_message() SNAP_PARAMETERS: ClassVar[dict[str, str]] = { 'x': '1', 'y': '2', 'r': '3', 'p': '4', 'phase': '4', 'θ': '4', 'aux1': '5', 'aux2': '6', 'aux3': '7', 'aux4': '8', 'freq': '9', 'ch1': '10', 'ch2': '11' }
[docs] def snap(self, *parameters: str) -> tuple[float, ...]: """ Get between 2 and 6 parameters at a single instant. This provides a coherent snapshot of measured signals. Pick up to 6 from: X, Y, R, θ, the aux inputs 1-4, frequency, or what is currently displayed on channels 1 and 2. Reading X and Y (or R and θ) gives a coherent snapshot of the signal. Snap is important when the time constant is very short, a time constant less than 100 ms. Args: *parameters: From 2 to 6 strings of names of parameters for which the values are requested. including: 'x', 'y', 'r', 'p', 'phase' or 'θ', 'aux1', 'aux2', 'aux3', 'aux4', 'freq', 'ch1', and 'ch2'. Returns: A tuple of floating point values in the same order as requested. Examples: >>> lockin.snap('x','y') -> tuple(x,y) >>> lockin.snap('aux1','aux2','freq','phase') >>> -> tuple(aux1,aux2,freq,phase) Note: Volts for x, y, r, and aux 1-4 Degrees for θ Hertz for freq Unknown for ch1 and ch2. It will depend on what was set. - If X,Y,R and θ are all read, then the values of X,Y are recorded approximately 10 µs apart from R,θ. Thus, the values of X and Y may not yield the exact values of R and θ from a single snap. - The values of the Aux Inputs may have an uncertainty of up to 32 µs. - The frequency is computed only every other period or 40 ms, whichever is longer. """ if not 2 <= len(parameters) <= 6: raise KeyError( 'It is only possible to request values of 2 to 6 parameters' ' at a time.') for name in parameters: if name.lower() not in self.SNAP_PARAMETERS: raise KeyError(f'{name} is an unknown parameter. Refer' f' to `SNAP_PARAMETERS` for a list of valid' f' parameter names') p_ids = [self.SNAP_PARAMETERS[name.lower()] for name in parameters] output = self.ask(f'SNAP? {",".join(p_ids)}') return tuple(float(val) for val in output.split(','))
[docs] def increment_sensitivity(self) -> bool: """ Increment the sensitivity setting of the lock-in. This is equivalent to pushing the sensitivity up button on the front panel. This has no effect if the sensitivity is already at the maximum. Returns: Whether or not the sensitivity was actually changed. """ return self._change_sensitivity(1)
[docs] def decrement_sensitivity(self) -> bool: """ Decrement the sensitivity setting of the lock-in. This is equivalent to pushing the sensitivity down button on the front panel. This has no effect if the sensitivity is already at the minimum. Returns: Whether or not the sensitivity was actually changed. """ return self._change_sensitivity(-1)
def _change_sensitivity(self, dn: int) -> bool: if self.input_config() in ['a', 'a-b']: n_to = self._N_TO_VOLT to_n = self._VOLT_TO_N else: n_to = self._N_TO_CURR to_n = self._CURR_TO_N n = to_n[self.sensitivity()] if n + dn > max(n_to.keys()) or n + dn < min(n_to.keys()): return False self.sensitivity.set(n_to[n + dn]) return True def _set_buffer_SR(self, SR: int) -> None: self.write(f'SRAT {SR}') self._buffer1_ready = False self._buffer2_ready = False self.sweep_setpoints.update_units_if_constant_sample_rate() def _get_ch_ratio(self, channel: int) -> str: val_mapping = {1: {0: 'none', 1: 'Aux In 1', 2: 'Aux In 2'}, 2: {0: 'none', 1: 'Aux In 3', 2: 'Aux In 4'}} resp = int(self.ask(f'DDEF ? {channel}').split(',')[1]) return val_mapping[channel][resp] def _set_ch_ratio(self, channel: int, ratio: str) -> None: val_mapping = {1: {'none': 0, 'Aux In 1': 1, 'Aux In 2': 2}, 2: {'none': 0, 'Aux In 3': 1, 'Aux In 4': 2}} vals = val_mapping[channel].keys() if ratio not in vals: raise ValueError(f'{ratio} not in {vals}') ratio_int = val_mapping[channel][ratio] disp_val = int(self.ask(f'DDEF ? {channel}').split(',')[0]) self.write(f'DDEF {channel}, {disp_val}, {ratio_int}') self._buffer_ready = False def _get_ch_display(self, channel: int) -> str: val_mapping = {1: {0: 'X', 1: 'R', 2: 'X Noise', 3: 'Aux In 1', 4: 'Aux In 2'}, 2: {0: 'Y', 1: 'Phase', 2: 'Y Noise', 3: 'Aux In 3', 4: 'Aux In 4'}} resp = int(self.ask(f'DDEF ? {channel}').split(',')[0]) return val_mapping[channel][resp] def _set_ch_display(self, channel: int, disp: str) -> None: val_mapping = {1: {'X': 0, 'R': 1, 'X Noise': 2, 'Aux In 1': 3, 'Aux In 2': 4}, 2: {'Y': 0, 'Phase': 1, 'Y Noise': 2, 'Aux In 3': 3, 'Aux In 4': 4}} vals = val_mapping[channel].keys() if disp not in vals: raise ValueError(f'{disp} not in {vals}') disp_int = val_mapping[channel][disp] # Since ratio AND display are set simultaneously, # we get and then re-set the current ratio value ratio_val = int(self.ask(f'DDEF ? {channel}').split(',')[1]) self.write(f'DDEF {channel}, {disp_int}, {ratio_val}') self._buffer_ready = False # we update the unit of the datatrace # according to the choice of channel params = self.parameters dataparam = params[f'ch{channel}_datatrace'] assert isinstance(dataparam, ChannelTrace) dataparam.update_unit() def _set_units(self, unit: str) -> None: # TODO: # make a public parameter function that allows to change the units for param in [self.X, self.Y, self.R, self.sensitivity]: param.unit = unit def _get_complex_voltage(self) -> complex: x, y = self.snap("X", "Y") return x + 1.0j * y def _get_input_config(self, s: int) -> str: mode = self._N_TO_INPUT_CONFIG[int(s)] if mode in ['a', 'a-b']: self.sensitivity.vals = self._VOLT_ENUM self._set_units('V') else: self.sensitivity.vals = self._CURR_ENUM self._set_units('A') return mode def _set_input_config(self, s: str) -> int: if s in ['a', 'a-b']: self.sensitivity.vals = self._VOLT_ENUM self._set_units('V') else: self.sensitivity.vals = self._CURR_ENUM self._set_units('A') return self._INPUT_CONFIG_TO_N[s] def _get_sensitivity(self, s: int) -> float: if self.input_config() in ['a', 'a-b']: return self._N_TO_VOLT[int(s)] else: return self._N_TO_CURR[int(s)] def _set_sensitivity(self, s: float) -> int: if self.input_config() in ['a', 'a-b']: return self._VOLT_TO_N[s] else: return self._CURR_TO_N[s]
[docs] def autorange(self, max_changes: int = 1) -> None: """ Automatically changes the sensitivity of the instrument according to the R value and defined max_changes. Args: max_changes: Maximum number of steps allowing the function to automatically change the sensitivity (default is 1). The actual number of steps needed to change to the optimal sensitivity may be more or less than this maximum. """ def autorange_once() -> bool: r = self.R() sens = self.sensitivity() if r > 0.9 * sens: return self.increment_sensitivity() elif r < 0.1 * sens: return self.decrement_sensitivity() return False sets = 0 while autorange_once() and sets < max_changes: sets += 1 time.sleep(self.time_constant())
[docs] def set_sweep_parameters( self, sweep_param: Parameter, start: float, stop: float, n_points: int = 10, label: str | None = None, ) -> None: self.sweep_setpoints.sweep_array = np.linspace(start, stop, n_points) self.sweep_setpoints.unit = sweep_param.unit if label is not None: self.sweep_setpoints.label = label elif sweep_param.label is not None: self.sweep_setpoints.label = sweep_param.label
class GeneratedSetPoints(Parameter): """ A parameter that generates a setpoint array from start, stop and num points parameters. """ def __init__( self, sweep_array: Iterable[float | int] = np.linspace(0, 1, 10), *args: Any, **kwargs: Any, ) -> None: super().__init__(*args, **kwargs) self.sweep_array = sweep_array self.update_units_if_constant_sample_rate() def update_units_if_constant_sample_rate(self) -> None: """ If the buffer is filled at a constant sample rate, update the unit to "s" and label to "Time"; otherwise do nothing """ assert isinstance(self.root_instrument, SR830) SR = self.root_instrument.buffer_SR.get() if SR != 'Trigger': self.unit = 's' self.label = 'Time' def set_raw(self, value: Iterable[float | int]) -> None: self.sweep_array = value def get_raw(self) -> ParamRawDataType: assert isinstance(self.root_instrument, SR830) SR = self.root_instrument.buffer_SR.get() if SR == 'Trigger': return self.sweep_array else: N = self.root_instrument.buffer_npts.get() dt = 1/SR return np.linspace(0, N*dt, N)