"""
QCoDeS driver for the MSO/DPO5000/B, DPO7000/C,
DPO70000/B/C/D/DX/SX, DSA70000/B/C/D, and
MSO70000/C/DX Series Digital Oscilloscopes
"""
import textwrap
import time
from functools import partial
from typing import TYPE_CHECKING, Any, ClassVar, Generic, Literal, Self
import numpy as np
import numpy.typing as npt
from qcodes.instrument import (
ChannelList,
Instrument,
InstrumentBase,
InstrumentBaseKWArgs,
InstrumentChannel,
VisaInstrument,
VisaInstrumentKWArgs,
)
from qcodes.parameters import (
Parameter,
ParameterWithSetpoints,
create_on_off_val_mapping,
)
from qcodes.parameters.parameter_base import ParameterDataTypeVar
from qcodes.validators import Arrays, Enum, Numbers
if TYPE_CHECKING:
from collections.abc import Callable
from typing_extensions import Unpack
def strip_quotes(string: str) -> str:
"""
This function is used as a get_parser for various
parameters in this driver
"""
return string.strip('"')
[docs]
class TektronixDPOModeError(Exception):
"""
Raise this exception if we are in a wrong mode to
perform an action
"""
pass
ModeError = TektronixDPOModeError
"""
Alias for backwards compatibility
"""
class TektronixDPO7000xx(VisaInstrument):
"""
QCoDeS driver for the MSO/DPO5000/B, DPO7000/C,
DPO70000/B/C/D/DX/SX, DSA70000/B/C/D, and
MSO70000/C/DX Series Digital Oscilloscopes
"""
number_of_channels = 4
number_of_measurements = 8 # The number of available
# measurements does not change.
default_terminator = "\n"
def __init__(
self, name: str, address: str, **kwargs: "Unpack[VisaInstrumentKWArgs]"
) -> None:
super().__init__(name, address, **kwargs)
self.horizontal: TektronixDPOHorizontal = self.add_submodule(
"horizontal", TektronixDPOHorizontal(self, "horizontal")
)
"""Instrument module horizontal"""
self.data: TektronixDPOData = self.add_submodule(
"data", TektronixDPOData(self, "data")
)
"""Instrument module data"""
self.waveform: TektronixDPOWaveformFormat = self.add_submodule(
"waveform", TektronixDPOWaveformFormat(self, "waveform")
)
"""Instrument module waveform"""
self.trigger: TektronixDPOTrigger = self.add_submodule(
"trigger", TektronixDPOTrigger(self, "trigger")
)
"""Instrument module trigger"""
self.delayed_trigger: TektronixDPOTrigger = self.add_submodule(
"delayed_trigger",
TektronixDPOTrigger(self, "delayed_trigger", delayed_trigger=True),
)
"""Instrument module delayed_trigger"""
self.acquisition: TektronixDPOAcquisition = self.add_submodule(
"acquisition", TektronixDPOAcquisition(self, "acquisition")
)
"""Instrument module acquisition"""
self.cursor: TektronixDPOCursor = self.add_submodule(
"cursor", TektronixDPOCursor(self, "cursor")
)
"""Instrument module cursor"""
self.measure_immediate: TektronixDPOMeasurementImmediate = self.add_submodule(
"measure_immediate",
TektronixDPOMeasurementImmediate(self, "measure_immediate"),
)
"""Instrument module measure immediate"""
measurement_list = ChannelList(self, "measurement", TektronixDPOMeasurement)
for measurement_number in range(1, self.number_of_measurements):
measurement_name = f"measurement{measurement_number}"
measurement_module = TektronixDPOMeasurement(
self, measurement_name, measurement_number
)
self.add_submodule(measurement_name, measurement_module)
measurement_list.append(measurement_module)
self.measurement: ChannelList[TektronixDPOMeasurement] = self.add_submodule(
"measurement", measurement_list
)
"""Instrument module measurement"""
self.statistics: TektronixDPOMeasurementStatistics = self.add_submodule(
"statistics", TektronixDPOMeasurementStatistics(self, "statistics")
)
"""Instrument module statistics"""
channel_list = ChannelList(self, "channel", TektronixDPOChannel)
for channel_number in range(1, self.number_of_channels + 1):
channel_name = f"channel{channel_number}"
channel_module = TektronixDPOChannel(
self,
channel_name,
channel_number,
)
self.add_submodule(channel_name, channel_module)
channel_list.append(channel_module)
self.channel: ChannelList[TektronixDPOChannel] = self.add_submodule(
"channel", channel_list
)
"""Instrument module channel"""
self.connect_message()
def ask_raw(self, cmd: str) -> str:
"""
Sometimes the instrument returns non-ascii characters in response
strings manually adjust the encoding to latin-1
"""
self.visa_log.debug(f"Querying: {cmd}")
self.visa_handle.write(cmd)
response = self.visa_handle.read(encoding="latin-1")
self.visa_log.debug(f"Response: {response}")
return response
[docs]
class TektronixDPOData(InstrumentChannel):
"""
This submodule sets and retrieves information regarding the
data source for the "CURVE?" query, which is used when
retrieving waveform data.
"""
def __init__(
self,
parent: InstrumentBase,
name: str,
**kwargs: "Unpack[InstrumentBaseKWArgs]",
) -> None:
super().__init__(parent, name, **kwargs)
# We can choose to retrieve data from arbitrary
# start and stop indices of the buffer.
self.start_index: Parameter = self.add_parameter(
"start_index",
get_cmd="DATa:STARt?",
set_cmd="DATa:STARt {}",
get_parser=int,
)
"""Parameter start_index"""
self.stop_index: Parameter = self.add_parameter(
"stop_index", get_cmd="DATa:STOP?", set_cmd="DATa:STOP {}", get_parser=int
)
"""Parameter stop_index"""
self.source: Parameter = self.add_parameter(
"source",
get_cmd="DATa:SOU?",
set_cmd="DATa:SOU {}",
vals=Enum(*TektronixDPOWaveform.valid_identifiers),
)
"""Parameter source"""
self.encoding: Parameter = self.add_parameter(
"encoding",
get_cmd="DATa:ENCdg?",
set_cmd="DATa:ENCdg {}",
get_parser=strip_quotes,
vals=Enum(
"ASCIi",
"FAStest",
"RIBinary",
"RPBinary",
"FPBinary",
"SRIbinary",
"SRPbinary",
"SFPbinary",
),
docstring=textwrap.dedent(
"""
For a detailed explanation of the
set arguments, please consult the
programmers manual at page 263/264.
http://download.tek.com/manual/077001022.pdf
"""
),
)
"""
Parameter encoding
For a detailed explanation of the
set arguments, please consult the
programmers manual at page 263/264.
http://download.tek.com/manual/077001022.pdf
"""
[docs]
class TektronixDPOChannel(InstrumentChannel):
"""
The main channel module for the oscilloscope. The parameters
defined here reflect the waveforms as they are displayed on
the instrument display.
"""
def __init__(
self,
parent: Instrument | InstrumentChannel,
name: str,
channel_number: int,
**kwargs: "Unpack[InstrumentBaseKWArgs]",
) -> None:
super().__init__(parent, name, **kwargs)
self._identifier = f"CH{channel_number}"
self.waveform: TektronixDPOWaveform = self.add_submodule(
"waveform", TektronixDPOWaveform(self, "waveform", self._identifier)
)
"""Instrument module waveform"""
self.coupling: Parameter[Literal["AC", "DC", "DCREJECT", "GND"], Self] = (
self.add_parameter(
"coupling",
get_cmd=f"{self._identifier}:COUPling?",
set_cmd=f"{self._identifier}:COUPling {{}}",
vals=Enum("AC", "DC", "DCREJECT", "GND"),
get_parser=str,
)
)
"""Parameter coupling: 'AC', 'DC', 'DCREJECT', 'GND'"""
self.scale: Parameter = self.add_parameter(
"scale",
get_cmd=f"{self._identifier}:SCA?",
set_cmd=f"{self._identifier}:SCA {{}}",
get_parser=float,
unit="V/div",
)
"""Parameter scale V/div"""
self.offset: Parameter = self.add_parameter(
"offset",
get_cmd=f"{self._identifier}:OFFS?",
set_cmd=f"{self._identifier}:OFFS {{}}",
get_parser=float,
unit="V",
)
"""Parameter offset voltage"""
self.position: Parameter = self.add_parameter(
"position",
get_cmd=f"{self._identifier}:POS?",
set_cmd=f"{self._identifier}:POS {{}}",
get_parser=float,
vals=Numbers(-8, 8),
unit="div",
)
"""Parameter position [-8, 8] divisions"""
self.termination: Parameter = self.add_parameter(
"termination",
get_cmd=f"{self._identifier}:TER?",
set_cmd=f"{self._identifier}:TER {{}}",
vals=Enum(50, 1e6),
get_parser=float,
unit="Ohm",
)
"""Parameter termination"""
self.analog_to_digital_threshold: Parameter = self.add_parameter(
"analog_to_digital_threshold",
get_cmd=f"{self._identifier}:THRESH?",
set_cmd=f"{self._identifier}:THRESH {{}}",
get_parser=float,
unit="V",
)
"""Parameter analog_to_digital_threshold"""
self.termination_voltage: Parameter = self.add_parameter(
"termination_voltage",
get_cmd=f"{self._identifier}:VTERm:BIAS?",
set_cmd=f"{self._identifier}:VTERm:BIAS {{}}",
get_parser=float,
unit="V",
)
"""Parameter termination_voltage"""
[docs]
def set_trace_length(self, value: int) -> None:
"""
Set the trace length when retrieving data
through the 'waveform' interface
Args:
value: The requested number of samples in the trace
"""
if self.root_instrument.horizontal.record_length() < value:
raise ValueError(
"Cannot set a trace length which is larger than "
"the record length. Please switch to manual mode "
"and adjust the record length first"
)
self.root_instrument.data.start_index(1)
self.root_instrument.data.stop_index(value)
[docs]
def set_trace_time(self, value: float) -> None:
"""
Args:
value: The time over which a trace is desired.
"""
sample_rate = self.root_instrument.horizontal.sample_rate()
required_sample_count = int(sample_rate * value)
self.set_trace_length(required_sample_count)
[docs]
class TektronixDPOHorizontal(InstrumentChannel):
"""
This module controls the horizontal axis of the scope
"""
def __init__(
self,
parent: Instrument | InstrumentChannel,
name: str,
**kwargs: "Unpack[InstrumentBaseKWArgs]",
) -> None:
super().__init__(parent, name, **kwargs)
self.mode: Parameter = self.add_parameter(
"mode",
get_cmd="HORizontal:MODE?",
set_cmd="HORizontal:MODE {}",
vals=Enum("auto", "constant", "manual"),
get_parser=str.lower,
docstring="""
Auto mode attempts to keep record length
constant as you change the time per division
setting. Record length is read only.
Constant mode attempts to keep sample rate
constant as you change the time per division
setting. Record length is read only.
Manual mode lets you change sample mode and
record length. Time per division or Horizontal
scale is read only.
""",
)
"""
Auto mode attempts to keep record length
constant as you change the time per division
setting. Record length is read only.
Constant mode attempts to keep sample rate
constant as you change the time per division
setting. Record length is read only.
Manual mode lets you change sample mode and
record length. Time per division or Horizontal
scale is read only.
"""
self.unit: Parameter = self.add_parameter(
"unit", get_cmd="HORizontal:MAIn:UNIts?", get_parser=strip_quotes
)
"""Parameter unit"""
self.record_length: Parameter = self.add_parameter(
"record_length",
get_cmd="HORizontal:MODE:RECOrdlength?",
set_cmd=self._set_record_length,
get_parser=float,
)
"""Parameter record_length"""
self.sample_rate: Parameter = self.add_parameter(
"sample_rate",
get_cmd="HORizontal:MODE:SAMPLERate?",
set_cmd="HORizontal:MODE:SAMPLERate {}",
get_parser=float,
unit=f"sample/{self.unit()}",
)
"""Parameter sample_rate"""
self.scale: Parameter = self.add_parameter(
"scale",
get_cmd="HORizontal:MODE:SCAle?",
set_cmd=self._set_scale,
get_parser=float,
unit=f"{self.unit()}/div",
)
"""Parameter scale"""
self.position: Parameter = self.add_parameter(
"position",
get_cmd="HORizontal:POSition?",
set_cmd="HORizontal:POSition {}",
get_parser=float,
unit="%",
docstring=textwrap.dedent(
"""
The horizontal position relative to a
received trigger. E.g. a value of '10'
sets the trigger position of the waveform
such that 10% of the display is to the
left of the trigger position.
"""
),
)
"""
The horizontal position relative to a
received trigger. E.g. a value of '10'
sets the trigger position of the waveform
such that 10% of the display is to the
left of the trigger position.
"""
self.roll: Parameter = self.add_parameter(
"roll",
get_cmd="HORizontal:ROLL?",
set_cmd="HORizontal:ROLL {}",
vals=Enum("Auto", "On", "Off"),
docstring=textwrap.dedent(
"""
Use Roll Mode when you want to view data at
very slow sweep speeds.
"""
),
)
"""
Use Roll Mode when you want to view data at
very slow sweep speeds.
"""
def _set_record_length(self, value: int) -> None:
if self.mode() != "manual":
raise TektronixDPOModeError(
"The record length can only be changed in manual mode"
)
self.write(f"HORizontal:MODE:RECOrdlength {value}")
def _set_scale(self, value: float) -> None:
if self.mode() == "manual":
raise TektronixDPOModeError("The scale cannot be changed in manual mode")
self.write(f"HORizontal:MODE:SCAle {value}")
class TektronixDPOAcquisition(InstrumentChannel):
"""
This submodule controls the acquisition mode of the
oscilloscope. It is used to set the acquisition mode
and the number of acquisitions.
"""
def __init__(
self,
parent: Instrument,
name: str,
**kwargs: "Unpack[InstrumentBaseKWArgs]",
) -> None:
super().__init__(parent, name, **kwargs)
self.mode: Parameter[
Literal["sample", "peakdetect", "average", "high_res", "wfmdb", "envelope"],
Self,
] = self.add_parameter(
"mode",
get_cmd="ACQuire:MODe?",
set_cmd="ACQuire:MODe {}",
vals=Enum(
"sample",
"peakdetect",
"average",
"high_res",
"wfmdb",
"envelope",
),
get_parser=str.lower,
)
"""Sample mode. This command sets or queries the acquisition mode. The acquisition mode
determines how the instrument acquires and processes data. The available acquisition modes are:
- Sample: The instrument acquires data at the specified sample rate and record length.
This is the default acquisition mode.
- Peak Detect: The instrument captures the maximum and minimum values for each sample interval. This mode is useful
for capturing narrow pulses or glitches that may be missed in sample mode.
- Average: The instrument acquires multiple waveforms and averages them together to reduce noise.
The number of waveforms to average can be set with the 'averages' parameter.
- High Res: The instrument acquires data at a higher resolution by using oversampling and digital filtering.
This mode is useful for capturing small signal details.
- WfmDB: Statistical database aquisition mode. The instrument acquires data and stores it in a statistical
database for later analysis.
- Envelope: The instrument captures the maximum and minimum values for each sample interval over multiple acquisitions
"""
self.state: Parameter[Literal["ON", "OFF", "RUN", "STOP"], Self] = (
self.add_parameter(
"state",
get_cmd="ACQuire:STATE?",
set_cmd="ACQuire:STATE {}",
vals=Enum(
"ON",
"OFF",
"RUN",
"STOP",
),
get_parser=str.upper,
)
)
"""
This command starts or stops acquisitions. When state is set to ON or RUN, a
new acquisition will be started. If the last acquisition was a single acquisition
sequence, a new single sequence acquisition will be started. If the last acquisition
was continuous, a new continuous acquisition will be started.
State can be 'ON', 'OFF', 'RUN', or 'STOP'.
"""
self.stop_after: Parameter = self.add_parameter(
"stop_after",
get_cmd="ACQuire:STOPAfter?",
set_cmd="ACQuire:STOPAfter {}",
vals=Enum("SEQUENCE", "RUNSTOP"),
get_parser=str.upper,
)
"""This command sets or queries whether the instrument continually acquires
acquisitions or acquires a single sequence. Pressing SINGLE on the front
panel button is equivalent to sending these commands: ACQUIRE:STOPAFTER
SEQUENCE and ACQUIRE:STATE 1."""
[docs]
class TektronixDPOTrigger(InstrumentChannel):
"""
Submodule for trigger setup.
You can trigger with the A (Main) trigger system alone
or combine the A (Main) trigger with the B (Delayed) trigger
to trigger on sequential events. When using sequential
triggering, the A trigger event arms the trigger system, and
the B trigger event triggers the instrument when the B
trigger conditions are met.
A and B triggers can (and typically do) have separate sources.
The B trigger condition is based on a time delay or a specified
number of events.
See page75, Using A (Main) and B (Delayed) triggers.
https://download.tek.com/manual/MSO70000C-DX-DPO70000C-DX-MSO-DPO7000C-MSO-DPO5000B-Oscilloscope-Quick-Start-User-Manual-071298006.pdf
"""
def __init__(
self,
parent: Instrument,
name: str,
delayed_trigger: bool = False,
**kwargs: "Unpack[InstrumentBaseKWArgs]",
):
super().__init__(parent, name, **kwargs)
self._identifier = "B" if delayed_trigger else "A"
trigger_types = ["edge", "logic", "pulse"]
if self._identifier == "A":
trigger_types.extend(
["video", "i2c", "can", "spi", "communication", "serial", "rs232"]
)
self.ready: Parameter = self.add_parameter(
"ready",
get_cmd=f"TRIGger:{self._identifier}:READY?",
get_parser=str.lower,
)
"""Indicates whether the trigger system is ready to accept a trigger.
A value of 1 indicates that the trigger system is ready to accept a trigger.
A value of 0 indicates that the trigger system is not ready to accept a trigger.
"""
self.state: Parameter = self.add_parameter(
"state",
get_cmd="TRIGger:STATe?",
get_parser=str.lower,
)
"""Gets the current Trigger state:
ARMED indicates that the instrument is acquiring pretrigger information.
AUTO indicates that the instrument is in the automatic mode and acquires data
even in the absence of a trigger.
DPO indicates that the instrument is in DPO mode.
PARTIAL indicates that the A trigger has occurred and the instrument is waiting
for the B trigger to occur.
READY indicates that all pretrigger information is acquired and that the instrument
is ready to accept a trigger.
SAVE indicates that the instrument is in save mode and is not acquiring data.
TRIGGER indicates that the instrument triggered and is acquiring the post trigger
information.
"""
self.type: Parameter = self.add_parameter(
"type",
get_cmd=f"TRIGger:{self._identifier}:TYPE?",
set_cmd=self._trigger_type,
vals=Enum(*trigger_types),
get_parser=str.lower,
)
"""Trigger type"""
edge_couplings = ["ac", "dc", "hfrej", "lfrej", "noiserej"]
if self._identifier == "B":
edge_couplings.append("atrigger")
self.edge_coupling: Parameter = self.add_parameter(
"edge_coupling",
get_cmd=f"TRIGger:{self._identifier}:EDGE:COUPling?",
set_cmd=f"TRIGger:{self._identifier}:EDGE:COUPling {{}}",
vals=Enum(*edge_couplings),
get_parser=str.lower,
)
"""Trigger edge coupling for A and B triggers:
Trigger A: 'ac', 'dc', 'hfrej', 'lfrej', 'noiserej'
Trigger B: 'ac', 'dc', 'hfrej', 'lfrej', 'noiserej', 'atrigger'
"""
self.edge_slope: Parameter = self.add_parameter(
"edge_slope",
get_cmd=f"TRIGger:{self._identifier}:EDGE:SLOpe?",
set_cmd=f"TRIGger:{self._identifier}:EDGE:SLOpe {{}}",
vals=Enum("RISE", "rise", "FALL", "fall", "EITHER", "either"),
get_parser=str.lower,
)
"""Trigger edge slope: 'rise', 'fall', or 'either'"""
trigger_sources = [
f"CH{i}" for i in range(1, TektronixDPO7000xx.number_of_channels)
]
trigger_sources.extend([f"D{i}" for i in range(0, 16)])
if self._identifier == "A":
trigger_sources.append("line")
trigger_sources.append("AUX")
self.source: Parameter = self.add_parameter(
"source",
get_cmd=f"TRIGger:{self._identifier}:EDGE:SOUrce?",
set_cmd=f"TRIGger:{self._identifier}:EDGE:SOUrce {{}}",
vals=Enum(*trigger_sources),
)
"""Trigger source: 'CH1', 'CH2', ..., 'CH4', 'D0', 'D1', ..., 'D15', 'AUX', 'LINE'"""
self.level: Parameter = self.add_parameter(
"level",
get_cmd=f"TRIGger:{self._identifier}:LEVel?",
set_cmd=f"TRIGger:{self._identifier}:LEVel {{}}",
get_parser=float,
unit="V",
)
"""Trigger level: The voltage level at which the trigger condition is met."""
def _trigger_type(self, value: str) -> None:
if value.lower() != "edge":
raise NotImplementedError(
"We currently only support the 'edge' trigger type"
)
self.write(f"TRIGger:{self._identifier}:TYPE {value}")
[docs]
class TektronixDPOMeasurementParameter(
Parameter[ParameterDataTypeVar, "TektronixDPOMeasurement"],
Generic[ParameterDataTypeVar],
):
"""
A measurement parameter does not only return the instantaneous value
of a measurement, but can also return some statistics. The accumulation
time over which these statistics are gathered can be controlled through
the 'time_constant' parameter on the submodule
'TektronixDPOMeasurementStatistics'. Here we also find the method 'reset'
to reset the values over which the statistics are gathered.
"""
def _get(self, metric: str) -> float:
measurement_channel = self.instrument
if measurement_channel.type.get_latest() != self.name:
measurement_channel.type(self.name)
measurement_channel.state(1)
measurement_channel.wait_adjustment_time()
measurement_number = measurement_channel.measurement_number
str_value = measurement_channel.ask(
f"MEASUrement:MEAS{measurement_number}:{metric}?"
)
return float(str_value)
[docs]
def mean(self) -> float:
return self._get("MEAN")
[docs]
def max(self) -> float:
return self._get("MAX")
[docs]
def min(self) -> float:
return self._get("MINI")
[docs]
def stdev(self) -> float:
return self._get("STDdev")
[docs]
def get_raw(self) -> float:
return self._get("VALue")
[docs]
def set_raw(self, value: Any) -> None:
raise ValueError("A measurement cannot be set")
[docs]
class TektronixDPOMeasurement(InstrumentChannel):
"""
The measurement submodule
"""
# It was found by trial and error that adjusting
# the measurement type and source takes some time
# to reflect properly on the value of the
# measurement. Wait a minimum of ...
_minimum_adjustment_time = 0.1
# seconds after setting measurement type/source before
# calling the measurement value SCPI command.
measurements: ClassVar[list[tuple[str, str]]] = [
("amplitude", "V"),
("area", "Vs"),
("burst", "s"),
("carea", "Vs"),
("cmean", "V"),
("crms", "V"),
("delay", "s"),
("distduty", "%"),
("extinctdb", "dB"),
("extinctpct", "%"),
("extinctratio", ""),
("eyeheight", "V"),
("eyewidth", "s"),
("fall", "s"),
("frequency", "Hz"),
("high", "V"),
("hits", "hits"),
("low", "V"),
("maximum", "V"),
("mean", "V"),
("median", "V"),
("minimum", "V"),
("ncross", "s"),
("nduty", "%"),
("novershoot", "%"),
("nwidth", "s"),
("pbase", "V"),
("pcross", "s"),
("pctcross", "%"),
("pduty", "%"),
("peakhits", "hits"),
("period", "s"),
("phase", "°"),
("pk2pk", "V"),
("pkpkjitter", "s"),
("pkpknoise", "V"),
("povershoot", "%"),
("ptop", "V"),
("pwidth", "s"),
("qfactor", ""),
("rise", "s"),
("rms", "V"),
("rmsjitter", "s"),
("rmsnoise", "V"),
("sigma1", "%"),
("sigma2", "%"),
("sigma3", "%"),
("sixsigmajit", "s"),
("snratio", ""),
("stddev", "V"),
("undefined", ""),
("waveforms", "wfms"),
]
def __init__(
self,
parent: Instrument,
name: str,
measurement_number: int,
**kwargs: "Unpack[InstrumentBaseKWArgs]",
) -> None:
super().__init__(parent, name, **kwargs)
self._measurement_number = measurement_number
self._adjustment_time = time.perf_counter()
self.state: Parameter = self.add_parameter(
"state",
get_cmd=f"MEASUrement:MEAS{self._measurement_number}:STATe?",
set_cmd=f"MEASUrement:MEAS{self._measurement_number}:STATe {{}}",
val_mapping=create_on_off_val_mapping(on_val="1", off_val="0"),
)
"""Parameter state"""
self.type: Parameter = self.add_parameter(
"type",
get_cmd=f"MEASUrement:MEAS{self._measurement_number}:TYPe?",
set_cmd=self._set_measurement_type,
get_parser=str.lower,
vals=Enum(*(m[0] for m in self.measurements)),
docstring=textwrap.dedent(
"Please see page 566-569 of the programmers manual "
"for a detailed description of these arguments. "
"http://download.tek.com/manual/077001022.pdf"
),
)
"""Parameter type"""
for measurement, unit in self.measurements:
self.add_parameter(
name=measurement,
unit=unit,
parameter_class=TektronixDPOMeasurementParameter,
)
for src in [1, 2]:
self.add_parameter(
f"source{src}",
get_cmd=f"MEASUrement:MEAS{self._measurement_number}:SOUrce{src}?",
set_cmd=partial(self._set_source, src),
vals=Enum(*([*TektronixDPOWaveform.valid_identifiers, "HISTogram"])),
)
@property
def measurement_number(self) -> int:
return self._measurement_number
def _set_measurement_type(self, value: str) -> None:
self._adjustment_time = time.perf_counter()
self.write(f"MEASUrement:MEAS{self._measurement_number}:TYPe {value}")
def _set_source(self, source_number: int, value: str) -> None:
self._adjustment_time = time.perf_counter()
self.write(
f"MEASUrement:MEAS{self._measurement_number}:SOUrce{source_number} {value}"
)
[docs]
def wait_adjustment_time(self) -> None:
"""
Wait until the minimum time after adjusting the measurement source or
type has elapsed
"""
time_since_adjust = time.perf_counter() - self._adjustment_time
if time_since_adjust < self._minimum_adjustment_time:
time_remaining = self._minimum_adjustment_time - time_since_adjust
time.sleep(time_remaining)
[docs]
class TektronixDPOMeasurementStatistics(InstrumentChannel):
def __init__(
self,
parent: InstrumentBase,
name: str,
**kwargs: "Unpack[InstrumentBaseKWArgs]",
):
super().__init__(parent=parent, name=name, **kwargs)
self.mode: Parameter = self.add_parameter(
"mode",
get_cmd="MEASUrement:STATIstics:MODe?",
set_cmd="MEASUrement:STATIstics:MODe {}",
vals=Enum("OFF", "ALL", "VALUEMean", "MINMax", "MEANSTDdev"),
docstring=textwrap.dedent(
"This command controls the operation and display of measurement "
"statistics. "
"1. OFF turns off all measurements. This is the default value "
"2. ALL turns on statistics and displays all statistics for "
"each measurement. "
"3. VALUEMean turns on statistics and displays the value and the "
"mean (μ) of each measurement. "
"4. MINMax turns on statistics and displays the min and max of "
"each measurement. "
"5. MEANSTDdev turns on statistics and displays the mean and "
"standard deviation of each measurement."
),
)
"""Parameter mode"""
self.time_constant: Parameter = self.add_parameter(
"time_constant",
get_cmd="MEASUrement:STATIstics:WEIghting?",
set_cmd="MEASUrement:STATIstics:WEIghting {}",
get_parser=int,
docstring=textwrap.dedent(
"This command sets or queries the time constant for mean and "
"standard deviation statistical accumulations, which is equivalent "
"to selecting Measurement Setup from the Measure menu, clicking "
"the Statistics button and entering the desired Weight n= value."
),
)
"""Parameter time_constant"""
[docs]
def reset(self) -> None:
self.write("MEASUrement:STATIstics:COUNt RESEt")
class TektronixDPOMeasurementImmediate(InstrumentChannel):
"""
The measurement commands let you specify an additional measurement, IMMed. The immediate measurement
has no front panel equivalent. Immediate measurements are never displayed.
Because they are computed only when needed, immediate measurements slow the
waveform update rate less than displayed measurements.
"""
def __init__(
self,
parent: Instrument,
name: str,
**kwargs: "Unpack[InstrumentBaseKWArgs]",
) -> None:
super().__init__(parent, name, **kwargs)
self.gating: Parameter = self.add_parameter(
"gating",
get_cmd="MEASUrement:GATing?",
set_cmd="MEASUrement:GATing {}",
vals=Enum("ON", "OFF", "ZOOM1", "ZOOM2", "ZOOM3", "ZOOM4", "CURSOR"),
)
"""Gating for the immediate measurement. Gating allows you to specify a subset of the waveform
to be measured. When gating is on, the measurement is performed only on the portion of the waveform
defined by the gate. The gate can be defined by zooming in on a portion of
the waveform and selecting one of the zoom gates (ZOOM1, ZOOM2, ZOOM3, ZOOM4), or by using the
cursor gate (CURSOR), which uses the horizontal positions of the cursors to define the gate.
"""
self.source1: Parameter = self.add_parameter(
"source1",
get_cmd="MEASUrement:IMMed:SOUrce1?",
set_cmd="MEASUrement:IMMed:SOUrce1 {}",
vals=Enum(*TektronixDPOWaveform.valid_identifiers),
)
"""Source 1 for the immediate measurement:
CH1, CH2, CH3, CH4, MATH1, MATH2, MATH3, MATH4,
REF1, REF2, REF3, REF4, HISTogram"""
self.source2: Parameter = self.add_parameter(
"source2",
get_cmd="MEASUrement:IMMed:SOUrce2?",
set_cmd="MEASUrement:IMMed:SOUrce2 {}",
vals=Enum(*TektronixDPOWaveform.valid_identifiers),
)
"""Source 2 for the immediate measurement. Source2 measurements only apply
to phase and delay measurement types, which require both a target (Source1)
and reference (Source2) source.
CH1, CH2, CH3, CH4, MATH1, MATH2, MATH3, MATH4,
REF1, REF2, REF3, REF4
"""
self.type: Parameter = self.add_parameter(
"type",
get_cmd="MEASUrement:IMMed:TYPE?",
set_cmd="MEASUrement:IMMed:TYPE {}",
vals=Enum(
"ACRMS",
"AMPlitude",
"AREa",
"BURst",
"CARea",
"CMEan",
"CRMs",
"DELay",
"DISTDUty",
"EXTINCTDB",
"EXTINCTPCT",
"EXTINCTRATIO",
"EYEHeight",
"EYEWIdth",
"FALL",
"FREQuency",
"HIGH",
"HITs",
"LOW",
"MAXimum",
"MEAN",
"MEDian",
"MINImum",
"NCROss",
"NDUty",
"NOVershoot",
"NWIdth",
"PBASe",
"PCROss",
"PCTCROss",
"PDUty",
"PEAKHits",
"PERIod",
"PHAse",
"PK2Pk",
"PKPKJitter",
"PKPKNoise",
"POVershoot",
"PTOP",
"PWIdth",
"QFACtor",
"RISe",
"RMS",
"RMSJitter",
"RMSNoise",
"SIGMA1",
"SIGMA2",
"SIGMA3",
"SIXSigmajit",
"SNRatio",
"STDdev",
"UNDEFINED",
"WAVEFORMS",
),
get_parser=str.lower,
)
"""
Immediate measurement type
Please see page 2-547 of the programmers manual for a detailed description of these arguments.
https://download.tek.com/manual/MSO-DPO5000-B-DPO7000-C-DPO70000-B-C-D-DX-DSA70000-B-C-D-and-MSO70000-C-DX-_2.pdf
"""
self.units: Parameter = self.add_parameter(
"units",
get_cmd="MEASUrement:IMMed:UNITS?",
get_parser=strip_quotes,
)
"""Units of the immediate measurement"""
self.value: Parameter = self.add_parameter(
"value",
get_cmd="MEASUrement:IMMed:VALue?",
get_parser=float,
)
"""The value of the immediate measurement"""
class TektronixDPOCursor(InstrumentChannel):
"""
The cursor submodule allows you to set and retrieve
information regarding the cursor type, state, and
positions. The cursor can be used to measure
voltage and time differences between two points on
the waveform display.
Methods:
- function: Set or get the cursor type (e.g., horizontal bars, vertical bars, etc.)
- state: Set or get the cursor state (ON or OFF)
- x1: Set or get the x1 position of the cursor (in seconds)
- x2: Set or get the x2 position of the cursor (in seconds)
- y1: Set or get the y1 position of the cursor (in Volts)
- y2: Set or get the y2 position of the cursor (in Volts)
"""
def __init__(
self,
parent: Instrument,
name: str,
**kwargs: "Unpack[InstrumentBaseKWArgs]",
) -> None:
super().__init__(parent, name, **kwargs)
self.function: Parameter = self.add_parameter(
"function",
get_cmd="CURSOR:FUNCtion?",
set_cmd="CURSOR:FUNCtion {}",
vals=Enum(
"OFF",
"HBARS",
"VBARS",
"SCREEN",
"WAVEFORM",
),
get_parser=str.lower,
)
"""Cursor Type [OFF, HBARS, VBARS, SCREEN, WAVEFORM]"""
self.state: Parameter = self.add_parameter(
"state",
get_cmd="CURSOR:STATE?",
set_cmd="CURSOR:STATE {}",
vals=Enum("ON", "OFF"),
get_parser=str.lower,
)
"""Cursor state [ON, OFF]"""
self.x1: Parameter = self.add_parameter(
"x1",
get_cmd="CURSOR:VBARS:POSITION1?",
set_cmd="CURSOR:VBARS:POSITION1 {}",
get_parser=float,
unit="s",
)
"""Cursor x1 position in seconds"""
self.x2: Parameter = self.add_parameter(
"x2",
get_cmd="CURSOR:VBARS:POSITION2?",
set_cmd="CURSOR:VBARS:POSITION2 {}",
get_parser=float,
unit="s",
)
"""Cursor x2 position in seconds"""
self.y1: Parameter = self.add_parameter(
"y1",
get_cmd="CURSOR:HBARS:POSITION1?",
set_cmd="CURSOR:HBARS:POSITION1 {}",
get_parser=float,
unit="V",
)
"""Cursor y1 position in Volts"""
self.y2: Parameter = self.add_parameter(
"y2",
get_cmd="CURSOR:HBARS:POSITION2?",
set_cmd="CURSOR:HBARS:POSITION2 {}",
get_parser=float,
unit="V",
)
"""Cursor y2 position in Volts"""