Source code for qcodes.instrument_drivers.Keysight.keysightb1500.KeysightB1517A

import re
import textwrap
from typing import TYPE_CHECKING, Any, Literal, cast, overload

import numpy as np
from typing_extensions import NotRequired, TypedDict, Unpack

import qcodes.validators as vals
from qcodes.instrument import InstrumentBaseKWArgs, InstrumentChannel
from qcodes.parameters import Group, GroupParameter, Parameter, ParamRawDataType

from . import constants
from .constants import (
    AAD,
    MM,
    ChNr,
    IMeasRange,
    IOutputRange,
    MeasurementStatus,
    ModuleKind,
    VMeasRange,
    VOutputRange,
)
from .KeysightB1500_module import KeysightB1500Module, parse_spot_measurement_response
from .KeysightB1500_sampling_measurement import SamplingMeasurement
from .message_builder import MessageBuilder

if TYPE_CHECKING:
    from collections.abc import Sequence

    from qcodes.instrument_drivers.Keysight.keysightb1500.KeysightB1500_base import (
        KeysightB1500,
    )


class SweepSteps(TypedDict):
    """
    A dictionary holding all the parameters that specifies the staircase
    sweep (WV).
    """

    chan: NotRequired[int | constants.ChNr]
    sweep_mode: constants.SweepMode | int
    sweep_range: constants.VOutputRange | int
    sweep_start: float
    sweep_end: float
    sweep_steps: int
    current_compliance: float | None
    power_compliance: float | None


[docs] class KeysightB1500IVSweeper(InstrumentChannel): def __init__( self, parent: "KeysightB1517A", name: str, **kwargs: Unpack[InstrumentBaseKWArgs], ): super().__init__(parent, name, **kwargs) self._sweep_step_parameters: SweepSteps = { "sweep_mode": constants.SweepMode.LINEAR, "sweep_range": constants.VOutputRange.AUTO, "sweep_start": 0.0, "sweep_end": 0.0, "sweep_steps": 1, "current_compliance": None, "power_compliance": None, } self.sweep_auto_abort: Parameter = self.add_parameter( name="sweep_auto_abort", set_cmd=self._set_sweep_auto_abort, get_cmd=self._get_sweep_auto_abort, set_parser=constants.Abort, get_parser=constants.Abort, vals=vals.Enum(*list(constants.Abort)), initial_cache_value=constants.Abort.ENABLED, docstring=textwrap.dedent( """ The WM command enables or disables the automatic abort function for the staircase sweep sources and the pulsed sweep source. The automatic abort function stops the measurement when one of the following conditions occurs: - Compliance on the measurement channel - Compliance on the non-measurement channel - Overflow on the AD converter - Oscillation on any channel This command also sets the post measurement condition for the sweep sources. After the measurement is normally completed, the staircase sweep sources force the value specified by the post parameter, and the pulsed sweep source forces the pulse base value. If the measurement is stopped by the automatic abort function, the staircase sweep sources force the start value, and the pulsed sweep source forces the pulse base value after sweep. """ ), ) """ The WM command enables or disables the automatic abort function for the staircase sweep sources and the pulsed sweep source. The automatic abort function stops the measurement when one of the following conditions occurs: - Compliance on the measurement channel - Compliance on the non-measurement channel - Overflow on the AD converter - Oscillation on any channel This command also sets the post measurement condition for the sweep sources. After the measurement is normally completed, the staircase sweep sources force the value specified by the post parameter, and the pulsed sweep source forces the pulse base value. If the measurement is stopped by the automatic abort function, the staircase sweep sources force the start value, and the pulsed sweep source forces the pulse base value after sweep. """ self.post_sweep_voltage_condition: Parameter = self.add_parameter( name="post_sweep_voltage_condition", set_cmd=self._set_post_sweep_voltage_condition, get_cmd=self._get_post_sweep_voltage_condition, set_parser=constants.WM.Post, get_parser=constants.WM.Post, vals=vals.Enum(*list(constants.WM.Post)), initial_cache_value=constants.WM.Post.START, docstring=textwrap.dedent( """ Source output value after the measurement is normally completed. If this parameter is not set, the sweep sources force the start value. """ ), ) """ Source output value after the measurement is normally completed. If this parameter is not set, the sweep sources force the start value. """ self.hold_time: GroupParameter = self.add_parameter( name="hold_time", initial_value=0.0, vals=vals.Numbers(0, 655.35), unit="s", parameter_class=GroupParameter, docstring=textwrap.dedent( """ Hold time (in seconds) that is the wait time after starting measurement and before starting delay time for the first step 0 to 655.35 s, with 10 ms resolution. Numeric expression. """ ), ) """ Hold time (in seconds) that is the wait time after starting measurement and before starting delay time for the first step 0 to 655.35 s, with 10 ms resolution. Numeric expression. """ self.delay: GroupParameter = self.add_parameter( name="delay", initial_value=0.0, vals=vals.Numbers(0, 65.535), unit="s", parameter_class=GroupParameter, docstring=textwrap.dedent( """ Delay time (in seconds) that is the wait time after starting to force a step output and before starting a step measurement. 0 to 65.535 s, with 0.1 ms resolution. Numeric expression. """ ), ) """ Delay time (in seconds) that is the wait time after starting to force a step output and before starting a step measurement. 0 to 65.535 s, with 0.1 ms resolution. Numeric expression. """ self.step_delay: GroupParameter = self.add_parameter( name="step_delay", initial_value=0.0, vals=vals.Numbers(0, 1), unit="s", parameter_class=GroupParameter, docstring=textwrap.dedent( """ Step delay time (in seconds) that is the wait time after starting a step measurement and before starting to force the next step output. 0 to 1 s, with 0.1 ms resolution. Numeric expression. If this parameter is not set, step delay will be 0. If step delay is shorter than the measurement time, the B1500 waits until the measurement completes, then forces the next step output. """ ), ) """ Step delay time (in seconds) that is the wait time after starting a step measurement and before starting to force the next step output. 0 to 1 s, with 0.1 ms resolution. Numeric expression. If this parameter is not set, step delay will be 0. If step delay is shorter than the measurement time, the B1500 waits until the measurement completes, then forces the next step output. """ self.trigger_delay: GroupParameter = self.add_parameter( name="trigger_delay", initial_value=0.0, unit="s", parameter_class=GroupParameter, docstring=textwrap.dedent( """ Step source trigger delay time (in seconds) that is the wait time after completing a step output setup and before sending a step output setup completion trigger. 0 to the value of ``delay`` s, with 0.1 ms resolution. If this parameter is not set, trigger delay will be 0. """ ), ) """ Step source trigger delay time (in seconds) that is the wait time after completing a step output setup and before sending a step output setup completion trigger. 0 to the value of ``delay`` s, with 0.1 ms resolution. If this parameter is not set, trigger delay will be 0. """ self.measure_delay: GroupParameter = self.add_parameter( name="measure_delay", initial_value=0.0, unit="s", vals=vals.Numbers(0, 65.535), parameter_class=GroupParameter, docstring=textwrap.dedent( """ Step measurement trigger delay time (in seconds) that is the wait time after receiving a start step measurement trigger and before starting a step measurement. 0 to 65.535 s, with 0.1 ms resolution. Numeric expression. If this parameter is not set, measure delay will be 0. """ ), ) """ Step measurement trigger delay time (in seconds) that is the wait time after receiving a start step measurement trigger and before starting a step measurement. 0 to 65.535 s, with 0.1 ms resolution. Numeric expression. If this parameter is not set, measure delay will be 0. """ self._set_sweep_delays_group = Group( [ self.hold_time, self.delay, self.step_delay, self.trigger_delay, self.measure_delay, ], set_cmd="WT " "{hold_time}," "{delay}," "{step_delay}," "{trigger_delay}," "{measure_delay}", get_cmd=self._get_sweep_delays(), get_parser=self._get_sweep_delays_parser, ) self.sweep_mode: Parameter = self.add_parameter( name="sweep_mode", set_cmd=self._set_sweep_mode, get_cmd=self._get_sweep_mode, vals=vals.Enum(*list(constants.SweepMode)), set_parser=constants.SweepMode, snapshot_get=False, docstring=textwrap.dedent( """ Sweep mode. Note that Only linear sweep (mode=1 or 3) is available for the staircase sweep with pulsed bias. 1: Linear sweep (single stair, start to stop.) 2: Log sweep (single stair, start to stop.) 3: Linear sweep (double stair, start to stop to start.) 4: Log sweep (double stair, start to stop to start.) """ ), ) """ Sweep mode. Note that Only linear sweep (mode=1 or 3) is available for the staircase sweep with pulsed bias. 1: Linear sweep (single stair, start to stop.) 2: Log sweep (single stair, start to stop.) 3: Linear sweep (double stair, start to stop to start.) 4: Log sweep (double stair, start to stop to start.) """ self.sweep_range: Parameter = self.add_parameter( name="sweep_range", set_cmd=self._set_sweep_range, get_cmd=self._get_sweep_range, vals=vals.Enum(*list(constants.VOutputRange)), set_parser=constants.VOutputRange, snapshot_get=False, docstring=textwrap.dedent( """ Ranging type for staircase sweep voltage output. Integer expression. See Table 4-4 on page 20. The B1500 usually uses the minimum range that covers both start and stop values to force the staircase sweep voltage. However, if you set `power_compliance` and if the following formulas are true, the B1500 uses the minimum range that covers the output value, and changes the output range dynamically (20 V range or above). Range changing may cause 0 V output in a moment. For the limited auto ranging, the instrument never uses the range less than the specified range. - Icomp > maximum current for the output range - Pcomp/output voltage > maximum current for the output range """ ), ) """ Ranging type for staircase sweep voltage output. Integer expression. See Table 4-4 on page 20. The B1500 usually uses the minimum range that covers both start and stop values to force the staircase sweep voltage. However, if you set `power_compliance` and if the following formulas are true, the B1500 uses the minimum range that covers the output value, and changes the output range dynamically (20 V range or above). Range changing may cause 0 V output in a moment. For the limited auto ranging, the instrument never uses the range less than the specified range. - Icomp > maximum current for the output range - Pcomp/output voltage > maximum current for the output range """ self.sweep_start: Parameter = self.add_parameter( name="sweep_start", set_cmd=self._set_sweep_start, get_cmd=self._get_sweep_start, unit="V", vals=vals.Numbers(-25, 25), snapshot_get=False, docstring=textwrap.dedent( """ Start value of the stair case sweep (in V). For the log sweep, start and stop must have the same polarity. """ ), ) """ Start value of the stair case sweep (in V). For the log sweep, start and stop must have the same polarity. """ self.sweep_end: Parameter = self.add_parameter( name="sweep_end", set_cmd=self._set_sweep_end, get_cmd=self._get_sweep_end, unit="V", vals=vals.Numbers(-25, 25), snapshot_get=False, docstring=textwrap.dedent( """ Stop value of the DC bias sweep (in V). For the log sweep,start and stop must have the same polarity. """ ), ) """ Stop value of the DC bias sweep (in V). For the log sweep,start and stop must have the same polarity. """ self.sweep_steps: Parameter = self.add_parameter( name="sweep_steps", set_cmd=self._set_sweep_steps, get_cmd=self._get_sweep_steps, vals=vals.Ints(1, 1001), snapshot_get=False, docstring=textwrap.dedent( """ Number of steps for staircase sweep. Possible values from 1 to 1001""" ), ) """ Number of steps for staircase sweep. Possible values from 1 to 1001 """ self.current_compliance: Parameter = self.add_parameter( name="current_compliance", set_cmd=self._set_current_compliance, get_cmd=self._get_current_compliance, unit="A", vals=vals.Numbers(-40, 40), snapshot_get=False, docstring=textwrap.dedent( """ Current compliance (in A). Refer to Manual 2016. See Table 4-7 on page 24, Table 4-9 on page 26, Table 4-12 on page 27, or Table 4-15 on page 28 for each measurement resource type. If you do not set current_compliance, the previous value is used. Compliance polarity is automatically set to the same polarity as the output value, regardless of the specified Icomp. If the output value is 0, the compliance polarity is positive. If you set Pcomp, the maximum Icomp value for the measurement resource is allowed, regardless of the output range setting. """ ), ) """ Current compliance (in A). Refer to Manual 2016. See Table 4-7 on page 24, Table 4-9 on page 26, Table 4-12 on page 27, or Table 4-15 on page 28 for each measurement resource type. If you do not set current_compliance, the previous value is used. Compliance polarity is automatically set to the same polarity as the output value, regardless of the specified Icomp. If the output value is 0, the compliance polarity is positive. If you set Pcomp, the maximum Icomp value for the measurement resource is allowed, regardless of the output range setting. """ self.power_compliance: Parameter = self.add_parameter( name="power_compliance", set_cmd=self._set_power_compliance, get_cmd=self._get_power_compliance, unit="W", vals=vals.Numbers(0.001, 80), snapshot_get=False, docstring=textwrap.dedent( """ Power compliance (in W). Resolution: 0.001 W. If it is not entered, the power compliance is not set. This parameter is not available for HVSMU. 0.001 to 2 for MPSMU/HRSMU, 0.001 to 20 for HPSMU, 0.001 to 40 for HCSMU, 0.001 to 80 for dual HCSMU, 0.001 to 3 for MCSMU, 0.001 to 100 for UHVU """ ), ) """ Power compliance (in W). Resolution: 0.001 W. If it is not entered, the power compliance is not set. This parameter is not available for HVSMU. 0.001 to 2 for MPSMU/HRSMU, 0.001 to 20 for HPSMU, 0.001 to 40 for HCSMU, 0.001 to 80 for dual HCSMU, 0.001 to 3 for MCSMU, 0.001 to 100 for UHVU """ def _set_sweep_mode(self, value: constants.SweepMode) -> None: self._sweep_step_parameters["sweep_mode"] = value self._set_from_sweep_step_parameters() def _get_sweep_mode(self) -> constants.SweepMode: mode_val = self._get_sweep_steps_parameters("sweep_mode") return constants.SweepMode(mode_val) def _set_sweep_range(self, value: constants.VOutputRange) -> None: self._sweep_step_parameters["sweep_range"] = value self._set_from_sweep_step_parameters() def _get_sweep_range(self) -> constants.VOutputRange: range_val = self._get_sweep_steps_parameters("sweep_range") return constants.VOutputRange(range_val) def _set_sweep_start(self, value: float) -> None: self._sweep_step_parameters["sweep_start"] = value self._set_from_sweep_step_parameters() def _get_sweep_start(self) -> float: sweep_start = self._get_sweep_steps_parameters("sweep_start") return sweep_start def _set_sweep_end(self, value: float) -> None: self._sweep_step_parameters["sweep_end"] = value self._set_from_sweep_step_parameters() def _get_sweep_end(self) -> float: sweep_end = self._get_sweep_steps_parameters("sweep_end") return sweep_end def _set_sweep_steps(self, value: int) -> None: self._sweep_step_parameters["sweep_steps"] = value self._set_from_sweep_step_parameters() def _get_sweep_steps(self) -> int: sweep_steps = self._get_sweep_steps_parameters("sweep_steps") return sweep_steps def _set_current_compliance(self, value: float | None) -> None: self._sweep_step_parameters["current_compliance"] = value self._set_from_sweep_step_parameters() def _get_current_compliance(self) -> float | None: current_compliance = self._get_sweep_steps_parameters("current_compliance") return current_compliance def _set_power_compliance(self, value: float | None) -> None: if self._sweep_step_parameters["current_compliance"] is None: raise ValueError( "Current compliance must be set before setting power compliance" ) self._sweep_step_parameters["power_compliance"] = value self._set_from_sweep_step_parameters() def _get_power_compliance(self) -> float | None: power_compliance = self._get_sweep_steps_parameters("power_compliance") return power_compliance def _set_from_sweep_step_parameters(self) -> None: msg = MessageBuilder().wv( chnum=self.parent.channels[0], mode=self._sweep_step_parameters["sweep_mode"], v_range=self._sweep_step_parameters["sweep_range"], start=self._sweep_step_parameters["sweep_start"], stop=self._sweep_step_parameters["sweep_end"], step=self._sweep_step_parameters["sweep_steps"], i_comp=self._sweep_step_parameters["current_compliance"], p_comp=self._sweep_step_parameters["power_compliance"], ) self.write(msg.message) @staticmethod def _get_sweep_delays() -> str: msg = MessageBuilder().lrn_query( type_id=constants.LRN.Type.STAIRCASE_SWEEP_MEASUREMENT_SETTINGS ) cmd = msg.message return cmd @staticmethod def _get_sweep_delays_parser(response: str) -> dict[str, float]: match = re.search( "WT(?P<hold_time>.+?),(?P<delay>.+?)," "(?P<step_delay>.+?),(?P<trigger_delay>.+?)," "(?P<measure_delay>.+?)(;|$)", response, ) if not match: raise ValueError("Sweep delays (WT) not found.") resp_dict = match.groupdict() out_dict = {key: float(value) for key, value in resp_dict.items()} return out_dict def _set_sweep_auto_abort(self, val: bool | constants.Abort) -> None: msg = MessageBuilder().wm(abort=val) self.write(msg.message) def _set_post_sweep_voltage_condition(self, val: constants.WM.Post | int) -> None: msg = MessageBuilder().wm(abort=self.sweep_auto_abort(), post=val) self.write(msg.message) def _get_sweep_auto_abort_setting(self) -> dict[str, str]: msg = MessageBuilder().lrn_query( type_id=constants.LRN.Type.STAIRCASE_SWEEP_MEASUREMENT_SETTINGS ) response = self.ask(msg.message) match = re.search( r"WM(?P<abort_function>.+?),(?P<output_after_sweep>.+?)(;|$)", response, ) if match is None: raise RuntimeError( "Did not find expected response for sweep auto abort settings" ) resp_dict = match.groupdict() return resp_dict def _get_sweep_auto_abort(self) -> int: resp_dict = self._get_sweep_auto_abort_setting() return int(resp_dict["abort_function"]) def _get_post_sweep_voltage_condition(self) -> int: resp_dict = self._get_sweep_auto_abort_setting() return int(resp_dict["output_after_sweep"]) @overload def _get_sweep_steps_parameters( self, name: Literal["chan"] ) -> int | constants.ChNr: ... @overload def _get_sweep_steps_parameters( self, name: Literal["sweep_mode"] ) -> constants.SweepMode | int: ... @overload def _get_sweep_steps_parameters( self, name: Literal["sweep_range"] ) -> constants.VOutputRange | int: ... @overload def _get_sweep_steps_parameters( self, name: Literal["sweep_start", "sweep_end"] ) -> float: ... @overload def _get_sweep_steps_parameters(self, name: Literal["sweep_steps"]) -> int: ... @overload def _get_sweep_steps_parameters( self, name: Literal["current_compliance", "power_compliance"] ) -> float | None: ... def _get_sweep_steps_parameters( self, name: Literal[ "chan", "sweep_mode", "sweep_range", "sweep_start", "sweep_end", "sweep_steps", "current_compliance", "power_compliance", ], ) -> ( constants.ChNr | constants.SweepMode | constants.VOutputRange | int | float | None ): msg = MessageBuilder().lrn_query( type_id=constants.LRN.Type.STAIRCASE_SWEEP_MEASUREMENT_SETTINGS ) cmd = msg.message response = self.ask(cmd) out_dict = self._get_sweep_steps_parser(response) if out_dict.get("chan") != self.parent.channels[0]: raise ValueError( "Sweep parameters (WV) such as " "sweep_mode, sweep_range, sweep_start, " "sweep_end, sweep_steps etc are not set for " "this SMU." ) return out_dict.get(name) @staticmethod def _get_sweep_steps_parser(response: str) -> SweepSteps: match = re.search( r"WV(?P<chan>.+?)," r"(?P<sweep_mode>.+?)," r"(?P<sweep_range>.+?)," r"(?P<sweep_start>.+?)," r"(?P<sweep_end>.+?)," r"(?P<sweep_steps>.+?)" r"(,(?P<current_compliance>.+?)|;|$)" r"(,(?P<power_compliance>.+?)|;|$)" r"(;|$)", response, ) if not match: raise ValueError("Sweep steps (WV) not found.") resp_dict = match.groupdict() if resp_dict["current_compliance"] is not None: current_output = float(resp_dict["current_compliance"]) else: current_output = None if resp_dict["power_compliance"] is not None: power_compliance = float(resp_dict["power_compliance"]) else: power_compliance = None out_dict: SweepSteps = { "chan": int(resp_dict["chan"]), "sweep_mode": int(resp_dict["sweep_mode"]), "sweep_range": int(resp_dict["sweep_range"]), "sweep_start": float(resp_dict["sweep_start"]), "sweep_end": float(resp_dict["sweep_end"]), "sweep_steps": int(resp_dict["sweep_steps"]), "current_compliance": current_output, "power_compliance": power_compliance, } return out_dict
IVSweeper = KeysightB1500IVSweeper """ Alias for backwards compatibility """ class _ParameterWithStatus(Parameter): def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) self._measurement_status: MeasurementStatus | None = None @property def measurement_status(self) -> MeasurementStatus | None: return self._measurement_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 _SpotMeasurementVoltageParameter(_ParameterWithStatus): def set_raw(self, value: ParamRawDataType) -> None: smu = cast("KeysightB1517A", self.instrument) if smu._source_config["output_range"] is None: smu._source_config["output_range"] = constants.VOutputRange.AUTO if not isinstance(smu._source_config["output_range"], constants.VOutputRange): raise TypeError( "Asking to force voltage, but source_config contains a " "current output range" ) msg = MessageBuilder().dv( chnum=smu.channels[0], v_range=smu._source_config["output_range"], voltage=value, i_comp=smu._source_config["compliance"], comp_polarity=smu._source_config["compl_polarity"], i_range=smu._source_config["min_compliance_range"], ) smu.write(msg.message) smu.root_instrument._reset_measurement_statuses_of_smu_spot_measurement_parameters( "voltage" ) def get_raw(self) -> ParamRawDataType: smu = cast("KeysightB1517A", self.instrument) msg = MessageBuilder().tv( chnum=smu.channels[0], v_range=smu._measure_config["v_measure_range"], ) response = smu.ask(msg.message) parsed = parse_spot_measurement_response(response) self._measurement_status = parsed["status"] return parsed["value"] class _SpotMeasurementCurrentParameter(_ParameterWithStatus): def set_raw(self, value: ParamRawDataType) -> None: smu = cast("KeysightB1517A", self.instrument) if smu._source_config["output_range"] is None: smu._source_config["output_range"] = constants.IOutputRange.AUTO if not isinstance(smu._source_config["output_range"], constants.IOutputRange): raise TypeError( "Asking to force current, but source_config contains a " "voltage output range" ) msg = MessageBuilder().di( chnum=smu.channels[0], i_range=smu._source_config["output_range"], current=value, v_comp=smu._source_config["compliance"], comp_polarity=smu._source_config["compl_polarity"], v_range=smu._source_config["min_compliance_range"], ) smu.write(msg.message) smu.root_instrument._reset_measurement_statuses_of_smu_spot_measurement_parameters( "current" ) def get_raw(self) -> ParamRawDataType: smu = cast("KeysightB1517A", self.instrument) msg = MessageBuilder().ti( chnum=smu.channels[0], i_range=smu._measure_config["i_measure_range"], ) response = smu.ask(msg.message) parsed = parse_spot_measurement_response(response) self._measurement_status = parsed["status"] return parsed["value"]
[docs] class KeysightB1517A(KeysightB1500Module): """ Driver for Keysight B1517A Source/Monitor Unit module for B1500 Semiconductor Parameter Analyzer. Args: parent: mainframe B1500 instance that this module belongs to name: Name of the instrument instance to create. If `None` (Default), then the name is autogenerated from the instrument class. slot_nr: Slot number of this module (not channel number) """ MODULE_KIND = ModuleKind.SMU _interval_validator = vals.Numbers(0.0001, 65.535) def __init__( self, parent: "KeysightB1500", name: str | None, slot_nr: int, **kwargs: Unpack[InstrumentBaseKWArgs], ): super().__init__(parent, name, slot_nr, **kwargs) self.channels = (ChNr(slot_nr),) self._measure_config: dict[str, Any | None] = { k: None for k in ( "v_measure_range", "i_measure_range", ) } self._source_config: dict[str, Any | None] = { k: None for k in ( "output_range", "compliance", "compl_polarity", "min_compliance_range", ) } self._timing_parameters: dict[str, Any | None] = { k: None for k in ("h_bias", "interval", "number", "h_base") } # We want to snapshot these configuration dictionaries self._meta_attrs += ["_measure_config", "_source_config", "_timing_parameters"] self.add_submodule("iv_sweep", KeysightB1500IVSweeper(self, "iv_sweep")) self.setup_fnc_already_run: bool = False self.power_line_frequency: int = 50 self._average_coefficient: int = 1 self._valid_v_measure_ranges: list[VMeasRange] = [ VMeasRange.AUTO, VMeasRange.MIN_0V5, VMeasRange.MIN_2V, VMeasRange.MIN_5V, VMeasRange.MIN_20V, VMeasRange.MIN_40V, VMeasRange.MIN_100V, VMeasRange.FIX_0V5, VMeasRange.FIX_2V, VMeasRange.FIX_5V, VMeasRange.FIX_20V, VMeasRange.FIX_40V, VMeasRange.FIX_100V, ] self._valid_i_measure_ranges: list[IMeasRange] = [ IMeasRange.AUTO, IMeasRange.MIN_1pA, IMeasRange.MIN_10pA, IMeasRange.MIN_100pA, IMeasRange.MIN_1nA, IMeasRange.MIN_10nA, IMeasRange.MIN_100nA, IMeasRange.MIN_1uA, IMeasRange.MIN_10uA, IMeasRange.MIN_100uA, IMeasRange.MIN_1mA, IMeasRange.MIN_10mA, IMeasRange.MIN_100mA, IMeasRange.FIX_1pA, IMeasRange.FIX_10pA, IMeasRange.FIX_100pA, IMeasRange.FIX_1nA, IMeasRange.FIX_10nA, IMeasRange.FIX_100nA, IMeasRange.FIX_1uA, IMeasRange.FIX_10uA, IMeasRange.FIX_100uA, IMeasRange.FIX_1mA, IMeasRange.FIX_10mA, IMeasRange.FIX_100mA, ] self._valid_v_output_ranges: list[VOutputRange] = [ VOutputRange.AUTO, VOutputRange.MIN_0V5, VOutputRange.MIN_2V, VOutputRange.MIN_5V, VOutputRange.MIN_20V, VOutputRange.MIN_40V, VOutputRange.MIN_100V, ] self._valid_i_output_ranges: list[IOutputRange] = [ IOutputRange.AUTO, IOutputRange.MIN_1pA, IOutputRange.MIN_10pA, IOutputRange.MIN_100pA, IOutputRange.MIN_1nA, IOutputRange.MIN_10nA, IOutputRange.MIN_100nA, IOutputRange.MIN_1uA, IOutputRange.MIN_10uA, IOutputRange.MIN_100uA, IOutputRange.MIN_1mA, IOutputRange.MIN_10mA, IOutputRange.MIN_100mA, ] self.measurement_mode: Parameter = self.add_parameter( name="measurement_mode", get_cmd=None, set_cmd=self._set_measurement_mode, set_parser=MM.Mode, vals=vals.Enum(*list(MM.Mode)), initial_cache_value=MM.Mode.SPOT, docstring=textwrap.dedent( """ Set measurement mode for this module. It is recommended for this parameter to use values from :class:`.constants.MM.Mode` enumeration. Refer to the documentation of ``MM`` command in the programming guide for more information.""" ), ) """ Set measurement mode for this module. It is recommended for this parameter to use values from :class:`.constants.MM.Mode` enumeration. Refer to the documentation of ``MM`` command in the programming guide for more information. """ # Instrument is initialized with this setting having value of # `1`, spot measurement mode, hence let's set the parameter's cache to # this value since it is not possible to request this value from the # instrument. self.measurement_operation_mode: Parameter = self.add_parameter( name="measurement_operation_mode", set_cmd=self._set_measurement_operation_mode, get_cmd=self._get_measurement_operation_mode, set_parser=constants.CMM.Mode, vals=vals.Enum(*list(constants.CMM.Mode)), docstring=textwrap.dedent( """ The methods sets the SMU measurement operation mode. This is not available for the high speed spot measurement. mode : SMU measurement operation mode. `constants.CMM.Mode` """ ), ) """ The methods sets the SMU measurement operation mode. This is not available for the high speed spot measurement. mode : SMU measurement operation mode. `constants.CMM.Mode` """ self.voltage: _SpotMeasurementVoltageParameter = self.add_parameter( name="voltage", parameter_class=_SpotMeasurementVoltageParameter, unit="V", snapshot_get=False, ) """Parameter voltage""" self.current: _SpotMeasurementCurrentParameter = self.add_parameter( name="current", parameter_class=_SpotMeasurementCurrentParameter, unit="A", snapshot_get=False, ) """Parameter current""" self.time_axis: Parameter = self.add_parameter( name="time_axis", get_cmd=self._get_time_axis, vals=vals.Arrays(shape=(self._get_number_of_samples,)), snapshot_value=False, label="Time", unit="s", ) """Parameter time_axis""" self.sampling_measurement_trace: SamplingMeasurement = self.add_parameter( name="sampling_measurement_trace", parameter_class=SamplingMeasurement, vals=vals.Arrays(shape=(self._get_number_of_samples,)), setpoints=(self.time_axis,), ) """Parameter sampling_measurement_trace""" self.current_measurement_range: Parameter = self.add_parameter( name="current_measurement_range", set_cmd=self._set_current_measurement_range, get_cmd=self._get_current_measurement_range, vals=vals.Enum(*list(constants.IMeasRange)), set_parser=constants.IMeasRange, docstring=textwrap.dedent( """ This method specifies the current measurement range or ranging type.In the initial setting, the auto ranging is set. The range changing occurs immediately after the trigger (that is, during the measurements). Current measurement channel can be decided by the `measurement_operation_mode` method setting and the channel output mode (voltage or current). """ ), ) """ This method specifies the current measurement range or ranging type.In the initial setting, the auto ranging is set. The range changing occurs immediately after the trigger (that is, during the measurements). Current measurement channel can be decided by the `measurement_operation_mode` method setting and the channel output mode (voltage or current). """ self.enable_filter: Parameter = self.add_parameter( name="enable_filter", set_cmd=self._set_enable_filter, get_cmd=None, snapshot_get=False, vals=vals.Bool(), initial_cache_value=False, docstring=textwrap.dedent( """ This methods sets the connection mode of a SMU filter for each channel. A filter is mounted on the SMU. It assures clean source output with no spikes or overshooting. ``False``, meaning "disconnect" is the initial setting. Set to ``True`` to connect. """ ), ) """ This methods sets the connection mode of a SMU filter for each channel. A filter is mounted on the SMU. It assures clean source output with no spikes or overshooting. ``False``, meaning "disconnect" is the initial setting. Set to ``True`` to connect. """ def _get_number_of_samples(self) -> int: if self._timing_parameters["number"] is not None: sample_number = self._timing_parameters["number"] return sample_number else: raise Exception("set timing parameters first") def _get_time_axis(self) -> np.ndarray: sample_rate = self._timing_parameters["interval"] total_time = self._total_measurement_time() time_xaxis: np.ndarray = np.arange(0, total_time, sample_rate) return time_xaxis def _total_measurement_time(self) -> float: if ( self._timing_parameters["interval"] is None or self._timing_parameters["number"] is None ): raise Exception("set timing parameters first") sample_number = self._timing_parameters["number"] sample_rate = self._timing_parameters["interval"] total_time = float(sample_rate * sample_number) return total_time def _set_current_measurement_range( self, i_range: constants.IMeasRange | int ) -> None: msg = MessageBuilder().ri(chnum=self.channels[0], i_range=i_range) self.write(msg.message) def _get_current_measurement_range( self, ) -> list[tuple[constants.ChNr, constants.IMeasRange]]: response = self.ask( MessageBuilder() .lrn_query(type_id=constants.LRN.Type.MEASUREMENT_RANGING_STATUS) .message ) match = re.findall(r"RI (.+?),(.+?)($|;)", response) response_list = [ (constants.ChNr(int(i)), constants.IMeasRange(int(j))) for i, j, _ in match ] return response_list def _set_measurement_mode(self, mode: MM.Mode | int) -> None: self.write(MessageBuilder().mm(mode=mode, channels=[self.channels[0]]).message) def _set_measurement_operation_mode(self, mode: constants.CMM.Mode | int) -> None: self.write(MessageBuilder().cmm(mode=mode, chnum=self.channels[0]).message) def _get_measurement_operation_mode( self, ) -> list[tuple[constants.ChNr, constants.CMM.Mode]]: response = self.ask( MessageBuilder() .lrn_query(type_id=constants.LRN.Type.SMU_MEASUREMENT_OPERATION) .message ) match = re.findall(r"CMM (.+?),(.+?)($|;)", response) response_list = [ (constants.ChNr(int(i)), constants.CMM.Mode(int(j))) for i, j, _ in match ] return response_list def _set_enable_filter( self, enable_filter: bool, ) -> None: """ This methods sets the connection mode of a SMU filter for each channel. A filter is mounted on the SMU. It assures clean source output with no spikes or overshooting. Args: enable_filter : Status of the filter. False: Disconnect (initial setting). True: Connect. """ self.root_instrument.enable_smu_filters( enable_filter=enable_filter, channels=[self.channels[0]] )
[docs] def source_config( self, output_range: constants.OutputRange, compliance: float | int | None = None, compl_polarity: constants.CompliancePolarityMode | None = None, min_compliance_range: constants.MeasureRange | None = None, ) -> None: """Configure sourcing voltage/current Args: output_range: voltage/current output range compliance: voltage/current compliance value compl_polarity: compliance polarity mode min_compliance_range: minimum voltage/current compliance output range """ if min_compliance_range is not None: if isinstance(min_compliance_range, type(output_range)): raise TypeError( "When forcing voltage, min_compliance_range must be an " "current output range (and vice versa)." ) if isinstance(output_range, VOutputRange): if output_range not in self._valid_v_output_ranges: raise RuntimeError("Invalid Source Voltage Output Range") if isinstance(output_range, IOutputRange): if output_range not in self._valid_i_output_ranges: raise RuntimeError("Invalid Source Current Output Range") self._source_config = { "output_range": output_range, "compliance": compliance, "compl_polarity": compl_polarity, "min_compliance_range": min_compliance_range, }
[docs] def v_measure_range_config(self, v_measure_range: constants.VMeasRange) -> None: """Configure measuring voltage Args: v_measure_range: voltage measurement range """ if not isinstance(v_measure_range, constants.VMeasRange): raise TypeError( f"Expected valid voltage measurement range, got {v_measure_range}." ) if v_measure_range not in self._valid_v_measure_ranges: raise RuntimeError( f"{v_measure_range} voltage measurement " f"range is invalid for the device. Valid " f"ranges are {self._valid_v_measure_ranges}." ) self._measure_config["v_measure_range"] = v_measure_range
[docs] def i_measure_range_config(self, i_measure_range: constants.IMeasRange) -> None: """Configure measuring current Args: i_measure_range: current measurement range """ if not isinstance(i_measure_range, constants.IMeasRange): raise TypeError( f"Expected valid current measurement range, got {i_measure_range}." ) if i_measure_range not in self._valid_i_measure_ranges: raise RuntimeError( f"{i_measure_range} current measurement " f"range is invalid for the device. Valid " f"ranges are {self._valid_i_measure_ranges}." ) self._measure_config["i_measure_range"] = i_measure_range
[docs] def timing_parameters( self, h_bias: float, interval: float, number: int, h_base: float | None = None ) -> None: """ This command sets the timing parameters of the sampling measurement mode (:attr:`.MM.Mode.SAMPLING`, ``10``). Refer to the programming guide for more information about the ``MT`` command, especially for notes on sampling operation and about setting interval < 0.002 s. Args: h_bias: Time since the bias value output until the first sampling point. Numeric expression. in seconds. 0 (initial setting) to 655.35 s, resolution 0.01 s. The following values are also available for interval < 0.002 s. ``|h_bias|`` will be the time since the sampling start until the bias value output. -0.09 to -0.0001 s, resolution 0.0001 s. interval: Interval of the sampling. Numeric expression, 0.0001 to 65.535, in seconds. Initial value is 0.002. Resolution is 0.001 at interval < 0.002. Linear sampling of interval < 0.002 in 0.00001 resolution is available only when the following formula is satisfied. ``interval >= 0.0001 + 0.00002 * (number of measurement channels-1)`` number: Number of samples. Integer expression. 1 to the following value. Initial value is 1000. For the linear sampling: ``100001 / (number of measurement channels)``. For the log sampling: ``1 + (number of data for 11 decades)`` h_base: Hold time of the base value output until the bias value output. Numeric expression. in seconds. 0 (initial setting) to 655.35 s, resolution 0.01 s. """ # The duplication of kwargs in the calls below is due to the # difference in type annotations between ``MessageBuilder.mt()`` # method and ``_timing_parameters`` attribute. self._interval_validator.validate(interval) self._timing_parameters.update( h_bias=h_bias, interval=interval, number=number, h_base=h_base ) self.write( MessageBuilder() .mt(h_bias=h_bias, interval=interval, number=number, h_base=h_base) .message )
[docs] def use_high_speed_adc(self) -> None: """Use high-speed ADC type for this module/channel""" self.write( MessageBuilder() .aad(chnum=self.channels[0], adc_type=AAD.Type.HIGH_SPEED) .message )
[docs] def use_high_resolution_adc(self) -> None: """Use high-resolution ADC type for this module/channel""" self.write( MessageBuilder() .aad(chnum=self.channels[0], adc_type=AAD.Type.HIGH_RESOLUTION) .message )
[docs] def set_average_samples_for_high_speed_adc( self, number: int = 1, mode: constants.AV.Mode = constants.AV.Mode.AUTO ) -> None: """ This command sets the number of averaging samples of the high-speed ADC (A/D converter). This command is not effective for the high-resolution ADC. Also, this command is not effective for the measurements using pulse. Args: number: 1 to 1023, or -1 to -100. Initial setting is 1. For positive number input, this value specifies the number of samples depended on the mode value. For negative number input, this parameter specifies the number of power line cycles (PLC) for one point measurement. The Keysight B1500 gets 128 samples in 1 PLC. If number is negative it ignores the mode argument. mode : Averaging mode. Integer expression. This parameter is meaningless for negative number. `constants.AV.Mode.AUTO`: Auto mode (default setting). Number of samples = number x initial number. `constants.AV.Mode.MANUAL`: Manual mode. Number of samples = number """ self.write(MessageBuilder().av(number=number, mode=mode).message) self._average_coefficient = number
[docs] def setup_staircase_sweep( self, v_start: float, v_end: float, n_steps: int, post_sweep_voltage_val: constants.WMDCV.Post | int = constants.WMDCV.Post.STOP, av_coef: int = -1, enable_filter: bool = True, v_src_range: constants.OutputRange = constants.VOutputRange.AUTO, i_comp: float = 10e-6, i_meas_range: constants.MeasureRange | None = constants.IMeasRange.FIX_10uA, hold_time: float = 0, delay: float = 0, step_delay: float = 0, measure_delay: float = 0, abort_enabled: constants.Abort | int = constants.Abort.ENABLED, sweep_mode: constants.SweepMode | int = constants.SweepMode.LINEAR, ) -> None: """ Setup the staircase sweep measurement using the same set of commands (in the same order) as given in the programming manual - see pages 3-19 and 3-20. Args: v_start: starting voltage of staircase sweep v_end: ending voltage of staircase sweep n_steps: number of measurement points (uniformly distributed between v_start and v_end) post_sweep_voltage_val: voltage to hold at end of sweep (i.e. start or end val). Sweep chan will also output this voltage if an abort condition is encountered during the sweep av_coef: coefficient to use for av command to set ADC averaging. Negative value implies NPLC mode with absolute value of av_coeff the NPLC setting to use. Positive value implies auto mode and must be set to >= 4 enable_filter: turn SMU filter on or off v_src_range: range setting to use for voltage source i_comp: current compliance level i_meas_range: current measurement range hold_time: time (in s) to wait before starting very first measurement in sweep delay: time (in s) after starting to force a step output and before starting a step measurement step_delay: time (in s) after starting a step measurement before next step in staircase. If step_delay is < measurement time, B1500 waits until measurement complete and then forces the next step value. measure_delay: time (in s) after receiving a start step measurement trigger and before starting a step measurement abort_enabled: Enbale abort sweep_mode: Linear, log, linear-2-way or log-2-way """ self.set_average_samples_for_high_speed_adc(av_coef) self.enable_filter(enable_filter) self.source_config( output_range=v_src_range, compliance=i_comp, min_compliance_range=i_meas_range, ) self.voltage(v_start) self.measurement_operation_mode(constants.CMM.Mode.COMPLIANCE_SIDE) self.current_measurement_range(i_meas_range) self.iv_sweep.hold_time(hold_time) self.iv_sweep.delay(delay) self.iv_sweep.step_delay(step_delay) self.iv_sweep.measure_delay(measure_delay) self.iv_sweep.sweep_auto_abort(abort_enabled) self.iv_sweep.post_sweep_voltage_condition(post_sweep_voltage_val) self.iv_sweep.sweep_mode(sweep_mode) self.iv_sweep.sweep_range(v_src_range) self.iv_sweep.sweep_start(v_start) self.iv_sweep.sweep_end(v_end) self.iv_sweep.sweep_steps(n_steps) self.iv_sweep.current_compliance(i_comp) self.root_instrument.clear_timer_count() self.setup_fnc_already_run = True
B1517A = KeysightB1517A """ Alias for backwards compatibility """