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,
VisaInstrumentKWArgs,
)
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
from typing_extensions import Unpack
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
[docs]
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.volt: _MeasurementVoltageParameter = self.add_parameter(
"volt",
parameter_class=_MeasurementVoltageParameter,
label="Voltage",
unit="V",
snapshot_get=False,
)
"""Parameter volt"""
self.curr: _MeasurementCurrentParameter = self.add_parameter(
"curr",
parameter_class=_MeasurementCurrentParameter,
label="Current",
unit="A",
snapshot_get=False,
)
"""Parameter curr"""
self.res: Parameter = self.add_parameter(
"res",
get_cmd=f"{channel}.measure.r()",
get_parser=float,
set_cmd=False,
label="Resistance",
unit="Ohm",
)
"""Parameter res"""
self.mode: Parameter = 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.",
)
"""Selects the output source type. Can be either voltage or current."""
self.output: Parameter = 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),
)
"""Parameter output"""
self.linefreq: Parameter = self.add_parameter(
"linefreq",
label="Line frequency",
get_cmd="localnode.linefreq",
get_parser=float,
set_cmd=False,
unit="Hz",
)
"""Parameter linefreq"""
self.nplc: Parameter = 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),
)
"""Number of power line cycles, used to perform measurements"""
# volt range
# needs get after set (WilliamHPNielsen): why?
self.sourcerange_v: Parameter = 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]),
)
"""The range used when sourcing voltage This affects the range and the precision of the source."""
self.source_autorange_v_enabled: Parameter = 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),
)
"""Set autorange on/off for source voltage."""
self.measurerange_v: Parameter = 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]),
)
"""
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
"""
self.measure_autorange_v_enabled: Parameter = 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),
)
"""Set autorange on/off for measure voltage."""
# current range
# needs get after set
self.sourcerange_i: Parameter = 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]),
)
"""The range used when sourcing current This affects the range and the precision of the source."""
self.source_autorange_i_enabled: Parameter = 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),
)
"""Set autorange on/off for source current."""
self.measurerange_i: Parameter = 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]),
)
"""
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"""
self.measure_autorange_i_enabled: Parameter = 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),
)
"""Set autorange on/off for measure current."""
# Compliance limit
self.limitv: Parameter = 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",
)
"""Voltage limit e.g. the maximum voltage allowed in current mode. If exceeded the current will be clipped."""
# Compliance limit
self.limiti: Parameter = 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",
)
"""Current limit e.g. the maximum current allowed in voltage mode. If exceeded the voltage will be clipped."""
self.fastsweep: LuaSweepParameter = self.add_parameter(
"fastsweep", parameter_class=LuaSweepParameter
)
"""Parameter fastsweep"""
self.timetrace_npts: Parameter = self.add_parameter(
"timetrace_npts",
initial_value=500,
label="Number of points",
get_cmd=None,
set_cmd=None,
)
"""Parameter timetrace_npts"""
self.timetrace_dt: Parameter = self.add_parameter(
"timetrace_dt",
initial_value=1e-3,
label="Time resolution",
unit="s",
get_cmd=None,
set_cmd=None,
)
"""Parameter timetrace_dt"""
self.time_axis: TimeAxis = self.add_parameter(
name="time_axis",
label="Time",
unit="s",
snapshot_value=False,
vals=vals.Arrays(shape=(self.timetrace_npts,)),
parameter_class=TimeAxis,
)
"""Parameter time_axis"""
self.timetrace: TimeTrace = self.add_parameter(
"timetrace",
vals=vals.Arrays(shape=(self.timetrace_npts,)),
setpoints=(self.time_axis,),
parameter_class=TimeTrace,
)
"""Parameter timetrace"""
self.timetrace_mode: Parameter = self.add_parameter(
"timetrace_mode",
initial_value="current",
get_cmd=None,
set_cmd=self.timetrace._set_mode,
vals=vals.Enum("current", "voltage"),
)
"""Parameter timetrace_mode"""
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
[docs]
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)
[docs]
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}")
[docs]
class Keithley2600(VisaInstrument):
"""
This is the base class for all qcodes driver for the Keithley 2600 Source-Meter series.
This class should not be instantiated directly. Rather one of the subclasses for a
specific instrument should be used.
"""
default_terminator = "\n"
def __init__(
self, name: str, address: str, **kwargs: Unpack[VisaInstrumentKWArgs]
) -> None:
"""
Args:
name: Name to use internally in QCoDeS
address: VISA resource address
**kwargs: kwargs are forwarded to base class.
"""
super().__init__(name, address, **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.display_settext: Parameter = self.add_parameter(
"display_settext", set_cmd=self._display_settext, vals=vals.Strings()
)
"""Parameter display_settext"""
self.connect_message()
def _display_settext(self, text: str) -> None:
self.visa_handle.write(f'display.settext("{text}")')
[docs]
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
[docs]
def display_clear(self) -> None:
"""
This function clears the display, but also leaves it in user mode
"""
self.visa_handle.write("display.clear()")
[docs]
def display_normal(self) -> None:
"""
Set the display to the default mode
"""
self.visa_handle.write("display.screen = display.SMUA_SMUB")
[docs]
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)")
[docs]
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)
[docs]
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