Source code for qcodes.instrument_drivers.Keithley._Keithley_2600

from __future__ import annotations

import logging
import struct
import sys
import warnings
from enum import Enum
from typing import TYPE_CHECKING, Any, Literal

import numpy as np

import qcodes.validators as vals
from qcodes.instrument import Instrument, InstrumentChannel, VisaInstrument
from qcodes.parameters import (
    ArrayParameter,
    Parameter,
    ParameterWithSetpoints,
    ParamRawDataType,
    create_on_off_val_mapping,
)

if TYPE_CHECKING:
    from collections.abc import Sequence

    from qcodes_loop.data.data_set import DataSet


if sys.version_info >= (3, 11):
    from enum import StrEnum
else:

    class StrEnum(str, Enum):
        pass

log = logging.getLogger(__name__)


class LuaSweepParameter(ArrayParameter):
    """
    Parameter class to hold the data from a
    deployed Lua script sweep.
    """

    def __init__(self, name: str, instrument: Instrument, **kwargs: Any) -> None:

        super().__init__(
            name=name,
            shape=(1,),
            docstring="Holds a sweep",
            instrument=instrument,
            **kwargs,
        )

    def prepareSweep(self, start: float, stop: float, steps: int, mode: str) -> None:
        """
        Builds setpoints and labels

        Args:
            start: Starting point of the sweep
            stop: Endpoint of the sweep
            steps: No. of sweep steps
            mode: Type of sweep, either 'IV' (voltage sweep),
                'VI' (current sweep two probe setup) or
                'VIfourprobe' (current sweep four probe setup)
        """

        if mode not in ["IV", "VI", "VIfourprobe"]:
            raise ValueError('mode must be either "VI", "IV" or "VIfourprobe"')

        self.shape = (steps,)

        if mode == "IV":
            self.unit = "A"
            self.setpoint_names = ("Voltage",)
            self.setpoint_units = ("V",)
            self.label = "current"
            self._short_name = "iv_sweep"

        if mode == "VI":
            self.unit = "V"
            self.setpoint_names = ("Current",)
            self.setpoint_units = ("A",)
            self.label = "voltage"
            self._short_name = "vi_sweep"

        if mode == "VIfourprobe":
            self.unit = "V"
            self.setpoint_names = ("Current",)
            self.setpoint_units = ("A",)
            self.label = "voltage"
            self._short_name = "vi_sweep_four_probe"

        self.setpoints = (tuple(np.linspace(start, stop, steps)),)

        self.start = start
        self.stop = stop
        self.steps = steps
        self.mode = mode

    def get_raw(self) -> np.ndarray:

        if self.instrument is not None:
            data = self.instrument._fast_sweep(
                self.start, self.stop, self.steps, self.mode
            )
        else:
            raise RuntimeError("No instrument attached to Parameter.")

        return data


class TimeTrace(ParameterWithSetpoints):
    """
    A parameter class that holds the data corresponding to the time dependence of
    current and voltage.
    """

    def _check_time_trace(self) -> None:
        """
        A helper function that compares the integration time with measurement
        interval for accurate results.

        Raises:
            RuntimeError: If no instrument attached to Parameter.
        """
        if self.instrument is None:
            raise RuntimeError("No instrument attached to Parameter.")

        dt = self.instrument.timetrace_dt()
        nplc = self.instrument.nplc()
        linefreq = self.instrument.linefreq()
        plc = 1 / linefreq
        if nplc * plc > dt:
            warnings.warn(
                f"Integration time of {nplc*plc*1000:.1f} "
                f"ms is longer than {dt*1000:.1f} ms set "
                "as measurement interval. Consider lowering "
                "NPLC or increasing interval.",
                UserWarning,
                2,
            )

    def _set_mode(self, mode: str) -> None:
        """
        A helper function to set correct units and labels.

        Args:
            mode: User defined mode for the timetrace. It can be either
            "current" or "voltage".
        """
        if mode == "current":
            self.unit = "A"
            self.label = "Current"
        if mode == "voltage":
            self.unit = "V"
            self.label = "Voltage"

    def _time_trace(self) -> np.ndarray:
        """
        The function that prepares a Lua script for timetrace data acquisition.

        Raises:
            RuntimeError: If no instrument attached to Parameter.
        """

        if self.instrument is None:
            raise RuntimeError("No instrument attached to Parameter.")

        channel = self.instrument.channel
        npts = self.instrument.timetrace_npts()
        dt = self.instrument.timetrace_dt()
        mode = self.instrument.timetrace_mode()

        mode_map = {"current": "i", "voltage": "v"}

        script = [
            f"{channel}.measure.count={npts}",
            f"oldint={channel}.measure.interval",
            f"{channel}.measure.interval={dt}",
            f"{channel}.nvbuffer1.clear()",
            f"{channel}.measure.{mode_map[mode]}({channel}.nvbuffer1)",
            f"{channel}.measure.interval=oldint",
            f"{channel}.measure.count=1",
            "format.data = format.REAL32",
            "format.byteorder = format.LITTLEENDIAN",
            f"printbuffer(1, {npts}, {channel}.nvbuffer1.readings)",
        ]

        return self.instrument._execute_lua(script, npts)

    def get_raw(self) -> np.ndarray:

        if self.instrument is None:
            raise RuntimeError("No instrument attached to Parameter.")

        self._check_time_trace()
        data = self._time_trace()
        return data


class TimeAxis(Parameter):
    """
    A simple :class:`.Parameter` that holds all the times (relative to the
    measurement start) at which the points of the time trace were acquired.
    """

    def get_raw(self) -> np.ndarray:

        if self.instrument is None:
            raise RuntimeError("No instrument attached to Parameter.")

        npts = self.instrument.timetrace_npts()
        dt = self.instrument.timetrace_dt()
        return np.linspace(0, dt * npts, npts, endpoint=False)


[docs] class Keithley2600MeasurementStatus(StrEnum): """ Keeps track of measurement status. """ CURRENT_COMPLIANCE_ERROR = "Reached current compliance limit." VOLTAGE_COMPLIANCE_ERROR = "Reached voltage compliance limit." VOLTAGE_AND_CURRENT_COMPLIANCE_ERROR = ( "Reached both voltage and current compliance limits." ) NORMAL = "No error occured." COMPLIANCE_ERROR = "Reached compliance limit." # deprecated, dont use it. It exists only for backwards compatibility
MeasurementStatus = Keithley2600MeasurementStatus "Alias for backwards compatibility. Will eventually be deprecated and removed" _from_bits_tuple_to_status = { (0, 0): Keithley2600MeasurementStatus.NORMAL, (1, 0): Keithley2600MeasurementStatus.VOLTAGE_COMPLIANCE_ERROR, (0, 1): Keithley2600MeasurementStatus.CURRENT_COMPLIANCE_ERROR, (1, 1): Keithley2600MeasurementStatus.VOLTAGE_AND_CURRENT_COMPLIANCE_ERROR, } class _ParameterWithStatus(Parameter): def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) self._measurement_status: Keithley2600MeasurementStatus | None = None @property def measurement_status(self) -> Keithley2600MeasurementStatus | None: return self._measurement_status @staticmethod def _parse_response(data: str) -> tuple[float, Keithley2600MeasurementStatus]: value, meas_status = data.split("\t") status_bits = [ int(i) for i in bin(int(float(meas_status))).replace("0b", "").zfill(16)[::-1] ] status = _from_bits_tuple_to_status[ (status_bits[0], status_bits[1]) ] # pyright: ignore[reportArgumentType] return float(value), status def snapshot_base( self, update: bool | None = True, params_to_skip_update: Sequence[str] | None = None, ) -> dict[Any, Any]: snapshot = super().snapshot_base( update=update, params_to_skip_update=params_to_skip_update ) if self._snapshot_value: snapshot["measurement_status"] = self.measurement_status return snapshot class _MeasurementCurrentParameter(_ParameterWithStatus): def set_raw(self, value: ParamRawDataType) -> None: assert isinstance(self.instrument, Keithley2600Channel) assert isinstance(self.root_instrument, Keithley2600) smu_chan = self.instrument channel = smu_chan.channel smu_chan.write(f"{channel}.source.leveli={value:.12f}") smu_chan._reset_measurement_statuses_of_parameters() def get_raw(self) -> ParamRawDataType: assert isinstance(self.instrument, Keithley2600Channel) assert isinstance(self.root_instrument, Keithley2600) smu = self.instrument channel = self.instrument.channel data = smu.ask( f"{channel}.measure.i(), " f"status.measurement.instrument.{channel}.condition" ) value, status = self._parse_response(data) self._measurement_status = status return value class _MeasurementVoltageParameter(_ParameterWithStatus): def set_raw(self, value: ParamRawDataType) -> None: assert isinstance(self.instrument, Keithley2600Channel) assert isinstance(self.root_instrument, Keithley2600) smu_chan = self.instrument channel = smu_chan.channel smu_chan.write(f"{channel}.source.levelv={value:.12f}") smu_chan._reset_measurement_statuses_of_parameters() def get_raw(self) -> ParamRawDataType: assert isinstance(self.instrument, Keithley2600Channel) assert isinstance(self.root_instrument, Keithley2600) smu = self.instrument channel = self.instrument.channel data = smu.ask( f"{channel}.measure.v(), " f"status.measurement.instrument.{channel}.condition" ) value, status = self._parse_response(data) self._measurement_status = status return value class Keithley2600Channel(InstrumentChannel): """ Class to hold the two Keithley channels, i.e. SMUA and SMUB. """ def __init__(self, parent: Instrument, name: str, channel: str) -> None: """ Args: parent: The Instrument instance to which the channel is to be attached. name: The 'colloquial' name of the channel channel: The name used by the Keithley, i.e. either 'smua' or 'smub' """ if channel not in ["smua", "smub"]: raise ValueError('channel must be either "smub" or "smua"') super().__init__(parent, name) self.model = self._parent.model self._extra_visa_timeout = 5000 self._measurement_duration_factor = 2 # Ensures that we are always above # the expected time. vranges = self._parent._vranges iranges = self._parent._iranges vlimit_minmax = self.parent._vlimit_minmax ilimit_minmax = self.parent._ilimit_minmax self.add_parameter( "volt", parameter_class=_MeasurementVoltageParameter, label="Voltage", unit="V", snapshot_get=False, ) self.add_parameter( "curr", parameter_class=_MeasurementCurrentParameter, label="Current", unit="A", snapshot_get=False, ) self.add_parameter( "res", get_cmd=f"{channel}.measure.r()", get_parser=float, set_cmd=False, label="Resistance", unit="Ohm", ) self.add_parameter( "mode", get_cmd=f"{channel}.source.func", get_parser=float, set_cmd=f"{channel}.source.func={{:d}}", val_mapping={"current": 0, "voltage": 1}, docstring="Selects the output source type. " "Can be either voltage or current.", ) self.add_parameter( "output", get_cmd=f"{channel}.source.output", get_parser=float, set_cmd=f"{channel}.source.output={{:d}}", val_mapping=create_on_off_val_mapping(on_val=1, off_val=0), ) self.add_parameter( "linefreq", label="Line frequency", get_cmd="localnode.linefreq", get_parser=float, set_cmd=False, unit="Hz", ) self.add_parameter( "nplc", label="Number of power line cycles", set_cmd=f"{channel}.measure.nplc={{}}", get_cmd=f"{channel}.measure.nplc", get_parser=float, docstring="Number of power line cycles, used to perform measurements", vals=vals.Numbers(0.001, 25), ) # volt range # needs get after set (WilliamHPNielsen): why? self.add_parameter( "sourcerange_v", label="voltage source range", get_cmd=f"{channel}.source.rangev", get_parser=float, set_cmd=self._set_sourcerange_v, unit="V", docstring="The range used when sourcing voltage " "This affects the range and the precision " "of the source.", vals=vals.Enum(*vranges[self.model]), ) self.add_parameter( "source_autorange_v_enabled", label="voltage source autorange", get_cmd=f"{channel}.source.autorangev", get_parser=float, set_cmd=f"{channel}.source.autorangev={{}}", docstring="Set autorange on/off for source voltage.", val_mapping=create_on_off_val_mapping(on_val=1, off_val=0), ) self.add_parameter( "measurerange_v", label="voltage measure range", get_cmd=f"{channel}.measure.rangev", get_parser=float, set_cmd=self._set_measurerange_v, unit="V", docstring="The range to perform voltage " "measurements in. This affects the range " "and the precision of the measurement. " "Note that if you both measure and " "source current this will have no effect, " "set `sourcerange_v` instead", vals=vals.Enum(*vranges[self.model]), ) self.add_parameter( "measure_autorange_v_enabled", label="voltage measure autorange", get_cmd=f"{channel}.measure.autorangev", get_parser=float, set_cmd=f"{channel}.measure.autorangev={{}}", docstring="Set autorange on/off for measure voltage.", val_mapping=create_on_off_val_mapping(on_val=1, off_val=0), ) # current range # needs get after set self.add_parameter( "sourcerange_i", label="current source range", get_cmd=f"{channel}.source.rangei", get_parser=float, set_cmd=self._set_sourcerange_i, unit="A", docstring="The range used when sourcing current " "This affects the range and the " "precision of the source.", vals=vals.Enum(*iranges[self.model]), ) self.add_parameter( "source_autorange_i_enabled", label="current source autorange", get_cmd=f"{channel}.source.autorangei", get_parser=float, set_cmd=f"{channel}.source.autorangei={{}}", docstring="Set autorange on/off for source current.", val_mapping=create_on_off_val_mapping(on_val=1, off_val=0), ) self.add_parameter( "measurerange_i", label="current measure range", get_cmd=f"{channel}.measure.rangei", get_parser=float, set_cmd=self._set_measurerange_i, unit="A", docstring="The range to perform current " "measurements in. This affects the range " "and the precision of the measurement. " "Note that if you both measure and source " "current this will have no effect, set " "`sourcerange_i` instead", vals=vals.Enum(*iranges[self.model]), ) self.add_parameter( "measure_autorange_i_enabled", label="current autorange", get_cmd=f"{channel}.measure.autorangei", get_parser=float, set_cmd=f"{channel}.measure.autorangei={{}}", docstring="Set autorange on/off for measure current.", val_mapping=create_on_off_val_mapping(on_val=1, off_val=0), ) # Compliance limit self.add_parameter( "limitv", get_cmd=f"{channel}.source.limitv", get_parser=float, set_cmd=f"{channel}.source.limitv={{}}", docstring="Voltage limit e.g. the maximum voltage " "allowed in current mode. If exceeded " "the current will be clipped.", vals=vals.Numbers( vlimit_minmax[self.model][0], vlimit_minmax[self.model][1] ), unit="V", ) # Compliance limit self.add_parameter( "limiti", get_cmd=f"{channel}.source.limiti", get_parser=float, set_cmd=f"{channel}.source.limiti={{}}", docstring="Current limit e.g. the maximum current " "allowed in voltage mode. If exceeded " "the voltage will be clipped.", vals=vals.Numbers( ilimit_minmax[self.model][0], ilimit_minmax[self.model][1] ), unit="A", ) self.add_parameter("fastsweep", parameter_class=LuaSweepParameter) self.add_parameter( "timetrace_npts", initial_value=500, label="Number of points", get_cmd=None, set_cmd=None, ) self.add_parameter( "timetrace_dt", initial_value=1e-3, label="Time resolution", unit="s", get_cmd=None, set_cmd=None, ) self.add_parameter( name="time_axis", label="Time", unit="s", snapshot_value=False, vals=vals.Arrays(shape=(self.timetrace_npts,)), parameter_class=TimeAxis, ) self.add_parameter( "timetrace", vals=vals.Arrays(shape=(self.timetrace_npts,)), setpoints=(self.time_axis,), parameter_class=TimeTrace, ) self.add_parameter( "timetrace_mode", initial_value="current", get_cmd=None, set_cmd=self.timetrace._set_mode, vals=vals.Enum("current", "voltage"), ) self.channel = channel def _reset_measurement_statuses_of_parameters(self) -> None: assert isinstance(self.volt, _ParameterWithStatus) self.volt._measurement_status = None assert isinstance(self.curr, _ParameterWithStatus) self.curr._measurement_status = None def reset(self) -> None: """ Reset instrument to factory defaults. This resets only the relevant channel. """ self.write(f"{self.channel}.reset()") # remember to update all the metadata log.debug(f"Reset channel {self.channel}. Updating settings...") self.snapshot(update=True) def doFastSweep(self, start: float, stop: float, steps: int, mode: str) -> DataSet: """ Perform a fast sweep using a deployed lua script and return a QCoDeS DataSet with the sweep. Args: start: starting sweep value (V or A) stop: end sweep value (V or A) steps: number of steps mode: Type of sweep, either 'IV' (voltage sweep), 'VI' (current sweep two probe setup) or 'VIfourprobe' (current sweep four probe setup) """ try: from qcodes_loop.measure import Measure except ImportError as e: raise ImportError( "The doFastSweep method requires the " "qcodes_loop package to be installed." ) from e # prepare setpoints, units, name self.fastsweep.prepareSweep(start, stop, steps, mode) data = Measure(self.fastsweep).run() return data def _fast_sweep( self, start: float, stop: float, steps: int, mode: Literal["IV", "VI", "VIfourprobe"] = "IV", ) -> np.ndarray: """ Perform a fast sweep using a deployed Lua script. This is the engine that forms the script, uploads it, runs it, collects the data, and casts the data correctly. Args: start: starting voltage stop: end voltage steps: number of steps mode: Type of sweep, either 'IV' (voltage sweep), 'VI' (current sweep two probe setup) or 'VIfourprobe' (current sweep four probe setup) """ channel = self.channel # an extra visa query, a necessary precaution # to avoid timing out when waiting for long # measurements nplc = self.nplc() dV = (stop - start) / (steps - 1) if mode == "IV": meas = "i" sour = "v" func = "1" sense_mode = "0" elif mode == "VI": meas = "v" sour = "i" func = "0" sense_mode = "0" elif mode == "VIfourprobe": meas = "v" sour = "i" func = "0" sense_mode = "1" else: raise ValueError(f"Invalid mode {mode}") script = [ f"{channel}.measure.nplc = {nplc:.12f}", f"{channel}.source.output = 1", f"startX = {start:.12f}", f"dX = {dV:.12f}", f"{channel}.sense = {sense_mode}", f"{channel}.source.output = 1", f"{channel}.source.func = {func}", f"{channel}.measure.count = 1", f"{channel}.nvbuffer1.clear()", f"{channel}.nvbuffer1.appendmode = 1", f"for index = 1, {steps} do", " target = startX + (index-1)*dX", f" {channel}.source.level{sour} = target", f" {channel}.measure.{meas}({channel}.nvbuffer1)", "end", "format.data = format.REAL32", "format.byteorder = format.LITTLEENDIAN", f"printbuffer(1, {steps}, {channel}.nvbuffer1.readings)", ] return self._execute_lua(script, steps) def _execute_lua(self, _script: list[str], steps: int) -> np.ndarray: """ This is the function that sends the Lua script to be executed and returns the corresponding data from the buffer. Args: _script: The Lua script to be executed. steps: Number of points. """ nplc = self.nplc() linefreq = self.linefreq() _time_trace_extra_visa_timeout = self._extra_visa_timeout _factor = self._measurement_duration_factor estimated_measurement_duration = _factor * 1000 * steps * nplc / linefreq new_visa_timeout = ( estimated_measurement_duration + _time_trace_extra_visa_timeout ) self.write(self.root_instrument._scriptwrapper(program=_script, debug=True)) # now poll all the data # The problem is that a '\n' character might by chance be present in # the data fullsize = 4 * steps + 3 received = 0 data = b"" # we must wait for the script to execute with self.root_instrument.timeout.set_to(new_visa_timeout): while received < fullsize: data_temp = self.root_instrument.visa_handle.read_raw() received += len(data_temp) data += data_temp # From the manual p. 7-94, we know that a b'#0' is prepended # to the data and a b'\n' is appended data = data[2:-1] outdata = np.array(list(struct.iter_unpack("<f", data))) outdata = np.reshape(outdata, len(outdata)) return outdata def _set_sourcerange_v(self, val: float) -> None: channel = self.channel self.source_autorange_v_enabled(False) self.write(f"{channel}.source.rangev={val}") def _set_measurerange_v(self, val: float) -> None: channel = self.channel self.measure_autorange_v_enabled(False) self.write(f"{channel}.measure.rangev={val}") def _set_sourcerange_i(self, val: float) -> None: channel = self.channel self.source_autorange_i_enabled(False) self.write(f"{channel}.source.rangei={val}") def _set_measurerange_i(self, val: float) -> None: channel = self.channel self.measure_autorange_i_enabled(False) self.write(f"{channel}.measure.rangei={val}") class Keithley2600(VisaInstrument): """ This is the qcodes driver for the Keithley 2600 Source-Meter series, tested with Keithley 2614B """ def __init__(self, name: str, address: str, **kwargs: Any) -> None: """ Args: name: Name to use internally in QCoDeS address: VISA resource address **kwargs: kwargs are forwarded to base class. """ super().__init__(name, address, terminator="\n", **kwargs) model = self.ask("localnode.model") knownmodels = [ "2601B", "2602A", "2602B", "2604B", "2611B", "2612B", "2614B", "2634B", "2635B", "2636B", ] if model not in knownmodels: kmstring = ("{}, " * (len(knownmodels) - 1)).format(*knownmodels[:-1]) kmstring += f"and {knownmodels[-1]}." raise ValueError("Unknown model. Known model are: " + kmstring) self.model = model self._vranges = { "2601B": [0.1, 1, 6, 40], "2602A": [0.1, 1, 6, 40], "2602B": [0.1, 1, 6, 40], "2604B": [0.1, 1, 6, 40], "2611B": [0.2, 2, 20, 200], "2612B": [0.2, 2, 20, 200], "2614B": [0.2, 2, 20, 200], "2634B": [0.2, 2, 20, 200], "2635B": [0.2, 2, 20, 200], "2636B": [0.2, 2, 20, 200], } # TODO: In pulsed mode, models 2611B, 2612B, and 2614B # actually allow up to 10 A. self._iranges = { "2601B": [100e-9, 1e-6, 10e-6, 100e-6, 1e-3, 0.01, 0.1, 1, 3], "2602A": [100e-9, 1e-6, 10e-6, 100e-6, 1e-3, 0.01, 0.1, 1, 3], "2602B": [100e-9, 1e-6, 10e-6, 100e-6, 1e-3, 0.01, 0.1, 1, 3], "2604B": [100e-9, 1e-6, 10e-6, 100e-6, 1e-3, 0.01, 0.1, 1, 3], "2611B": [100e-9, 1e-6, 10e-6, 100e-6, 1e-3, 0.01, 0.1, 1, 1.5], "2612B": [100e-9, 1e-6, 10e-6, 100e-6, 1e-3, 0.01, 0.1, 1, 1.5], "2614B": [100e-9, 1e-6, 10e-6, 100e-6, 1e-3, 0.01, 0.1, 1, 1.5], "2634B": [ 1e-9, 10e-9, 100e-9, 1e-6, 10e-6, 100e-6, 1e-3, 10e-6, 100e-3, 1, 1.5, ], "2635B": [ 1e-9, 10e-9, 100e-9, 1e-6, 10e-6, 100e-6, 1e-3, 10e-6, 100e-3, 1, 1.5, ], "2636B": [ 1e-9, 10e-9, 100e-9, 1e-6, 10e-6, 100e-6, 1e-3, 10e-6, 100e-3, 1, 1.5, ], } self._vlimit_minmax = { "2601B": [10e-3, 40], "2602A": [10e-3, 40], "2602B": [10e-3, 40], "2604B": [10e-3, 40], "2611B": [20e-3, 200], "2612B": [20e-3, 200], "2614B": [20e-3, 200], "2634B": [20e-3, 200], "2635B": [20e-3, 200], "2636B": [20e-3, 200], } self._ilimit_minmax = { "2601B": [10e-9, 3], "2602A": [10e-9, 3], "2602B": [10e-9, 3], "2604B": [10e-9, 3], "2611B": [10e-9, 3], "2612B": [10e-9, 3], "2614B": [10e-9, 3], "2634B": [100e-12, 1.5], "2635B": [100e-12, 1.5], "2636B": [100e-12, 1.5], } # Add the channel to the instrument self.channels: list[Keithley2600Channel] = [] for ch in ["a", "b"]: ch_name = f"smu{ch}" channel = Keithley2600Channel(self, ch_name, ch_name) self.add_submodule(ch_name, channel) self.channels.append(channel) # display self.add_parameter( "display_settext", set_cmd=self._display_settext, vals=vals.Strings() ) self.connect_message() def _display_settext(self, text: str) -> None: self.visa_handle.write(f'display.settext("{text}")') def get_idn(self) -> dict[str, str | None]: IDNstr = self.ask_raw("*IDN?") vendor, model, serial, firmware = map(str.strip, IDNstr.split(",")) model = model[6:] IDN: dict[str, str | None] = { "vendor": vendor, "model": model, "serial": serial, "firmware": firmware, } return IDN def display_clear(self) -> None: """ This function clears the display, but also leaves it in user mode """ self.visa_handle.write("display.clear()") def display_normal(self) -> None: """ Set the display to the default mode """ self.visa_handle.write("display.screen = display.SMUA_SMUB") def exit_key(self) -> None: """ Get back the normal screen after an error: send an EXIT key press event """ self.visa_handle.write("display.sendkey(75)") def reset(self) -> None: """ Reset instrument to factory defaults. This resets both channels. """ self.write("reset()") # remember to update all the metadata log.debug("Reset instrument. Re-querying settings...") self.snapshot(update=True) def ask(self, cmd: str) -> str: """ Override of normal ask. This is important, since queries to the instrument must be wrapped in 'print()' """ return super().ask(f"print({cmd:s})") @staticmethod def _scriptwrapper(program: list[str], debug: bool = False) -> str: """ wraps a program so that the output can be put into visa_handle.write and run. The script will run immediately as an anonymous script. Args: program: A list of program instructions. One line per list item, e.g. ['for ii = 1, 10 do', 'print(ii)', 'end' ] debug: log additional debug output """ mainprog = "\r\n".join(program) + "\r\n" wrapped = f"loadandrunscript\r\n{mainprog}endscript" if debug: log.debug("Wrapped the following script:") log.debug(wrapped) return wrapped