import re
import textwrap
from typing import TYPE_CHECKING, Any
import numpy as np
import qcodes.validators as vals
from qcodes.instrument import InstrumentBaseKWArgs, InstrumentChannel
from qcodes.parameters import Group, GroupParameter, MultiParameter, Parameter
from . import constants
from .constants import MM, ChNr, ModuleKind
from .KeysightB1500_module import (
KeysightB1500Module,
StatusMixin,
_DCORRResponse,
_FMTResponse,
convert_dummy_val_to_nan,
fixed_negative_float,
fmt_response_base_parser,
format_dcorr_response,
get_name_label_unit_of_impedance_model,
parse_dcorr_query_response,
parse_dcv_measurement_response,
)
from .message_builder import MessageBuilder
if TYPE_CHECKING:
from typing_extensions import Unpack
from qcodes.instrument_drivers.Keysight.keysightb1500.KeysightB1500_base import (
KeysightB1500,
)
_pattern = re.compile(
r"((?P<status>\w)(?P<chnr>\w)(?P<dtype>\w))?"
r"(?P<value>[+-]\d{1,3}\.\d{3,6}E[+-]\d{2})"
)
[docs]
class KeysightB1500CVSweeper(InstrumentChannel):
def __init__(
self,
parent: "KeysightB1520A",
name: str,
**kwargs: "Unpack[InstrumentBaseKWArgs]",
):
super().__init__(parent, name, **kwargs)
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(
"""
enables or disables the automatic abort function
for the CV (DC bias) sweep measurement (MM18) and
the pulsed bias sweep measurement (MM20). The
automatic abort function stops the measurement
when one of the following conditions occurs:
- NULL loop unbalance condition
- IV amplifier saturation condition
- Overflow on the AD converter
"""
),
)
"""
enables or disables the automatic abort function
for the CV (DC bias) sweep measurement (MM18) and
the pulsed bias sweep measurement (MM20). The
automatic abort function stops the measurement
when one of the following conditions occurs:
- NULL loop unbalance condition
- IV amplifier saturation condition
- Overflow on the AD converter
"""
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.WMDCV.Post,
get_parser=constants.WMDCV.Post,
vals=vals.Enum(*list(constants.WMDCV.Post)),
initial_cache_value=constants.WMDCV.Post.START,
docstring=textwrap.dedent(
"""
This command also sets the post measurement
condition of the MFCMU. After the measurement is
normally completed, the DC bias sweep source
forces the value specified by the post parameter,
and the pulsed bias sweep source forces
the pulse base value.
If the measurement is stopped by the automatic
abort function, the DC bias sweep source forces
the start value, and the pulsed bias sweep source
forces the pulse base value after sweep.
"""
),
)
"""
This command also sets the post measurement
condition of the MFCMU. After the measurement is
normally completed, the DC bias sweep source
forces the value specified by the post parameter,
and the pulsed bias sweep source forces
the pulse base value.
If the measurement is stopped by the automatic
abort function, the DC bias sweep source forces
the start value, and the pulsed bias sweep source
forces the pulse base value after sweep.
"""
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, 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, 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,
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,
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,
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,
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``,
with 0.1 ms resolution. Numeric expression. 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``,
with 0.1 ms resolution. Numeric expression. 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, 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, 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="WTDCV "
"{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: GroupParameter = self.add_parameter(
name="sweep_mode",
initial_value=constants.SweepMode.LINEAR,
vals=vals.Enum(*list(constants.SweepMode)),
set_parser=constants.SweepMode,
parameter_class=GroupParameter,
docstring=textwrap.dedent(
"""
Sweep mode.
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.
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_start: GroupParameter = self.add_parameter(
name="sweep_start",
initial_value=0.0,
unit="V",
vals=vals.Numbers(-25, 25),
parameter_class=GroupParameter,
docstring=textwrap.dedent(
"""
Start value of the DC bias sweep (in V). For the log sweep,
start and stop must have the same polarity.
"""
),
)
"""
Start value of the DC bias sweep (in V). For the log sweep,
start and stop must have the same polarity.
"""
self.sweep_end: GroupParameter = self.add_parameter(
name="sweep_end",
initial_value=0.0,
unit="V",
vals=vals.Numbers(-25, 25),
parameter_class=GroupParameter,
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: GroupParameter = self.add_parameter(
name="sweep_steps",
initial_value=1,
vals=vals.Ints(1, 1001),
parameter_class=GroupParameter,
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._chan: GroupParameter = self.add_parameter(
name="_chan",
initial_value=self.parent.channels[0],
parameter_class=GroupParameter,
)
"""Parameter _chan"""
self._set_sweep_steps_group = Group(
[
self._chan,
self.sweep_mode,
self.sweep_start,
self.sweep_end,
self.sweep_steps,
],
set_cmd="WDCV "
"{_chan},"
"{sweep_mode},"
"{sweep_start},"
"{sweep_end},"
"{sweep_steps}",
get_cmd=self._get_sweep_steps(),
get_parser=self._get_sweep_steps_parser,
)
@staticmethod
def _get_sweep_delays() -> str:
msg = MessageBuilder().lrn_query(
type_id=constants.LRN.Type.CV_DC_BIAS_SWEEP_MEASUREMENT_SETTINGS
)
cmd = msg.message
return cmd
@staticmethod
def _get_sweep_delays_parser(response: str) -> dict[str, float]:
match = re.search(
"WTDCV(?P<hold_time>.+?),(?P<delay>.+?),"
"(?P<step_delay>.+?),(?P<trigger_delay>.+?),"
"(?P<measure_delay>.+?)(;|$)",
response,
)
if not match:
raise ValueError("Sweep delays (WTDCV) not found.")
out_str = match.groupdict()
out_dict = {key: float(value) for key, value in out_str.items()}
return out_dict
@staticmethod
def _get_sweep_steps() -> str:
msg = MessageBuilder().lrn_query(
type_id=constants.LRN.Type.CV_DC_BIAS_SWEEP_MEASUREMENT_SETTINGS
)
cmd = msg.message
return cmd
@staticmethod
def _get_sweep_steps_parser(response: str) -> dict[str, int | float]:
match = re.search(
r"WDCV(?P<_chan>.+?),(?P<sweep_mode>.+?),"
r"(?P<sweep_start>.+?),(?P<sweep_end>.+?),"
r"(?P<sweep_steps>.+?)(;|$)",
response,
)
if not match:
raise ValueError("Sweep steps (WDCV) not found.")
resp_dict = match.groupdict()
out_dict: dict[str, int | float] = {}
out_dict["_chan"] = int(resp_dict["_chan"])
out_dict["sweep_mode"] = int(resp_dict["sweep_mode"])
out_dict["sweep_start"] = fixed_negative_float(resp_dict["sweep_start"])
out_dict["sweep_end"] = fixed_negative_float(resp_dict["sweep_end"])
out_dict["sweep_steps"] = int(resp_dict["sweep_steps"])
return out_dict
def _set_sweep_auto_abort(self, val: bool | constants.Abort) -> None:
msg = MessageBuilder().wmdcv(abort=val)
self.write(msg.message)
def _set_post_sweep_voltage_condition(
self, val: constants.WMDCV.Post | int
) -> None:
msg = MessageBuilder().wmdcv(abort=self.sweep_auto_abort(), post=val)
self.write(msg.message)
def _get_sweep_auto_abort_settings(self) -> dict[str, str]:
msg = MessageBuilder().lrn_query(
type_id=constants.LRN.Type.CV_DC_BIAS_SWEEP_MEASUREMENT_SETTINGS
)
response = self.ask(msg.message)
match = re.search(
r"WMDCV(?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_settings()
return int(resp_dict["abort_function"])
def _get_post_sweep_voltage_condition(self) -> int:
resp_dict = self._get_sweep_auto_abort_settings()
if resp_dict["output_after_sweep"] is None:
raise ValueError(
"Received None. Set the parameter"
"``post_sweep_voltage_condition`` first."
)
return int(resp_dict["output_after_sweep"])
CVSweeper = KeysightB1500CVSweeper
"""
Alias for backwards compatibility
"""
[docs]
class KeysightB1520A(KeysightB1500Module):
"""
Driver for Keysight B1520A Capacitance Measurement 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)
"""
phase_compensation_timeout = 60 # manual says around 30 seconds
MODULE_KIND = ModuleKind.CMU
def __init__(
self,
parent: "KeysightB1500",
name: str | None,
slot_nr: int,
**kwargs: Any,
):
super().__init__(parent, name, slot_nr, **kwargs)
self.channels = (ChNr(slot_nr),)
self.setup_fnc_already_run = False
self._ranging_mode: constants.RangingMode = constants.RangingMode.AUTO
self._measurement_range_for_non_auto: int | None = None
self.voltage_dc: Parameter = self.add_parameter(
name="voltage_dc",
unit="V",
set_cmd=self._set_voltage_dc,
get_cmd=self._get_voltage_dc,
snapshot_get=False,
)
"""Parameter voltage_dc"""
self.voltage_ac: Parameter = self.add_parameter(
name="voltage_ac",
unit="V",
set_cmd=self._set_voltage_ac,
get_cmd=self._get_voltage_ac,
snapshot_get=False,
)
"""Parameter voltage_ac"""
self.frequency: Parameter = self.add_parameter(
name="frequency",
unit="Hz",
set_cmd=self._set_frequency,
get_cmd=self._get_frequency,
snapshot_get=False,
)
"""Parameter frequency"""
self.capacitance: Parameter = self.add_parameter(
name="capacitance", get_cmd=self._get_capacitance, snapshot_value=False
)
"""Parameter capacitance"""
self.add_submodule("correction", KeysightB1500Correction(self, "correction"))
self.phase_compensation_mode: Parameter = self.add_parameter(
name="phase_compensation_mode",
set_cmd=self._set_phase_compensation_mode,
get_cmd=None,
set_parser=constants.ADJ.Mode,
docstring=textwrap.dedent(
"""
This parameter selects the MFCMU phase compensation mode. This
command initializes the MFCMU. The available modes are captured
in :class:`constants.ADJ.Mode`:
- 0: Auto mode. Initial setting.
- 1: Manual mode.
- 2: Load adaptive mode.
For mode=0, the KeysightB1500 sets the compensation data
automatically. For mode=1, execute the
:meth:`phase_compensation` method ( the ``ADJ?`` command) to
perform the phase compensation and set the compensation data.
For mode=2, the KeysightB1500 performs the phase compensation
before every measurement. It is useful when there are wide load
fluctuations by changing the bias and so on."""
),
)
"""
This parameter selects the MFCMU phase compensation mode. This
command initializes the MFCMU. The available modes are captured
in :class:`constants.ADJ.Mode`:
- 0: Auto mode. Initial setting.
- 1: Manual mode.
- 2: Load adaptive mode.
For mode=0, the KeysightB1500 sets the compensation data
automatically. For mode=1, execute the
:meth:`phase_compensation` method ( the ``ADJ?`` command) to
perform the phase compensation and set the compensation data.
For mode=2, the KeysightB1500 performs the phase compensation
before every measurement. It is useful when there are wide load
fluctuations by changing the bias and so on.
"""
self.add_submodule("cv_sweep", KeysightB1500CVSweeper(self, "cv_sweep"))
self.adc_coef: GroupParameter = self.add_parameter(
name="adc_coef",
initial_value=1,
parameter_class=GroupParameter,
vals=vals.Ints(1, 1023),
docstring=textwrap.dedent(
"""
Coefficient used to define the number of averaging samples or
the averaging time. Integer expression.
- For mode=0: 1 to 1023. Initial setting/default setting is 2.
- For mode=2: 1 to 100. Initial setting/default setting is 1.
"""
),
)
"""
Coefficient used to define the number of averaging samples or
the averaging time. Integer expression.
- For mode=0: 1 to 1023. Initial setting/default setting is 2.
- For mode=2: 1 to 100. Initial setting/default setting is 1.
"""
self.adc_mode: GroupParameter = self.add_parameter(
name="adc_mode",
initial_value=constants.ACT.Mode.PLC,
parameter_class=GroupParameter,
vals=vals.Enum(*list(constants.ACT.Mode)),
set_parser=constants.ACT.Mode,
docstring=textwrap.dedent(
"""
Sets the number of averaging samples or the averaging time set
to the A/D converter of the MFCMU
``constants.ACT.Mode.AUTO``: Auto mode. Defines the number
of averaging samples given by the following formula. Then
initial averaging is the number of averaging samples
automatically set by the B1500 and you cannot change.
Number of averaging samples = N x initial averaging
``constants.ACT.Mode.PLC``: Power line cycle (PLC) mode.
Defines the averaging time given by the following formula.
Averaging time = N / power line frequency
"""
),
)
"""
Sets the number of averaging samples or the averaging time set
to the A/D converter of the MFCMU
``constants.ACT.Mode.AUTO``: Auto mode. Defines the number
of averaging samples given by the following formula. Then
initial averaging is the number of averaging samples
automatically set by the B1500 and you cannot change.
Number of averaging samples = N x initial averaging
``constants.ACT.Mode.PLC``: Power line cycle (PLC) mode.
Defines the averaging time given by the following formula.
Averaging time = N / power line frequency
"""
self._adc_group = Group(
[self.adc_mode, self.adc_coef],
set_cmd="ACT {adc_mode},{adc_coef}",
get_cmd=self._get_adc_mode(),
get_parser=self._get_adc_mode_parser,
)
self.ranging_mode: Parameter = self.add_parameter(
name="ranging_mode",
set_cmd=self._set_ranging_mode,
vals=vals.Enum(*list(constants.RangingMode)),
set_parser=constants.RangingMode,
get_cmd=None,
docstring=textwrap.dedent(
"""
Specifies the measurement range or the measurement ranging type
of the MFCMU. In the initial setting, the auto ranging is set.
The range changing occurs immediately after the trigger
(that is, during the measurements).
Possible ranging modes are autorange and fixed range.
"""
),
)
"""
Specifies the measurement range or the measurement ranging type
of the MFCMU. In the initial setting, the auto ranging is set.
The range changing occurs immediately after the trigger
(that is, during the measurements).
Possible ranging modes are autorange and fixed range.
"""
self.measurement_range_for_non_auto: Parameter = self.add_parameter(
name="measurement_range_for_non_auto",
set_cmd=self._set_measurement_range_for_non_auto,
get_cmd=None,
docstring=textwrap.dedent(
"""
Measurement range. Needs to set when ``ranging_mode`` is set to
PLC. The value should be integer 0 or more. 50 ohm, 100 ohm,
300 ohm, 1 kilo ohm, 3 kilo ohm, 10 kilo ohm, 30 kilo ohm,
100 kilo ohm, and 300 kilo ohm are selectable. Available
measurement ranges depend on the output signal frequency set by
the FC command."""
),
)
"""
Measurement range. Needs to set when ``ranging_mode`` is set to
PLC. The value should be integer 0 or more. 50 ohm, 100 ohm,
300 ohm, 1 kilo ohm, 3 kilo ohm, 10 kilo ohm, 30 kilo ohm,
100 kilo ohm, and 300 kilo ohm are selectable. Available
measurement ranges depend on the output signal frequency set by
the FC command.
"""
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)),
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.
"""
self.impedance_model: Parameter = self.add_parameter(
name="impedance_model",
set_cmd=self._set_impedance_model,
get_cmd=None,
vals=vals.Enum(*list(constants.IMP.MeasurementMode)),
set_parser=constants.IMP.MeasurementMode,
initial_value=constants.IMP.MeasurementMode.Cp_D,
docstring=textwrap.dedent(
"""
The IMP command specifies the parameter measured by the MFCMU.
Look at the ``constants.IMP.MeasurementMode`` for all the modes.
"""
),
)
"""
The IMP command specifies the parameter measured by the MFCMU.
Look at the ``constants.IMP.MeasurementMode`` for all the modes.
"""
self.ac_dc_volt_monitor: Parameter = self.add_parameter(
name="ac_dc_volt_monitor",
set_cmd=self._set_ac_dc_volt_monitor,
get_cmd=None,
vals=vals.Ints(0, 1),
initial_value=False,
docstring=textwrap.dedent(
"""
This command enables or disables the data monitor and data
output of the MFCMU AC voltage and DC voltage.
0: Disables the data monitor and output. Initial setting.
1: Enables the data monitor and output.
"""
),
)
"""
This command enables or disables the data monitor and data
output of the MFCMU AC voltage and DC voltage.
0: Disables the data monitor and output. Initial setting.
1: Enables the data monitor and output.
"""
self.cv_sweep_voltages: Parameter = self.add_parameter(
name="cv_sweep_voltages",
get_cmd=self._cv_sweep_voltages,
unit="V",
label="Voltage",
docstring=textwrap.dedent(
"""
Outputs the tuple of voltages to sweep. sweep_start, sweep_end
and sweep_step functions are used to define the values of
voltages. There are possible modes; linear sweep, log sweep,
linear 2 way sweep and log 2 way sweep. The output of
sweep_mode method is used to decide which mode to use.
"""
),
)
"""
Outputs the tuple of voltages to sweep. sweep_start, sweep_end
and sweep_step functions are used to define the values of
voltages. There are possible modes; linear sweep, log sweep,
linear 2 way sweep and log 2 way sweep. The output of
sweep_mode method is used to decide which mode to use.
"""
self.run_sweep: KeysightB1500CVSweepMeasurement = self.add_parameter(
name="run_sweep",
parameter_class=KeysightB1500CVSweepMeasurement,
docstring=textwrap.dedent(
"""
This is MultiParameter. Running the sweep runs the measurement
on the list of values of cv_sweep_voltages. The output is a
primary parameter (for ex Capacitance) and a secondary
parameter (for ex Dissipation) both of whom use the same
setpoint cv_sweep_voltages. The impedance_model defines exactly
what will be the primary and secondary parameter. The default
case is Capacitance and Dissipation.
"""
),
)
"""
This is MultiParameter. Running the sweep runs the measurement
on the list of values of cv_sweep_voltages. The output is a
primary parameter (for ex Capacitance) and a secondary
parameter (for ex Dissipation) both of whom use the same
setpoint cv_sweep_voltages. The impedance_model defines exactly
what will be the primary and secondary parameter. The default
case is Capacitance and Dissipation.
"""
def _cv_sweep_voltages(self) -> tuple[np.floating, ...]:
def sign(s: float) -> float:
return s and (1, -1)[s < 0]
start_value = self.cv_sweep.sweep_start()
end_value = self.cv_sweep.sweep_end()
step_value = self.cv_sweep.sweep_steps()
mode = self.cv_sweep.sweep_mode()
if mode in (2, 4):
if not sign(start_value) == sign(end_value):
if sign(start_value) == 0:
start_value = sign(start_value) * 0.005 # resolution
elif sign(end_value) == 0:
end_value = sign(end_value) * 0.005 # resolution
else:
raise AssertionError("Polarity of start and end is not same.")
def linear_sweep(
start: float, end: float, steps: int
) -> tuple[np.floating, ...]:
sweep_val = np.linspace(start, end, steps)
return tuple(sweep_val)
def log_sweep(start: float, end: float, steps: int) -> tuple[np.floating, ...]:
sweep_val = np.logspace(np.log10(start), np.log10(end), steps)
return tuple(sweep_val)
def linear_2way_sweep(
start: float, end: float, steps: int
) -> tuple[np.floating, ...]:
if steps % 2 == 0:
half_list = list(np.linspace(start, end, steps // 2))
sweep_val = half_list + half_list[::-1]
else:
half_list = list(np.linspace(start, end, steps // 2, endpoint=False))
sweep_val = half_list + [np.float64(end)] + half_list[::-1]
return tuple(sweep_val)
def log_2way_sweep(
start: float, end: float, steps: int
) -> tuple[np.floating, ...]:
if steps % 2 == 0:
half_list = list(
np.logspace(np.log10(start), np.log10(end), steps // 2)
)
sweep_val = half_list + half_list[::-1]
else:
half_list = list(
np.logspace(
np.log10(start), np.log10(end), steps // 2, endpoint=False
)
)
sweep_val = half_list + [end] + half_list[::-1]
return tuple(sweep_val)
modes = {1: linear_sweep, 2: log_sweep, 3: linear_2way_sweep, 4: log_2way_sweep}
return modes[mode](start_value, end_value, step_value)
def _set_voltage_dc(self, value: float) -> None:
msg = MessageBuilder().dcv(self.channels[0], value)
self.write(msg.message)
def _set_voltage_ac(self, value: float) -> None:
msg = MessageBuilder().acv(self.channels[0], value)
self.write(msg.message)
def _get_dcv(self) -> dict[str, str | float]:
if not self.is_enabled():
raise RuntimeError("The channels are disabled. Cannot get value.")
msg = MessageBuilder().lrn_query(self.channels[0])
response = self.ask(msg.message)
d = parse_dcv_measurement_response(response)
return d
def _get_voltage_dc(self) -> float:
dcv = self._get_dcv()
return float(dcv["voltage_dc"])
def _get_voltage_ac(self) -> float:
dcv = self._get_dcv()
return float(dcv["voltage_ac"])
def _get_frequency(self) -> float:
dcv = self._get_dcv()
return float(dcv["frequency"])
def _set_frequency(self, value: float) -> None:
msg = MessageBuilder().fc(self.channels[0], value)
self.write(msg.message)
def _get_capacitance(self) -> tuple[float, float]:
msg = MessageBuilder().tc(
chnum=self.channels[0], mode=constants.RangingMode.AUTO
)
response = self.ask(msg.message)
parsed = [item for item in re.finditer(_pattern, response)]
if len(parsed) not in (2, 4):
raise ValueError("Result format not supported.")
return float(parsed[0]["value"]), float(parsed[1]["value"])
def _set_phase_compensation_mode(self, mode: constants.ADJ.Mode) -> None:
msg = MessageBuilder().adj(chnum=self.channels[0], mode=mode)
self.write(msg.message)
[docs]
def phase_compensation(
self, mode: constants.ADJQuery.Mode | int | None = None
) -> constants.ADJQuery.Response:
"""
Performs the MFCMU phase compensation, sets the compensation
data to the KeysightB1500, and returns the execution results.
This method resets the MFCMU. Before executing this method, set the
phase compensation mode to manual by using
``phase_compensation_mode`` parameter, and open the measurement
terminals at the end of the device side. The execution of this
method will take about 30 seconds (the visa timeout for it is
controlled by :attr:`phase_compensation_timeout` attribute). The
compensation data is cleared by turning the KeysightB1500 off.
Args:
mode: Command operation mode :class:`.constants.ADJQuery.Mode`.
- 0: Use the last phase compensation data without measurement.
- 1: Perform the phase compensation data measurement.
If the mode parameter is not set, mode=1 is assumed by the
instrument.
Returns:
Status result of performing the phase compensation as
:class:`.constants.ADJQuery.Response`
"""
with self.root_instrument.timeout.set_to(self.phase_compensation_timeout):
msg = MessageBuilder().adj_query(chnum=self.channels[0], mode=mode)
response = self.ask(msg.message)
return constants.ADJQuery.Response(int(response))
@staticmethod
def _get_adc_mode() -> str:
msg = MessageBuilder().lrn_query(type_id=constants.LRN.Type.MFCMU_ADC_SETTING)
cmd = msg.message
return cmd
@staticmethod
def _get_adc_mode_parser(response: str) -> dict[str, int]:
match = re.search(r"ACT(?P<adc_mode>.+?),(?P<adc_coef>.+?)$", response)
if not match:
raise ValueError("ADC mode and coef (ATC) not found.")
out_str = match.groupdict()
out_dict = {key: int(value) for key, value in out_str.items()}
return out_dict
[docs]
def abort(self) -> None:
"""
Aborts currently running operation and the subsequent execution.
This does not abort the timeout process. Only when the kernel is
free this command is executed and the further commands are aborted.
"""
msg = MessageBuilder().ab()
self.write(msg.message)
def _set_measurement_mode(self, mode: MM.Mode | int) -> None:
self.root_instrument.set_measurement_mode(
mode=mode, channels=(self.channels[0],)
)
def _set_impedance_model(self, val: constants.IMP.MeasurementMode) -> None:
msg = MessageBuilder().imp(mode=val)
self.write(msg.message)
if hasattr(self, "run_sweep"):
self.run_sweep.update_name_label_unit_from_impedance_model(model=val)
def _set_ac_dc_volt_monitor(self, val: bool) -> None:
msg = MessageBuilder().lmn(enable_data_monitor=val)
self.write(msg.message)
def _set_ranging_mode(self, val: constants.RangingMode | int) -> None:
self._ranging_mode = constants.RangingMode(val)
if val == constants.RangingMode.AUTO:
self._measurement_range_for_non_auto = None
msg = MessageBuilder().rc(
chnum=self.channels[0],
ranging_mode=self._ranging_mode,
measurement_range=self._measurement_range_for_non_auto,
)
self.write(msg.message)
def _set_measurement_range_for_non_auto(self, val: int | None) -> None:
self._measurement_range_for_non_auto = val
msg = MessageBuilder().rc(
chnum=self.channels[0],
ranging_mode=self._ranging_mode,
measurement_range=self._measurement_range_for_non_auto,
)
self.write(msg.message)
[docs]
def setup_staircase_cv(
self,
v_start: float,
v_end: float,
n_steps: int,
freq: float,
ac_rms: float,
post_sweep_voltage_condition: (
constants.WMDCV.Post | int
) = constants.WMDCV.Post.STOP,
adc_mode: constants.ACT.Mode | int = constants.ACT.Mode.PLC,
adc_coef: int = 5,
imp_model: (
constants.IMP.MeasurementMode | int
) = constants.IMP.MeasurementMode.Cp_D,
ranging_mode: constants.RangingMode | int = constants.RangingMode.AUTO,
fixed_range_val: int | None = None,
hold_delay: float = 0,
delay: float = 0,
step_delay: float = 0,
trigger_delay: float = 0,
measure_delay: float = 0,
abort_enabled: constants.Abort | int = constants.Abort.ENABLED,
sweep_mode: constants.SweepMode | int = constants.SweepMode.LINEAR,
volt_monitor: bool = True,
) -> None:
"""
Convenience function which requires all inputs to properly setup a
CV sweep measurement. Function sets parameters in the order given
in the programming example in the manual. Returns error status
after setting all params.
Args:
v_start: Starting voltage for sweep
v_end: End voltage for sweep
n_steps: Number of steps in the sweep
freq: frequency
ac_rms: AC voltage
post_sweep_voltage_condition: Source output value after the
measurement is normally completed.
adc_mode: Sets the number of averaging samples or
the averaging time set to the A/D converter of the MFCMU.
adc_coef: the number of averaging samples or the
averaging time.
imp_model: specifies the units of the parameter measured by the
MFCMU.
ranging_mode: Auto range or Fixed range
fixed_range_val: Integer 0 or more. Available measurement ranges
depend on the output signal frequency.
See ``measurement_range_for_non_auto`` parameter for more info.
hold_delay: 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, with 10 ms resolution.
delay: Delay time (in seconds) that is the wait time after
starting to force a step output and before starting a step
measurement.
step_delay: 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, with 0.1 ms resolution.
If step_delay is shorter than the measurement time,
the B1500 waits until the measurement completes, then forces
the next step output.
trigger_delay: 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
delay, with 0.1 ms resolution.
measure_delay: 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, with 0.1 ms resolution.
abort_enabled: Boolean, enables or disables the automatic abort
function for the CV sweep measurement.
sweep_mode: Linear sweep, log sweep, linear 2 way sweep or
log 2 way sweep
volt_monitor: Accepts Boolean. If True, CV sweep measurement
outputs 4 parameter; primary parameter(for ex Capacitance),
secondary parameter(for ex Dissipation), ac source voltage
and dc source voltage. If False, the measurement only
outputs primary and secondary parameter.
"""
self.adc_mode(adc_mode)
self.adc_coef(adc_coef)
self.frequency(freq)
self.voltage_ac(ac_rms)
self.cv_sweep.sweep_auto_abort(abort_enabled)
self.cv_sweep.post_sweep_voltage_condition(post_sweep_voltage_condition)
self.cv_sweep.hold_time(hold_delay)
self.cv_sweep.delay(delay)
self.cv_sweep.step_delay(step_delay)
self.cv_sweep.trigger_delay(trigger_delay)
self.cv_sweep.measure_delay(measure_delay)
self.cv_sweep.sweep_mode(sweep_mode)
self.cv_sweep.sweep_start(v_start)
self.cv_sweep.sweep_end(v_end)
self.cv_sweep.sweep_steps(n_steps)
self.measurement_mode(constants.MM.Mode.CV_DC_SWEEP)
self.impedance_model(imp_model)
self.ac_dc_volt_monitor(volt_monitor)
self.ranging_mode(ranging_mode)
self.measurement_range_for_non_auto(fixed_range_val)
self.setup_fnc_already_run = True
B1520A = KeysightB1520A
"""
Alias for backwards compatiblitly
"""
[docs]
class KeysightB1500CVSweepMeasurement(MultiParameter, StatusMixin):
"""
CV sweep measurement outputs a list of primary (capacitance) and secondary
parameter (disipation).
Args:
name: Name of the Parameter.
instrument: Instrument to which this parameter communicates to.
"""
def __init__(self, name: str, instrument: KeysightB1520A, **kwargs: Any):
super().__init__(
name,
names=("", ""),
units=("", ""),
labels=("", ""),
shapes=((1,),) * 2,
setpoint_names=(("Voltage",),) * 2,
setpoint_labels=(("Voltage",),) * 2,
setpoint_units=(("V",),) * 2,
instrument=instrument,
**kwargs,
)
self.instrument: KeysightB1520A
self.root_instrument: KeysightB1500
self.update_name_label_unit_from_impedance_model()
#: Data, statuses, etc. of the first measured parameter
self.param1 = _FMTResponse(None, None, None, None)
#: Data, statuses, etc. of the second measured parameter
self.param2 = _FMTResponse(None, None, None, None)
#: Data, statuses, etc. of the AC voltage that the measured parameters
#: were measured for
self.ac_voltage = _FMTResponse(None, None, None, None)
#: Data, statuses, etc. of the AC voltage that the measured parameters
#: were measured for
self.dc_voltage = _FMTResponse(None, None, None, None)
self.power_line_frequency: int = 50
self._fudge: float = 1.5 # fudge factor for setting timeout
[docs]
def get_raw(self) -> tuple[tuple[float, ...], tuple[float, ...]]:
if not self.instrument.setup_fnc_already_run:
raise Exception("Sweep setup has not yet been run successfully")
delay_time = self.instrument.cv_sweep.step_delay()
nplc = self.instrument.adc_coef()
num_steps = self.instrument.cv_sweep.sweep_steps()
power_line_time_period = 1 / self.power_line_frequency
calculated_time = 2 * nplc * power_line_time_period * num_steps
estimated_timeout = max(delay_time, calculated_time) * num_steps
new_timeout = estimated_timeout * self._fudge
with self.root_instrument.timeout.set_to(new_timeout):
raw_data = self.instrument.ask(MessageBuilder().xe().message)
parsed_data = fmt_response_base_parser(raw_data)
if len(set(parsed_data.type)) == 2:
self.param1 = _FMTResponse(*(parsed_data[i][::2] for i in range(0, 4)))
self.param2 = _FMTResponse(*(parsed_data[i][1::2] for i in range(0, 4)))
self.shapes = ((num_steps,),) * 2
self.setpoints = ((self.instrument.cv_sweep_voltages(),),) * 2
else:
self.param1 = _FMTResponse(*(parsed_data[i][::4] for i in range(0, 4)))
self.param2 = _FMTResponse(*(parsed_data[i][1::4] for i in range(0, 4)))
self.ac_voltage = _FMTResponse(*(parsed_data[i][2::4] for i in range(0, 4)))
self.dc_voltage = _FMTResponse(*(parsed_data[i][3::4] for i in range(0, 4)))
self.shapes = ((len(self.dc_voltage.value),),) * 2
self.setpoints = ((self.dc_voltage.value,),) * 2
convert_dummy_val_to_nan(self.param1)
convert_dummy_val_to_nan(self.param2)
return self.param1.value, self.param2.value
[docs]
def update_name_label_unit_from_impedance_model(
self, model: constants.IMP.MeasurementMode | None = None
) -> None:
if model is None:
model = self.instrument.impedance_model()
# pyright does not seem to understand the type
# narrowing above
assert model is not None
self.names, self.labels, self.units = get_name_label_unit_of_impedance_model(
model
)
CVSweepMeasurement = KeysightB1500CVSweepMeasurement
"""
Alias for backwards compatibility
"""
[docs]
class KeysightB1500Correction(InstrumentChannel):
"""
A Keysight B1520A CMU submodule for performing open/short/load corrections.
"""
def __init__(self, parent: "KeysightB1520A", name: str, **kwargs: Any):
super().__init__(parent=parent, name=name, **kwargs)
self._chnum = parent.channels[0]
self.add_submodule(
"frequency_list",
KeysightB1500FrequencyList(self, "frequency_list", self._chnum),
)
[docs]
def enable(self, corr: constants.CalibrationType) -> None:
"""
This command enables the open/short/load correction. Before enabling a
correction, perform the corresponding correction data measurement by
using the :meth:`perform`.
Args:
corr: Depending on the the correction you want to perform,
set this to OPEN, SHORT or LOAD. For ex: In case of open
correction corr = constants.CalibrationType.OPEN.
"""
msg = MessageBuilder().corrst(chnum=self._chnum, corr=corr, state=True)
self.write(msg.message)
[docs]
def disable(self, corr: constants.CalibrationType) -> None:
"""
This command disables an open/short/load correction.
Args:
corr: Correction type as in :class:`.constants.CalibrationType`
"""
msg = MessageBuilder().corrst(chnum=self._chnum, corr=corr, state=False)
self.write(msg.message)
[docs]
def is_enabled(self, corr: constants.CalibrationType) -> constants.CORRST.Response:
"""
Query instrument to see if a correction of the given type is
enabled.
Args:
corr: Correction type as in :class:`.constants.CalibrationType`
"""
msg = MessageBuilder().corrst_query(chnum=self._chnum, corr=corr)
response = self.ask(msg.message)
return constants.CORRST.Response(int(response))
[docs]
def set_reference_values(
self,
corr: constants.CalibrationType,
mode: constants.DCORR.Mode,
primary: float,
secondary: float,
) -> None:
"""
This command disables the open/short/load correction function and
defines the calibration value or the reference value of the
open/short/load standard. Any previously measured correction data
will be invalid after calling this method.
Args:
corr: Correction mode from :class:`.constants.CalibrationType`.
OPEN for Open correction
SHORT for Short correction
LOAD for Load correction.
mode: Measurement mode from :class:`.constants.DCORR.Mode`
Cp-G (for open correction)
Ls-Rs (for short or load correction).
primary: Primary reference value of the standard. Cp value for
the open standard. in F. Ls value for the short or load
standard. in H.
secondary: Secondary reference value of the standard. G value
for the open standard. in S. Rs value for the short or load
standard. in Ω.
"""
msg = MessageBuilder().dcorr(
chnum=self._chnum,
corr=corr,
mode=mode,
primary=primary,
secondary=secondary,
)
self.write(msg.message)
[docs]
def get_reference_values(self, corr: constants.CalibrationType) -> str:
"""
This command returns the calibration values or the reference values of
the open/short/load standard.
Args:
corr: Correction mode from :class:`.constants.CalibrationType`.
OPEN for Open correction
SHORT for Short correction
LOAD for Load correction.
Returns:
A human-readable string with the correction mode
:class:`.constants.DCORR.Mode` and its reference values
"""
dcorr_response_tuple = self._get_reference_values(corr=corr)
return format_dcorr_response(dcorr_response_tuple)
def _get_reference_values(self, corr: constants.CalibrationType) -> _DCORRResponse:
msg = MessageBuilder().dcorr_query(chnum=self._chnum, corr=corr)
response = self.ask(msg.message)
return parse_dcorr_query_response(response)
Correction = KeysightB1500Correction
"""
Alias for backwards compatibility
"""
[docs]
class KeysightB1500FrequencyList(InstrumentChannel):
"""
A frequency list for open/short/load correction for Keysight B1520A CMU.
"""
def __init__(
self, parent: "KeysightB1500Correction", name: str, chnum: int, **kwargs: Any
):
super().__init__(parent=parent, name=name, **kwargs)
self._chnum = chnum
[docs]
def clear(self) -> None:
"""
Remove all frequencies in the list for data correction.
"""
self._clear(constants.CLCORR.Mode.CLEAR_ONLY)
[docs]
def clear_and_set_default(self) -> None:
"""
Remove all frequencies in the list for data correction AND set the
default frequency list.
For the list of default frequencies, refer to the documentation of
the ``CLCORR`` command in the programming manual.
"""
self._clear(constants.CLCORR.Mode.CLEAR_AND_SET_DEFAULT_FREQ)
def _clear(self, mode: constants.CLCORR.Mode) -> None:
msg = MessageBuilder().clcorr(chnum=self._chnum, mode=mode)
self.write(msg.message)
[docs]
def add(self, freq: float) -> None:
"""
Append MFCMU output frequency for data correction in the list.
The frequency value can be given with a certain resolution as per
Table 4-18 in the programming manual (year 2016).
"""
msg = MessageBuilder().corrl(chnum=self._chnum, freq=freq)
self.write(msg.message)
[docs]
def query(self, index: int | None = None) -> float:
"""
Query the frequency list for CMU data correction.
If ``index`` is ``None``, the query returns a total number of
frequencies in the list. If ``index`` is given, then the query
returns the frequency value from the list at that index.
"""
msg = MessageBuilder().corrl_query(chnum=self._chnum, index=index)
response = self.ask(msg.message)
return float(response)
FrequencyList = KeysightB1500FrequencyList
"""
Alias for backwards compatibility
"""