Module tektronix_func_gen
Tektronix arbitrary function generator control
Provides basic control of AFG1000 and AFG3000 series Tektronix Arbitrary Function Generators, possibly also others. This includes setting basic settings such as selecting functions, transferring or selecting custom waveforms, amplitude and offset control, phase syncronisation and frequency locking.
API documentation available here,
or in the repository docs/index.html. (To build the documentation
yourself use pdoc3 and run
$ python3 pdoc --html -o ./docs/ tektronix_func_gen
.)
Tested on Win10 with NI-VISA and PyVISA v1.11 (if using PyVISA <v1.11 use <v0.4 of this module).
Known issues
- For TekVISA users: a
pyvisa.errors.VI_ERROR_IO
is raised unless the Call Monitor application that comes with TekVISA is open and capturing (see issue #1). NI-VISA does not have this issue. - The offset of the built-in DC (flat) function cannot be controlled directly. A workaround is to transfer a flat custom waveform to a memory location, see Flat function offset control in this readme.
- The frequency limits can in practice be stricter than what is set by the module, as the module is using the limits for a sine, where as other functions, such as ramp might have lower limit
Installation
Put the module file in the folder wherein the Python file you will import it from resides.
Dependencies:
- The package needs VISA to be installed. It is tested with NI-VISA,
TekVISA might not work, see
Known issues
- The Python packages
numpy
andpyvisa
(>=v1.11) are required
Usage (through examples)
An example of basic control
import tektronix_func_gen as tfg
with tfg.FuncGen('VISA ADDRESS OF YOUR INSTRUMENT') as fgen:
fgen.ch1.set_function("SIN")
fgen.ch1.set_frequency(25, unit="Hz")
fgen.ch1.set_offset(50, unit="mV")
fgen.ch1.set_amplitude(0.002)
fgen.ch1.set_output("ON")
fgen.ch2.set_output("OFF")
# alternatively fgen.ch1.print_settings() to show from one channel only
fgen.print_settings()
yields something like (depending on the settings already in use)
Connected to TEKTRONIX model AFG1022, serial XXXXX
Current settings for TEKTRONIX AFG1022 XXXXX
Setting Ch1 Ch2 Unit
==========================
output ON OFF
function SIN RAMP
amplitude 0.002 1 Vpp
offset 0.05 -0.45 V
frequency 25.0 10.0 Hz
Settings can also be stored and restored:
"""Example showing how to connect, get the current settings of
the instrument, store them, change a setting and then restore the
initial settings"""
import tektronix_func_gen as tfg
with tfg.FuncGen('VISA ADDRESS OF YOUR INSTRUMENT') as fgen:
fgen.print_settings()
print("Saving these settings..")
settings = fgen.get_settings()
print("Change to 1Vpp amplitude for channel 1..")
fgen.ch1.set_amplitude(1)
fgen.print_settings()
print("Reset back to initial settings..")
fgen.set_settings(settings)
fgen.print_settings()
Syncronisation and frequency lock
The phase of the two channels can be syncronised with syncronise_waveforms()
.
Frequency lock can also be enabled/disabled with set_frequency_lock()
:
"""Example showing the frequency being set to 10Hz and then the frequency
lock enabled, using the frequency at ch1 as the common frequency"""
import tektronix_func_gen as tfg
with tfg.FuncGen('VISA ADDRESS OF YOUR INSTRUMENT', verbose=False) as fgen:
fgen.ch1.set_frequency(10)
fgen.set_frequency_lock("ON", use_channel=1)
Arbitrary waveforms
14 bit vertical resolution arbitrary waveforms can be transferred to the 256 available user defined functions on the function generator. The length of the waveform must be between 2 and 8192 points.
import numpy as np
import tektronix_func_gen as tfg
with tfg.FuncGen('VISA ADDRESS OF YOUR INSTRUMENT') as fgen:
# create waveform
x = np.linspace(0, 4*np.pi, 8000)
waveform = np.sin(x)+x/5
# transfer the waveform (normalises to the vertical waveform range)
fgen.set_custom_waveform(waveform, memory_num=5, verify=True)
# done, but let's have a look at the waveform catalogue ..
print("New waveform catalogue:")
for i, wav in enumerate(fgen.get_waveform_catalogue()): print(" {}: {}".format(i, wav))
# .. and set the waveform to channel 1
print("Set new wavefrom to channel 1..", end=" ")
fgen.ch1.set_output("OFF")
fgen.ch1.set_function("USER5")
print("ok")
# print current settings
fgen.print_settings()
Flat function offset control
The offset of the built-in DC function cannot be controlled (the offset command
simply does not work, an issue from Tektronix). A workaround is to transfer a
flat custom waveform (two or more points of half the vertical range
(arbitrary_waveform_resolution
)) to a memory location:
with tfg.FuncGen('VISA ADDRESS OF YOUR INSTRUMENT') as fgen:
flat_wfm = int(fgen.arbitrary_waveform_resolution/2)*np.ones(2).astype(np.int32)
fgen.set_custom_waveform(flat_wfm, memory_num=255, normalise=False)
fgen.ch1.set_function("USER255")
fgen.ch1.set_offset(2)
Note the normalise=False
argument.
Set voltage and frequency limits
Limits for amplitude, voltage and frequency for each channel are kept in a
dictionary FuncGenChannel.channel_limits
(these are the standard limits
for AFG1022)
channel_limits = {
"frequency lims": ({"min": 1e-6, "max": 25e6}, "Hz"),
"voltage lims": ({"50ohm": {"min": -5, "max": 5},
"highZ": {"min": -10, "max": 10}}, "V"),
"amplitude lims": ({"50ohm": {"min": 0.001, "max": 10},
"highZ": {"min": 0.002, "max": 20}}, "Vpp")}
They chan be changed by FuncGenChannel.set_limit()
, or by using the
FuncGenChannel.set_stricter_limits()
for a series of prompts.
import tektronix_func_gen as tfg
"""Example showing how limits can be read and changed"""
with tfg.FuncGen('VISA ADDRESS OF YOUR INSTRUMENT') as fgen:
lims = fgen.ch1.get_frequency_lims()
print("Channel 1 frequency limits: {}".format(lims))
print("Change the lower limit to 2Hz..")
fgen.ch1.set_limit("frequency lims", "min", 2)
lims = fgen.ch1.get_frequency_lims()
print("Channel 1 frequency limits: {}".format(lims))
print("Try to set ch1 frequency to 1Hz..")
try:
fgen.ch1.set_frequency(1)
except NotSetError as err:
print(err)
Impedance
Unfortunately the impedance (50Ω or high Z) cannot be controlled or read remotely.
Which setting is in use affects the limits of the output voltage. Use the optional
impedance keyword in the initialisation of the FuncGen
object to make the object
aware what limits applies: FuncGen('VISA ADDRESS OF YOUR INSTRUMENT', impedance=("highZ", "50ohm"))
.
Expand source code
# -*- coding: utf-8 -*-
"""
.. include:: ./README.md
"""
import copy
import pyvisa
import numpy as np
from typing import Tuple, List, Union
_VISA_ADDRESS = "USB0::0x0699::0x0353::1731975::INSTR"
def _SI_prefix_to_factor(unit):
"""Convert an SI prefix to a numerical factor
Parameters
----------
unit : str
The unit whose first character is checked against the list of
prefactors {"M": 1e6, "k": 1e3, "m": 1e-3}
Returns
-------
factor : float or `None`
The appropriate factor or 1 if not found in the list, or `None`
if the unit string is empty
"""
# SI prefix to numerical value
SI_conversion = {"M": 1e6, "k": 1e3, "m": 1e-3}
try: # using the unit's first character as key in the dictionary
factor = SI_conversion[unit[0]]
except KeyError: # if the entry does not exist
factor = 1
except IndexError: # if the unit string is empty
factor = None
return factor
## ~~~~~~~~~~~~~~~~~~~~~~~~~~~ ERROR CLASSES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ##
class NotSetError(Exception):
"""Error for when a value cannot be written to the instrument"""
class NotCompatibleError(Exception):
"""Error for when the instrument is not compatible with this module"""
## ~~~~~~~~~~~~~~~~~~~~~ FUNCTION GENERATOR CLASS ~~~~~~~~~~~~~~~~~~~~~~~~~~ ##
class FuncGen:
"""Class for interacting with Tektronix function generator
Parameters
----------
visa_address : str
VISA address of the insrument
impedance : tuple of {"highZ", "50ohm"}, default ("highZ",)*2
Determines voltage limits associated with high impedance (whether the
instrument is using 50ohm or high Z cannot be controlled through VISA).
For example `("highZ", "50ohm")` for to use high Z for ch1 and
50 ohm for ch2
timeout : int, default 1000
Timeout in milliseconds of instrument connection
verify_param_set : bool, default False
Verify that a value is successfully set after executing a set function
verbose : bool, default `True`
Choose whether to print information such as model upon connecting etc
override_compatibility : str, default `""`
If the instrument limits for the model connected to are not known
`NotCompatibleError` will be raised. To override and use either of
AFG1022, AFG1062, or AFG3022 limits, use their respecive model names as
argument. Note that this might lead to unexpected behaviour for custom
waveforms and 'MIN'/'MAX' keywords.
Attributes
----------
_visa_address : str
The VISA address of the instrument
_id : str
Comma separated string with maker, model, serial and firmware of
the instrument
_inst : `pyvisa.resources.Resource`
The PyVISA resource
_arbitrary_waveform_length : list
The permitted minimum and maximum length of an arbitrary waveform,
e.g. [2, 8192]
_arbitrary_waveform_resolution : int
The vertical resolution of the arbitrary waveform, for instance 14 bit
=> 2**14-1 = 16383
_max_waveform_memory_user_locations : int
The number of the last user memory location available
Raises
------
pyvisa.Error
If the supplied VISA address cannot be connected to
NotCompatibleError
If the instrument limits for the model connected to are not known
(Call the class with `override_compatibility=True` to override and
use AFG1022 limits)
"""
_is_connected = False
"""bool: Keeping track of whether the PYVISA connection has been established"""
instrument_limits = {}
"""dict: Contains the following keys with subdictionaries
- `frequency lims`
Containing the frequency limits for the instrument where the keys
"min" and "max" have values corresponding to minimum and maximum
frequencies in Hz
- `voltage lims`
Contains the maximum absolute voltage the instrument can output
for the keys "50ohm" and "highZ" according to the impedance setting
- `amplitude lims`
Contains the smallest and largest possible amplitudes where the
keys "50ohm" and "highZ" will have subdictionaries with keys
"min" and "max"
"""
def __init__(
self,
visa_address: str,
impedance: Tuple[str, str] = ("highZ",) * 2,
timeout: int = 1000,
verify_param_set: bool = False,
override_compatibility: str = "",
verbose: bool = True,
):
self._override_compat = override_compatibility
self._visa_address = visa_address
self.verify_param_set = verify_param_set
"""bool: Verify that a value is successfully set after executing a set function"""
self.verbose = verbose
"""bool: Choose whether to print information such as model upon connecting etc"""
self.open(visa_address, timeout)
self._initialise_model_properties()
self.channels = (
self._spawn_channel(1, impedance[0]),
self._spawn_channel(2, impedance[1]),
)
"""tuple of `FuncGenChannel`: Objects to control the channels"""
self.ch1 = self.channels[0]
"""`FuncGenChannel`: Short hand for `channels[0]` Object to control channel 1"""
self.ch2 = self.channels[1]
"""`FuncGenChannel`: Short hand for `channels[1]` Object to control channel 2"""
def __enter__(self, **kwargs):
# The kwargs will be passed on to __init__
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
def __del__(self):
self.close()
def open(self, visa_address: str, timeout: int):
try:
rm = pyvisa.ResourceManager()
self._inst = rm.open_resource(visa_address)
except pyvisa.Error:
print(f"\nVisaError: Could not connect to '{visa_address}'")
raise
self._is_connected = True
self.timeout = timeout
# Clear all the event registers and queues used in the instrument
# status and event reporting system
self.write("*CLS")
# Get information about the connected device
self._id = self.query("*IDN?")
# Second query might be needed due to unknown reason
if self._id == "":
self._id = self.query("*IDN?")
self._maker, self._model, self._serial = self._id.split(",")[:3]
if self.verbose:
print(
f"Connected to {self._maker} model {self._model}, "
f"serial {self._serial}"
)
def close(self):
"""Close the connection to the instrument"""
if self._is_connected:
self._inst.close()
self._is_connected = False
@property
def timeout(self) -> int:
"""The timeout of the PYVISA connection in milliseconds"""
return self._inst.timeout
@timeout.setter
def timeout(self, ms: int):
self._inst.timeout = ms
def _initialise_model_properties(self):
"""Initialises the limits of what the instrument can handle according
to the instrument model
Raises
------
NotCompatibleError
If the connected model is not necessarily compatible with this
package, slimits are not known.
"""
if np.any(["AFG1022" in a for a in [self._model, self._override_compat]]):
self.instrument_limits = {
"frequency lims": ({"min": 1e-6, "max": 25e6}, "Hz"),
"voltage lims": (
{"50ohm": {"min": -5, "max": 5}, "highZ": {"min": -10, "max": 10}},
"V",
),
"amplitude lims": (
{
"50ohm": {"min": 0.001, "max": 10},
"highZ": {"min": 0.002, "max": 20},
},
"Vpp",
),
}
self._arbitrary_waveform_length = [2, 8192] # min length, max length
self._arbitrary_waveform_resolution = 16383 # 14 bit
self._max_waveform_memory_user_locations = 255
elif np.any(["AFG1062" in a for a in [self._model, self._override_compat]]):
self.instrument_limits = {
"frequency lims": ({"min": 1e-6, "max": 60e6}, "Hz"),
"voltage lims": (
{"50ohm": {"min": -5, "max": 5}, "highZ": {"min": -10, "max": 10}},
"V",
),
"amplitude lims": (
{
"50ohm": {"min": 0.001, "max": 10},
"highZ": {"min": 0.002, "max": 20},
},
"Vpp",
),
}
self._arbitrary_waveform_length = [2, 1e6] # min length, max length
self._arbitrary_waveform_resolution = 16383 # 14 bit
self._max_waveform_memory_user_locations = 31
elif np.any(["AFG3022" in a for a in [self._model, self._override_compat]]):
self.instrument_limits = {
"frequency lims": ({"min": 1e-6, "max": 25e6}, "Hz"),
"voltage lims": (
{"50ohm": {"min": -5, "max": 5}, "highZ": {"min": -10, "max": 10}},
"V",
),
"amplitude lims": (
{
"50ohm": {"min": 0.01, "max": 10},
"highZ": {"min": 0.02, "max": 20},
},
"Vpp",
),
}
self._arbitrary_waveform_length = [2, 65536] # min length, max length
self._arbitrary_waveform_resolution = 16383 # 14 bit
self._max_waveform_memory_user_locations = 4
else:
msg = (
f"Model {self._model} might not be fully supported!\n"
" The module has been tested with AFG1022, AFG1062, and AFG3022.\n"
" To initiate and use the module as any of these, call the\n"
" class with for instance `override_compatibility='AFG1022'`\n"
" Note that this might lead to unexpected behaviour\n"
" for custom waveforms and 'MIN'/'MAX' keywords."
)
raise NotCompatibleError(msg)
def write(self, command: str, custom_err_message: str = None) -> int:
"""Write a VISA command to the instrument
Parameters
----------
command : str
The VISA command to be written to the instrument
custom_err_message : str, default `None`
When `None`, the RuntimeError message is "Writing/querying command
{command} failed: pyvisa returned StatusCode ..".
Otherwise, if a message is supplied "Could not {message}:
pyvisa returned StatusCode .."
Returns
-------
bytes : int
Number of bytes tranferred
Raises
------
RuntimeError
If status returned by PyVISA write command is not
`pyvisa.constants.StatusCode.success`
"""
num_bytes = self._inst.write(command)
self._check_pyvisa_status(command, custom_err_message=custom_err_message)
return num_bytes
def query(self, command: str, custom_err_message: str = None) -> str:
"""Query the instrument
Parameters
----------
command : str
The VISA query command
custom_err_message : str, default `None`
When `None`, the RuntimeError message is "Writing/querying command
{command} failed: pyvisa returned StatusCode ..".
Otherwise, if a message is supplied "Could not {message}:
pyvisa returned StatusCode .."
Returns
-------
str
The instrument's response
Raises
------
RuntimeError
If status returned by PyVISA write command is not
`pyvisa.constants.StatusCode.success`
"""
response = self._inst.query(command).strip()
self._check_pyvisa_status(command, custom_err_message=custom_err_message)
return response
def _check_pyvisa_status(self, command: str, custom_err_message: str = None):
"""Check the last status code of PyVISA
Parameters
----------
command : str
The VISA write/query command
Returns
-------
status : pyvisa.constants.StatusCode
Return value of the library call
Raises
------
RuntimeError
If status returned by PyVISA write command is not
`pyvisa.constants.StatusCode.success`
"""
status = self._inst.last_status
if not status == pyvisa.constants.StatusCode.success:
if custom_err_message is not None:
msg = (
f"Could not {custom_err_message}: pyvisa returned "
f"StatusCode {status} ({str(status)})"
)
raise RuntimeError(msg)
msg = (
f"Writing/querying command {command} failed: pyvisa returned "
f"StatusCode {status} ({str(status)})"
)
raise RuntimeError(msg)
return status
def get_error(self) -> str:
"""Get the contents of the Error/Event queue on the device
Returns
-------
str
Error/event number, description of error/event
"""
return self.query("SYSTEM:ERROR:NEXT?")
def _spawn_channel(self, channel: int, impedance: str) -> "FuncGenChannel":
"""Wrapper function to create a `FuncGenChannel` object for
a channel -- see the class docstring"""
return FuncGenChannel(self, channel, impedance)
def get_settings(self) -> List[dict]:
"""Get dictionaries of the current settings of the two channels
Returns
-------
settings : list of dicts
[ch1_dict, ch2_dict]: Settings currently in use as a dictionary
with keys output, function, amplitude, offset, and frequency with
corresponding values
"""
return [ch.get_settings() for ch in self.channels]
def print_settings(self):
"""Prints table of the current setting for both channels"""
settings = self.get_settings()
# Find the necessary padding for the table columns
# by evaluating the maximum length of the entries
key_padding = max([len(key) for key in settings[0].keys()])
ch_paddings = [
max([len(str(val[0])) for val in ch_settings.values()])
for ch_settings in settings
]
padding = [key_padding] + ch_paddings
print(f"\nCurrent settings for {self._maker} {self._model} {self._serial}\n")
row_format = "{:>{padd[0]}s} {:{padd[1]}s} {:{padd[2]}s} {}"
table_header = row_format.format("Setting", "Ch1", "Ch2", "Unit", padd=padding)
print(table_header)
print("=" * len(table_header))
for (ch1key, (ch1val, unit)), (_, (ch2val, _)) in zip(
settings[0].items(), settings[1].items()
):
print(
row_format.format(ch1key, str(ch1val), str(ch2val), unit, padd=padding)
)
def set_settings(self, settings: List[dict]):
"""Set the settings of both channels with settings dictionaries
(Each channel is turned off before applying the changes to avoid
potentially harmful combinations)
Parameteres
-----------
settings : list of dicts
List of settings dictionaries as returned by `get_settings`, first
entry for channel 1, second for channel 2. The dictionaries should
have keys output, function, amplitude, offset, and frequency
"""
for ch, s in zip(self.channels, settings):
ch.set_settings(s)
def syncronise_waveforms(self):
"""Syncronise waveforms of the two channels when using the same frequency
Note: Does NOT enable the frequency lock that can be enabled on the
user interface of the instrument)
"""
self.write(":PHAS:INIT", custom_err_message="syncronise waveforms")
def get_frequency_lock(self) -> bool:
"""Check if frequency lock is enabled
Returns
-------
bool
`True` if frequency lock enabled
"""
# If one is locked so is the other, so just need to check one
return int(self.query("SOURCE1:FREQuency:CONCurrent?")) == 1
def set_frequency_lock(self, state: str, use_channel: int = 1):
"""Enable the frequency lock to make the two channels have the same
frequency and phase of their signals, also after adjustments.
See also `FuncGen.syncronise_waveforms` for one-time sync only.
Parameters
----------
state : {"ON", "OFF"}
ON to enable, OFF to disable the lock
use_channel : int, default 1
Only relevant if turning the lock ON: The channel whose frequency
shall be used as the common freqency
"""
if self.verbose:
if state.lower() == "off" and not self.get_frequency_lock():
print(
f"(!) {self._model}: Tried to disable frequency lock, but "
f"frequency lock was not enabled"
)
return
if state.lower() == "on" and self.get_frequency_lock():
print(
f"(!) {self._model}: Tried to enable frequency lock, but "
f"frequency lock was already enabled"
)
return
# (Sufficient to disable for only one of the channels)
cmd = f"SOURCE{use_channel}:FREQuency:CONCurrent {state}"
msg = f"turn frequency lock {state}"
self.write(cmd, custom_err_message=msg)
def software_trig(self):
"""NOT TESTED: sends a trigger signal to the device
(for bursts or modulations)"""
self.write("*TRG", custom_err_message="send trigger signal")
## ~~~~~~~~~~~~~~~~~~~~~ CUSTOM WAVEFORM FUNCTIONS ~~~~~~~~~~~~~~~~~~~~~ ##
def get_waveform_catalogue(self) -> List[str]:
"""Get list of the waveforms that are in use (not empty)
Returns
-------
catalogue : list
Strings with the names of the user functions that are not empty
"""
catalogue = self.query("DATA:CATalog?").split(",")
catalogue = [wf[1:-1] for wf in catalogue] # strip off extra quotes
return catalogue
def get_custom_waveform(self, memory_num: int) -> np.ndarray:
"""Get the waveform currently stored in USER<memory_num>
Parameters
----------
memory_num : str or int {0,...,255}, default 0
Select which user memory to compare with
Returns
-------
waveform : ndarray
Waveform as ints spanning the resolution of the function gen or
and empty array if waveform not in use
"""
# Find the wavefroms in use
waveforms_in_use = self.get_waveform_catalogue()
if f"USER{memory_num}" in waveforms_in_use:
# Copy the waveform to edit memory
self.write(f"DATA:COPY EMEMory,USER{memory_num}")
# Get the length of the waveform
waveform_length = int(self.query("DATA:POINts? EMEMory"))
# Get the waveform (returns binary values)
waveform = self._inst.query_binary_values(
"DATA:DATA? EMEMory",
datatype="H",
is_big_endian=True,
container=np.ndarray,
)
msg = (
f"Waveform length from native length command (DATA:POINts?) "
f"and the processed binary values do not match, "
f"{waveform_length} and {len(waveform)} respectively"
)
assert len(waveform) == waveform_length, msg
return waveform
print(f"Waveform USER{memory_num} is not in use")
return np.array([])
def set_custom_waveform(
self,
waveform: np.ndarray,
normalise: bool = True,
memory_num: int = 0,
verify: bool = True,
print_progress: bool = True,
):
"""Transfer waveform data to edit memory and then user memory.
NOTE: Will overwrite without warnings
Parameters
----------
waveform : ndarray
Either unnormalised arbitrary waveform (then use `normalise=True`),
or ints spanning the resolution of the function generator
normalise : bool
Choose whether to normalise the waveform to ints over the
resolution span of the function generator
memory_num : str or int {0,...,255}, default 0
Select which user memory to copy to
verify : bool, default `True`
Verify that the waveform has been transferred and is what was sent
print_progress : bool, default `True`
Returns
-------
waveform : ndarray
The normalised waveform transferred
Raises
------
ValueError
If the waveform is not within the permitted length or value range
RuntimeError
If the waveform transferred to the instrument is of a different
length than the waveform supplied
"""
if not 0 <= memory_num <= self._max_waveform_memory_user_locations:
raise ValueError(
f"The memory location {memory_num} is not a valid "
"memory location for this model"
)
# Check if waveform data is suitable
if print_progress:
print("Check if waveform data is suitable..", end=" ")
self._check_arb_waveform_length(waveform)
try:
self._check_arb_waveform_type_and_range(waveform)
except ValueError as err:
if print_progress:
print(f"\n {err}")
print("Trying again normalising the waveform..", end=" ")
waveform = self._normalise_to_waveform(waveform)
if print_progress:
print("ok")
print("Transfer waveform to function generator..", end=" ")
# Transfer waveform
self._inst.write_binary_values(
"DATA:DATA EMEMory,", waveform, datatype="H", is_big_endian=True
)
# Check for errors and check lengths are matching
transfer_error = self.get_error()
emem_wf_length = self.query("DATA:POINts? EMEMory")
if emem_wf_length == "" or not int(emem_wf_length) == len(waveform):
msg = (
f"Waveform in temporary EMEMory has a length of {emem_wf_length}"
f", not of the same length as the waveform ({len(waveform)})."
f"\nError from the instrument: {transfer_error}"
)
raise RuntimeError(msg)
if print_progress:
print("ok")
print(f"Copy waveform to USER{memory_num}..", end=" ")
self.write(f"DATA:COPY USER{memory_num},EMEMory")
if print_progress:
print("ok")
if verify:
if print_progress:
print(f"Verify waveform USER{memory_num}..")
if f"USER{memory_num}" in self.get_waveform_catalogue():
verif = self._verify_waveform(
waveform,
memory_num,
normalise=normalise,
print_result=print_progress,
)
if not verif[0]:
raise RuntimeError(
f"USER{memory_num} does not contain the waveform"
)
else:
raise RuntimeError(f"USER{memory_num} is empty")
return waveform
def _normalise_to_waveform(self, shape: np.ndarray) -> np.ndarray:
"""Normalise a shape of any discretisation and range to a waveform that
can be transmitted to the function generator
.. note::
If you are transferring a flat/constant waveform, do not use this
normaisation function. Transfer a waveform like
`int(self._arbitrary_waveform_resolution/2)*np.ones(2).astype(np.int32)`
without normalising for a well behaved flat function.
Parameters
----------
shape : array_like
Array to be transformed to waveform, can be ints or floats,
any normalisation or discretisation
Returns
-------
waveform : ndarray
Waveform as ints spanning the resolution of the function gen
"""
# Check if waveform data is suitable
self._check_arb_waveform_length(shape)
# Normalise
waveform = shape - np.min(shape)
normalisation_factor = np.max(waveform)
waveform = waveform / normalisation_factor * self._arbitrary_waveform_resolution
return waveform.astype(np.uint16)
def _verify_waveform(
self,
waveform: np.ndarray,
memory_num: int,
normalise: bool = True,
print_result: bool = True,
) -> Tuple[bool, np.ndarray, list]:
"""Compare a waveform in user memory to argument waveform
Parameters
----------
waveform : ndarray
Waveform as ints spanning the resolution of the function gen
memory_num : str or int {0,...,255}, default 0
Select which user memory to compare with
normalise : bool, default `True`
Normalise test waveform
Returns
-------
bool
Boolean according to equal/not equal
instrument_waveform
The waveform on the instrument
list or `None`
List of the indices where the waveforms are not equal or `None` if
the waveforms were of different lengths
"""
if normalise: # make sure test waveform is normalised
waveform = self._normalise_to_waveform(waveform)
# Get the waveform on the instrument
instrument_waveform = self.get_custom_waveform(memory_num)
# Compare lengths
len_inst_wav, len_wav = len(instrument_waveform), len(waveform)
if not len_inst_wav == len_wav:
if print_result:
print(
f"The waveform in USER{memory_num} and the compared "
f"waveform are not of same length (instrument "
f"{len_inst_wav} vs {len_wav})"
)
return False, instrument_waveform, None
# Compare each element
not_equal = []
for i in range(len_wav):
if not instrument_waveform[i] == waveform[i]:
not_equal.append(i)
# Return depending of whether list is empty or not
if not not_equal: # if list is empty
if print_result:
print(
f"The waveform in USER{memory_num} and the compared "
f"waveform are equal"
)
return True, instrument_waveform, not_equal
if print_result:
print(
f"The waveform in USER{memory_num} and the compared "
f"waveform are NOT equal"
)
return False, instrument_waveform, not_equal
def _check_arb_waveform_length(self, waveform: np.ndarray):
"""Checks if waveform is within the acceptable length
Parameters
----------
waveform : array_like
Waveform or voltage list to be checked
Raises
------
ValueError
If the waveform is not within the permitted length
"""
if (len(waveform) < self._arbitrary_waveform_length[0]) or (
len(waveform) > self._arbitrary_waveform_length[1]
):
msg = (
"The waveform is of length {}, which is not within the "
"acceptable length {} < len < {}"
"".format(len(waveform), *self._arbitrary_waveform_length)
)
raise ValueError(msg)
def _check_arb_waveform_type_and_range(self, waveform: np.ndarray):
"""Checks if waveform is of int/np.int32 type and within the resolution
of the function generator
Parameters
----------
waveform : array_like
Waveform or voltage list to be checked
Raises
------
ValueError
If the waveform values are not int, np.uint16 or np.int32, or the
values are not within the permitted range
"""
for value in waveform:
if not isinstance(value, (int, np.uint16, np.int32)):
raise ValueError(
"The waveform contains values that are not"
"int, np.uint16 or np.int32"
)
if (value < 0) or (value > self._arbitrary_waveform_resolution):
raise ValueError(
f"The waveform contains values out of range "
f"({value} is not within the resolution "
f"[0, {self._arbitrary_waveform_resolution}])"
)
## ~~~~~~~~~~~~~~~~~~~~~~~ CHANNEL CLASS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ##
class FuncGenChannel:
"""Class for controlling a channel on a function generator object
Parameters
----------
fgen : `FuncGen`
The function generator object
channel : {1, 2}
The channel to be controlled
impedance : {"50ohm", "highZ"}
Determines voltage limits associated with high impedance (whether the
instrument is using 50ohm or high Z cannot be controlled through VISA)
Attributes
----------
_fgen : `FuncGen`
The function generator object for which the channel exists
_channel : {1, 2}
The number of the channel this object is addressing
_source : str
"SOURce{i}:" where {i} is the channel number
"""
_state_to_str = {"1": "ON", "0": "OFF", 1: "ON", 0: "OFF"}
"""Dictionary for converting output states to "ON" and "OFF" """
def __init__(self, fgen: FuncGen, channel: int, impedance: str):
self._fgen = fgen
self._channel = channel
self._source = f"SOURce{channel}:"
self.impedance = impedance
"""{"50ohm", "highZ"}: Determines voltage limits associated with high
impedance (whether the instrument is using 50ohm or high Z cannot be
controlled through VISA)"""
# Adopt limits dictionary from instrument
self.channel_limits = copy.deepcopy(self._fgen.instrument_limits)
"""Channel limits for the individual channel, same form as
`FuncGen.instrument_limits`"""
def _impedance_dependent_limit(self, limit_type: str) -> bool:
"""Check if the limit type is impedance dependent (voltages) or
not (frequency)
Returns
-------
bool
`True` if the limit is impedance dependent
"""
try: # to access the key "min" to check if impedance must be selected
_ = self.channel_limits[limit_type][0]["min"]
return False
except KeyError: # if the key does not exist
# The impedance must be selected
return True
def set_stricter_limits(self):
"""Set limits for the voltage and frequency limits of the channel output
through a series of prompts"""
print(f"Set stricter voltage and frequency limits for channel {self._channel}")
print("Use enter only to leave a limit unchanged.")
# Go through the different limits in the instrument_limits dict
for limit_type, (inst_limit_dict, unit) in self._fgen.instrument_limits.items():
use_impedance = self._impedance_dependent_limit(limit_type)
print(f"Set {limit_type} in {unit}", end=" ")
if use_impedance:
inst_limit_dict = inst_limit_dict[self.impedance]
print(f"[{self.impedance} impedance limit]")
else:
print("") # get new line
# Go through the min and max for the limit type
for key, inst_value in inst_limit_dict.items():
# prompt for new value
new_value = input(f" {key} (instrument limit {inst_value}{unit}): ")
if new_value == "":
# Do not change if empty
print("\tLimit not changed")
else:
try: # to convert to float
new_value = float(new_value)
except ValueError:
print(
f"\tLimit unchanged: Could not convert '{new_value}' "
f"to float"
)
continue # to next item in dict
# Set the new limit
self.set_limit(limit_type, key, new_value, verbose=True)
def set_limit(
self, limit_type: str, bound: str, new_value: float, verbose: bool = False
) -> bool:
"""Set a limit if the new value is within the instrument limits and are
self consistent (max larger than min)
Parameterers
------------
limit_type : str
The name of the limit in the channel_limits dictionary
bound : {"min", "max"}
Specifies if it is the max or the min limit that is to be set
new_value : float
The new value to be used for the limit
verbose : bool
Print confirmation that the limit was set or reason for why the
limit was not set
Returns
-------
bool
`True` if new limit set, `False` otherwise
"""
# Short hand references
inst_limit_dict = self._fgen.instrument_limits[limit_type]
channel_limit_dict = self.channel_limits[limit_type]
# Find the instrument limit and unit
use_impedance = self._impedance_dependent_limit(limit_type)
if use_impedance:
inst_value = inst_limit_dict[0][self.impedance][bound]
else:
inst_value = inst_limit_dict[0][bound]
unit = inst_limit_dict[1]
# Check that the new value is within the intrument limits
acceptable_min = bound == "min" and new_value > inst_value
if use_impedance:
current_min = channel_limit_dict[0][self.impedance]["min"]
else:
current_min = channel_limit_dict[0]["min"]
larger_than_min = new_value > current_min
acceptable_max = bound == "max" and new_value < inst_value and larger_than_min
if acceptable_min or acceptable_max: # within the limits
# Set the new channel_limit, using the impedance depending on the
# limit type. Beware that the shorthand cannot be used, as this
# only changes the shorthand not the dictionary itself
if use_impedance:
self.channel_limits[limit_type][0][self.impedance][bound] = new_value
else:
self.channel_limits[limit_type][0][bound] = new_value
if verbose:
print(f"\tNew limit set {new_value}{unit}")
return True
if verbose: # print description of why the limit was not set
if larger_than_min:
reason = "larger" if bound == "max" else "smaller"
print(
f"\tNew limit NOT set: {new_value}{unit} is {reason} than "
f"the instrument limit ({inst_value}{unit})"
)
else:
print(
f"\tNew limit NOT set: {new_value}{unit} is smaller than the "
f"current set minimum ({current_min}{unit})"
)
return False
# Get currently used parameters from function generator
def get_output_state(self) -> int:
"""Returns 0 for "OFF", 1 for "ON" """
return int(self._fgen.query(f"OUTPut{self._channel}:STATe?"))
def get_function(self) -> str:
"""Returns string of function name"""
return self._fgen.query(f"{self._source}FUNCtion:SHAPe?")
def get_amplitude(self) -> float:
"""Returns peak-to-peak voltage in volts"""
return float(self._fgen.query(f"{self._source}VOLTage:AMPLitude?"))
def get_offset(self) -> float:
"""Returns offset voltage in volts"""
return float(self._fgen.query(f"{self._source}VOLTage:OFFSet?"))
def get_frequency(self) -> float:
"""Returns frequency in Hertz"""
return float(self._fgen.query(f"{self._source}FREQuency?"))
# Get limits set in the channel class
def get_frequency_lims(self) -> List[float]:
"""Returns list of min and max frequency limits"""
return [self.channel_limits["frequency lims"][0][key] for key in ["min", "max"]]
def get_voltage_lims(self) -> List[float]:
"""Returns list of min and max voltage limits for the current impedance"""
return [
self.channel_limits["voltage lims"][0][self.impedance][key]
for key in ["min", "max"]
]
def get_amplitude_lims(self) -> List[float]:
"""Returns list of min and max amplitude limits for the current impedance"""
return [
self.channel_limits["amplitude lims"][0][self.impedance][key]
for key in ["min", "max"]
]
def get_settings(self) -> dict:
"""Get the settings for the channel
Returns
-------
current_settings : dict
Settings currently in use as a dictionary with keys output,
function, amplitude, offset, and frequency and values tuples of
the corresponding return and unit
"""
return {
"output": (self._state_to_str[self.get_output_state()], ""),
"function": (self.get_function(), ""),
"amplitude": (self.get_amplitude(), "Vpp"),
"offset": (self.get_offset(), "V"),
"frequency": (self.get_frequency(), "Hz"),
}
def print_settings(self):
"""Print the settings currently in use for the channel (Recommended
to use the `FuncGen.print_settings` for printing both channels)
"""
settings = self.get_settings()
longest_key = max([len(key) for key in settings.keys()])
print("\nCurrent settings for channel {}".format(self._channel))
print("==============================")
for key, (val, unit) in settings.items():
print("{:>{num_char}s} {} {}".format(key, val, unit, num_char=longest_key))
def set_settings(self, settings: dict):
"""Set the settings of the channel with a settings dictionary. Will
set the outout to OFF before applyign the settings (and turn the
channel ON or leave it OFF depending on the settings dict)
Parameteres
-----------
settings : dict
Settings dictionary as returned by `get_settings`: should have
keys output, function, amplitude, offset, and frequency
"""
# First turn off to ensure no potentially harmful
# combination of settings
self.set_output_state("OFF")
# Set settings according to dictionary
self.set_function(settings["function"][0])
self.set_amplitude(settings["amplitude"][0])
self.set_offset(settings["offset"][0])
self.set_frequency(settings["frequency"][0])
self.set_output_state(settings["output"][0])
def set_output_state(self, state: Union[int, str]):
"""Enables or diables the output of the channel
Parameters
----------
state : int or str
"ON" or int 1 to enable
"OFF" or int 0 to disable
Raises
------
NotSetError
If `self._fgen.verify_param_set` is `True` and the value after
applying the set function does not match the value returned by the
get function
"""
err_msg = f"turn channel {self._channel} to state {state}"
self._fgen.write(
f"OUTPut{self._channel}:STATe {state}", custom_err_message=err_msg
)
if self._fgen.verify_param_set:
actual_state = self.get_output_state()
if not actual_state == state:
msg = (
f"Channel {self._channel} was not turned {state}, it is "
f"{self._state_to_str[actual_state]}.\n"
f"Error from the instrument: {self._fgen.get_error()}"
)
raise NotSetError(msg)
def get_output(self) -> int:
"""Wrapper for get_output_state"""
return self.get_output_state()
def set_output(self, state: Union[int, str]):
"""Wrapper for set_output_state"""
self.set_output_state(state)
def set_function(self, shape: str):
"""Set the function shape of the output
Parameters
----------
shape : {SINusoid, SQUare, PULSe, RAMP, PRNoise, <Built_in>, USER[0],
USER1, ..., USER255, EMEMory, EFILe}
<Built_in>::={StairDown|StairUp|Stair Up&Dwn|Trapezoid|RoundHalf|
AbsSine|AbsHalfSine|ClippedSine|ChoppedSine|NegRamp|OscRise|
OscDecay|CodedPulse|PosPulse|NegPulse|ExpRise|ExpDecay|Sinc|
Tan|Cotan|SquareRoot|X^2|HaverSine|Lorentz|Ln(x)|X^3|CauchyDistr|
BesselJ|BesselY|ErrorFunc|Airy|Rectangle|Gauss|Hamming|Hanning|
Bartlett|Blackman|Laylight|Triangle|DC|Heart|Round|Chirp|Rhombus|
Cardiac}
Raises
------
NotSetError
If `self._fgen.verify_param_set` is `True` and the value after
applying the set function does not match the value returned by the
get function
"""
cmd = f"{self._source}FUNCtion:SHAPe {shape}"
self._fgen.write(cmd, custom_err_message=f"set function {shape}")
if self._fgen.verify_param_set:
actual_shape = self.get_function()
if not actual_shape == shape:
msg = (
f"Function {shape} was not set on channel {self._channel}, "
f"it is {actual_shape}. Check that the function name is "
f"correctly spelt. Look up `set_function.__doc__` to see "
f"available shapes.\n Error from the instrument: "
f"{self._fgen.get_error()}"
)
raise NotSetError(msg)
def set_amplitude(self, amplitude: float):
"""Set the peak-to-peak amplitude in volts
Parameters
----------
amplitude : float or {"max", "min"}
0.1mV or four digits resolution, "max" or "min" will set the
amplitude to the maximum or minimum limit given in `channel_limits`
Raises
------
NotSetError
If `self._fgen.verify_param_set` is `True` and the value after
applying the set function does not match the value returned by the
get function
"""
# Check if keyword min or max is given
if str(amplitude).lower() in ["min", "max"]:
unit = "" # no unit for MIN/MAX
# Look up what the limit is for this keyword
amplitude = self.channel_limits["amplitude lims"][0][self.impedance][
str(amplitude).lower()
]
else:
unit = "Vpp"
# Check if the given amplitude is within the current limits
min_ampl, max_ampl = self.get_amplitude_lims()
if amplitude < min_ampl or amplitude > max_ampl:
msg = (
f"Could not set the amplitude {amplitude}{unit} as it "
f"is not within the amplitude limits set for the instrument "
f"[{min_ampl}, {max_ampl}]{unit}"
)
raise NotSetError(msg)
# Check that the new amplitude will not violate voltage limits
min_volt, max_volt = self.get_voltage_lims()
current_offset = self.get_offset()
if (
amplitude / 2 - current_offset < min_volt
or amplitude / 2 + current_offset > max_volt
):
msg = (
f"Could not set the amplitude {amplitude}{unit} as the amplitude "
f"combined with the offset ({current_offset}V) will be outside the "
f"absolute voltage limits [{min_volt}, {max_volt}]{unit}"
)
raise NotSetError(msg)
# Set the amplitude
cmd = f"{self._source}VOLTage:LEVel {amplitude}{unit}"
err_msg = f"set amplitude {amplitude}{unit}"
self._fgen.write(cmd, custom_err_message=err_msg)
# Verify that the amplitude has been set
if self._fgen.verify_param_set:
actual_amplitude = self.get_amplitude()
# Multiply with the appropriate factor according to SI prefix, or
# if string is empty, use the value looked up from channel_limits earlier
if not unit == "":
check_amplitude = amplitude * _SI_prefix_to_factor(unit)
else:
check_amplitude = amplitude
if not actual_amplitude == check_amplitude:
msg = (
f"Amplitude {amplitude}{unit} was not set on channel "
f"{self._channel}, it is {actual_amplitude}Vpp. Check that "
f" the number is within the possible range and in the "
f"correct format.\nError from the instrument: "
f"{self._fgen.get_error()}"
)
raise NotSetError(msg)
def set_offset(self, offset: float, unit: str = "V"):
"""Set offset in volts (or mV, see options)
Parameters
----------
offset : float
Unknown resolution, guessing 0.1mV or four digits resolution
unit : {mV, V}, default V
Raises
------
NotSetError
If `self._fgen.verify_param_set` is `True` and the value after
applying the set function does not match the value returned by the
get function
"""
# Check that the new offset will not violate voltage limits
min_volt, max_volt = self.get_voltage_lims()
current_amplitude = self.get_amplitude()
offset = _SI_prefix_to_factor(unit) * offset
if (
current_amplitude / 2 - offset < min_volt
or current_amplitude / 2 + offset > max_volt
):
msg = (
f"Could not set the offset {offset}V as the offset combined "
f"with the amplitude ({current_amplitude}V) will be outside "
f"the absolute voltage limits [{min_volt}, {max_volt}]V"
)
raise NotSetError(msg)
# Set the offset
cmd = f"{self._source}VOLTage:LEVel:OFFSet {offset}{unit}"
err_msg = f"set offset {offset}{unit}"
self._fgen.write(cmd, custom_err_message=err_msg)
# Verify that the offset has been set
if self._fgen.verify_param_set:
actual_offset = self.get_offset()
# Multiply with the appropriate factor according to SI prefix
check_offset = offset * _SI_prefix_to_factor(unit)
if not actual_offset == check_offset:
msg = (
f"Offset {offset}{unit} was not set on channel "
f"{self._channel}, it is {actual_offset}V. Check that the "
f"number is within the possible range and in the correct "
f"format.\nError from the instrument: {self._fgen.get_error()}"
)
raise NotSetError(msg)
def set_frequency(self, freq: float, unit: str = "Hz"):
"""Set the frequency in Hertz (or mHz, kHz, MHz, see options)
Parameters
----------
freq : float
The resolution is 1 μHz or 12 digits.
unit : {mHz, Hz, kHz, MHz}, default Hz
Raises
------
NotSetError
If `self._fgen.verify_param_set` is `True` and the value after
applying the set function does not match the value returned by the
get function
"""
if str(freq).lower() in ["min", "max"]: # handle min and max keywords
unit = "" # no unit for MIN/MAX
# Look up what the limit is for this keyword
freq = self.channel_limits["frequency lims"][0][str(freq).lower()]
else:
# Check if the given frequency is within the current limits
min_freq, max_freq = self.get_frequency_lims()
freq = _SI_prefix_to_factor(unit) * freq
if freq < min_freq or freq > max_freq:
msg = (
f"Could not set the frequency {freq}Hz as it is not "
f"within the frequency limits set for the instrument "
f"[{min_freq}, {max_freq}]Hz"
)
raise NotSetError(msg)
# Set the frequency
self._fgen.write(
f"{self._source}FREQuency:FIXed {freq}{unit}",
custom_err_message=f"set frequency {freq}{unit}",
)
# Verify that the amplitude has been set
if self._fgen.verify_param_set:
actual_freq = self.get_frequency()
# Multiply with the appropriate factor according to SI prefix, or
# if string is empty, use the value looked up from channel_limits earlier
if not unit == "":
check_freq = freq * _SI_prefix_to_factor(unit)
else:
check_freq = freq
if not actual_freq == check_freq:
msg = (
f"Frequency {freq}{unit} was not set on channel {self._channel}"
f", it is {actual_freq}Hz. Check that the number is within "
f"the possible range and in the correct format.\nError "
f"from the instrument: {self._fgen.get_error()}"
)
raise NotSetError(msg)
## ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ EXAMPLES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ##
def example_basic_control(address: str):
"""Example showing how to connect, and the most basic control of the
instrument parameters"""
print("\n\n", example_basic_control.__doc__)
with FuncGen(address) as fgen:
fgen.ch1.set_function("SIN")
fgen.ch1.set_frequency(25, unit="Hz")
fgen.ch1.set_offset(50, unit="mV")
fgen.ch1.set_amplitude(0.002)
fgen.ch1.set_output("ON")
fgen.ch2.set_output("OFF")
# Alternatively fgen.ch1.print_settings() to show from one channel only
fgen.print_settings()
def example_change_settings(address: str):
"""Example showing how to get the current settings of the instrument,
store them, change a setting and then restore the initial settings"""
print("\n\n", example_change_settings.__doc__)
with FuncGen(address) as fgen:
fgen.print_settings()
print("Saving these settings..")
settings = fgen.get_settings()
print("Change to 1Vpp amplitude for channel 1..")
fgen.ch1.set_amplitude(1)
fgen.print_settings()
print("Reset back to initial settings..")
fgen.set_settings(settings)
fgen.print_settings()
def example_lock_frequencies(address: str):
"""Example showing the frequency being set to 10Hz and then the frequency
lock enabled, using the frequency at ch1 as the common frequency"""
print("\n\n", example_lock_frequencies.__doc__)
with FuncGen(address, verbose=False) as fgen:
fgen.ch1.set_frequency(10)
fgen.set_frequency_lock("ON", use_channel=1)
def example_changing_limits(address: str):
"""Example showing how limits can be read and changed"""
print("\n\n", example_changing_limits.__doc__)
with FuncGen(address) as fgen:
lims = fgen.ch1.get_frequency_lims()
print("Channel 1 frequency limits: {}".format(lims))
print("Change the lower limit to 2Hz..")
fgen.ch1.set_limit("frequency lims", "min", 2)
lims = fgen.ch1.get_frequency_lims()
print("Channel 1 frequency limits: {}".format(lims))
print("Try to set ch1 frequency to 1Hz..")
try:
fgen.ch1.set_frequency(1)
except NotSetError as err:
print(err)
def example_set_and_use_custom_waveform(
fgen: FuncGen = None,
address: str = None,
channel: int = 1,
plot_signal: bool = True,
):
"""Example showing a waveform being created, transferred to the instrument,
and applied to a channel"""
print("\n\n", example_set_and_use_custom_waveform.__doc__)
# Create a signal
x = np.linspace(0, 4 * np.pi, 8000)
signal = np.sin(x) + x / 5
if plot_signal: # plot the signal for visual control
import matplotlib.pyplot as plt
plt.plot(signal)
plt.show()
# Create initialise fgen if it was not supplied
if fgen is None:
fgen = FuncGen(address)
close_fgen = True # specify that it should be closed at end of function
else:
close_fgen = False # do not close the supplied fgen at end
print("Current waveform catalogue")
for i, wav in enumerate(fgen.get_waveform_catalogue()):
print(f" {i}: {wav}")
# Transfer the waveform
fgen.set_custom_waveform(signal, memory_num=5, verify=True)
print("New waveform catalogue:")
for i, wav in enumerate(fgen.get_waveform_catalogue()):
print(f" {i}: {wav}")
print(f"Set new wavefrom to channel {channel}..", end=" ")
fgen.channels[channel - 1].set_output_state("OFF")
fgen.channels[channel - 1].set_function("USER5")
print("ok")
# Print current settings
fgen.get_settings()
if close_fgen:
fgen.close()
## ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ MAIN FUNCTION ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ##
if __name__ == "__main__":
example_basic_control(_VISA_ADDRESS)
example_change_settings(_VISA_ADDRESS)
example_lock_frequencies(_VISA_ADDRESS)
example_changing_limits(_VISA_ADDRESS)
with FuncGen(_VISA_ADDRESS) as fgen:
example_set_and_use_custom_waveform(fgen)
Functions
def example_basic_control(address: str)
-
Example showing how to connect, and the most basic control of the instrument parameters
Expand source code
def example_basic_control(address: str): """Example showing how to connect, and the most basic control of the instrument parameters""" print("\n\n", example_basic_control.__doc__) with FuncGen(address) as fgen: fgen.ch1.set_function("SIN") fgen.ch1.set_frequency(25, unit="Hz") fgen.ch1.set_offset(50, unit="mV") fgen.ch1.set_amplitude(0.002) fgen.ch1.set_output("ON") fgen.ch2.set_output("OFF") # Alternatively fgen.ch1.print_settings() to show from one channel only fgen.print_settings()
def example_change_settings(address: str)
-
Example showing how to get the current settings of the instrument, store them, change a setting and then restore the initial settings
Expand source code
def example_change_settings(address: str): """Example showing how to get the current settings of the instrument, store them, change a setting and then restore the initial settings""" print("\n\n", example_change_settings.__doc__) with FuncGen(address) as fgen: fgen.print_settings() print("Saving these settings..") settings = fgen.get_settings() print("Change to 1Vpp amplitude for channel 1..") fgen.ch1.set_amplitude(1) fgen.print_settings() print("Reset back to initial settings..") fgen.set_settings(settings) fgen.print_settings()
def example_changing_limits(address: str)
-
Example showing how limits can be read and changed
Expand source code
def example_changing_limits(address: str): """Example showing how limits can be read and changed""" print("\n\n", example_changing_limits.__doc__) with FuncGen(address) as fgen: lims = fgen.ch1.get_frequency_lims() print("Channel 1 frequency limits: {}".format(lims)) print("Change the lower limit to 2Hz..") fgen.ch1.set_limit("frequency lims", "min", 2) lims = fgen.ch1.get_frequency_lims() print("Channel 1 frequency limits: {}".format(lims)) print("Try to set ch1 frequency to 1Hz..") try: fgen.ch1.set_frequency(1) except NotSetError as err: print(err)
def example_lock_frequencies(address: str)
-
Example showing the frequency being set to 10Hz and then the frequency lock enabled, using the frequency at ch1 as the common frequency
Expand source code
def example_lock_frequencies(address: str): """Example showing the frequency being set to 10Hz and then the frequency lock enabled, using the frequency at ch1 as the common frequency""" print("\n\n", example_lock_frequencies.__doc__) with FuncGen(address, verbose=False) as fgen: fgen.ch1.set_frequency(10) fgen.set_frequency_lock("ON", use_channel=1)
def example_set_and_use_custom_waveform(fgen: FuncGen = None, address: str = None, channel: int = 1, plot_signal: bool = True)
-
Example showing a waveform being created, transferred to the instrument, and applied to a channel
Expand source code
def example_set_and_use_custom_waveform( fgen: FuncGen = None, address: str = None, channel: int = 1, plot_signal: bool = True, ): """Example showing a waveform being created, transferred to the instrument, and applied to a channel""" print("\n\n", example_set_and_use_custom_waveform.__doc__) # Create a signal x = np.linspace(0, 4 * np.pi, 8000) signal = np.sin(x) + x / 5 if plot_signal: # plot the signal for visual control import matplotlib.pyplot as plt plt.plot(signal) plt.show() # Create initialise fgen if it was not supplied if fgen is None: fgen = FuncGen(address) close_fgen = True # specify that it should be closed at end of function else: close_fgen = False # do not close the supplied fgen at end print("Current waveform catalogue") for i, wav in enumerate(fgen.get_waveform_catalogue()): print(f" {i}: {wav}") # Transfer the waveform fgen.set_custom_waveform(signal, memory_num=5, verify=True) print("New waveform catalogue:") for i, wav in enumerate(fgen.get_waveform_catalogue()): print(f" {i}: {wav}") print(f"Set new wavefrom to channel {channel}..", end=" ") fgen.channels[channel - 1].set_output_state("OFF") fgen.channels[channel - 1].set_function("USER5") print("ok") # Print current settings fgen.get_settings() if close_fgen: fgen.close()
Classes
class FuncGen (visa_address: str, impedance: Tuple[str, str] = ('highZ', 'highZ'), timeout: int = 1000, verify_param_set: bool = False, override_compatibility: str = '', verbose: bool = True)
-
Class for interacting with Tektronix function generator
Parameters
visa_address
:str
- VISA address of the insrument
impedance
:tuple
of{"highZ", "50ohm"}
, default("highZ",)*2
- Determines voltage limits associated with high impedance (whether the
instrument is using 50ohm or high Z cannot be controlled through VISA).
For example
("highZ", "50ohm")
for to use high Z for ch1 and 50 ohm for ch2 timeout
:int
, default1000
- Timeout in milliseconds of instrument connection
verify_param_set
:bool
, defaultFalse
- Verify that a value is successfully set after executing a set function
verbose
:bool
, defaultTrue
- Choose whether to print information such as model upon connecting etc
override_compatibility
:str
, default""
- If the instrument limits for the model connected to are not known
NotCompatibleError
will be raised. To override and use either of AFG1022, AFG1062, or AFG3022 limits, use their respecive model names as argument. Note that this might lead to unexpected behaviour for custom waveforms and 'MIN'/'MAX' keywords.
Attributes
_visa_address
:str
- The VISA address of the instrument
_id
:str
- Comma separated string with maker, model, serial and firmware of the instrument
_inst
:pyvisa.resources.Resource
- The PyVISA resource
_arbitrary_waveform_length
:list
- The permitted minimum and maximum length of an arbitrary waveform, e.g. [2, 8192]
_arbitrary_waveform_resolution
:int
- The vertical resolution of the arbitrary waveform, for instance 14 bit => 2**14-1 = 16383
_max_waveform_memory_user_locations
:int
- The number of the last user memory location available
Raises
pyvisa.Error
- If the supplied VISA address cannot be connected to
NotCompatibleError
- If the instrument limits for the model connected to are not known
(Call the class with
override_compatibility=True
to override and use AFG1022 limits)
Expand source code
class FuncGen: """Class for interacting with Tektronix function generator Parameters ---------- visa_address : str VISA address of the insrument impedance : tuple of {"highZ", "50ohm"}, default ("highZ",)*2 Determines voltage limits associated with high impedance (whether the instrument is using 50ohm or high Z cannot be controlled through VISA). For example `("highZ", "50ohm")` for to use high Z for ch1 and 50 ohm for ch2 timeout : int, default 1000 Timeout in milliseconds of instrument connection verify_param_set : bool, default False Verify that a value is successfully set after executing a set function verbose : bool, default `True` Choose whether to print information such as model upon connecting etc override_compatibility : str, default `""` If the instrument limits for the model connected to are not known `NotCompatibleError` will be raised. To override and use either of AFG1022, AFG1062, or AFG3022 limits, use their respecive model names as argument. Note that this might lead to unexpected behaviour for custom waveforms and 'MIN'/'MAX' keywords. Attributes ---------- _visa_address : str The VISA address of the instrument _id : str Comma separated string with maker, model, serial and firmware of the instrument _inst : `pyvisa.resources.Resource` The PyVISA resource _arbitrary_waveform_length : list The permitted minimum and maximum length of an arbitrary waveform, e.g. [2, 8192] _arbitrary_waveform_resolution : int The vertical resolution of the arbitrary waveform, for instance 14 bit => 2**14-1 = 16383 _max_waveform_memory_user_locations : int The number of the last user memory location available Raises ------ pyvisa.Error If the supplied VISA address cannot be connected to NotCompatibleError If the instrument limits for the model connected to are not known (Call the class with `override_compatibility=True` to override and use AFG1022 limits) """ _is_connected = False """bool: Keeping track of whether the PYVISA connection has been established""" instrument_limits = {} """dict: Contains the following keys with subdictionaries - `frequency lims` Containing the frequency limits for the instrument where the keys "min" and "max" have values corresponding to minimum and maximum frequencies in Hz - `voltage lims` Contains the maximum absolute voltage the instrument can output for the keys "50ohm" and "highZ" according to the impedance setting - `amplitude lims` Contains the smallest and largest possible amplitudes where the keys "50ohm" and "highZ" will have subdictionaries with keys "min" and "max" """ def __init__( self, visa_address: str, impedance: Tuple[str, str] = ("highZ",) * 2, timeout: int = 1000, verify_param_set: bool = False, override_compatibility: str = "", verbose: bool = True, ): self._override_compat = override_compatibility self._visa_address = visa_address self.verify_param_set = verify_param_set """bool: Verify that a value is successfully set after executing a set function""" self.verbose = verbose """bool: Choose whether to print information such as model upon connecting etc""" self.open(visa_address, timeout) self._initialise_model_properties() self.channels = ( self._spawn_channel(1, impedance[0]), self._spawn_channel(2, impedance[1]), ) """tuple of `FuncGenChannel`: Objects to control the channels""" self.ch1 = self.channels[0] """`FuncGenChannel`: Short hand for `channels[0]` Object to control channel 1""" self.ch2 = self.channels[1] """`FuncGenChannel`: Short hand for `channels[1]` Object to control channel 2""" def __enter__(self, **kwargs): # The kwargs will be passed on to __init__ return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() def __del__(self): self.close() def open(self, visa_address: str, timeout: int): try: rm = pyvisa.ResourceManager() self._inst = rm.open_resource(visa_address) except pyvisa.Error: print(f"\nVisaError: Could not connect to '{visa_address}'") raise self._is_connected = True self.timeout = timeout # Clear all the event registers and queues used in the instrument # status and event reporting system self.write("*CLS") # Get information about the connected device self._id = self.query("*IDN?") # Second query might be needed due to unknown reason if self._id == "": self._id = self.query("*IDN?") self._maker, self._model, self._serial = self._id.split(",")[:3] if self.verbose: print( f"Connected to {self._maker} model {self._model}, " f"serial {self._serial}" ) def close(self): """Close the connection to the instrument""" if self._is_connected: self._inst.close() self._is_connected = False @property def timeout(self) -> int: """The timeout of the PYVISA connection in milliseconds""" return self._inst.timeout @timeout.setter def timeout(self, ms: int): self._inst.timeout = ms def _initialise_model_properties(self): """Initialises the limits of what the instrument can handle according to the instrument model Raises ------ NotCompatibleError If the connected model is not necessarily compatible with this package, slimits are not known. """ if np.any(["AFG1022" in a for a in [self._model, self._override_compat]]): self.instrument_limits = { "frequency lims": ({"min": 1e-6, "max": 25e6}, "Hz"), "voltage lims": ( {"50ohm": {"min": -5, "max": 5}, "highZ": {"min": -10, "max": 10}}, "V", ), "amplitude lims": ( { "50ohm": {"min": 0.001, "max": 10}, "highZ": {"min": 0.002, "max": 20}, }, "Vpp", ), } self._arbitrary_waveform_length = [2, 8192] # min length, max length self._arbitrary_waveform_resolution = 16383 # 14 bit self._max_waveform_memory_user_locations = 255 elif np.any(["AFG1062" in a for a in [self._model, self._override_compat]]): self.instrument_limits = { "frequency lims": ({"min": 1e-6, "max": 60e6}, "Hz"), "voltage lims": ( {"50ohm": {"min": -5, "max": 5}, "highZ": {"min": -10, "max": 10}}, "V", ), "amplitude lims": ( { "50ohm": {"min": 0.001, "max": 10}, "highZ": {"min": 0.002, "max": 20}, }, "Vpp", ), } self._arbitrary_waveform_length = [2, 1e6] # min length, max length self._arbitrary_waveform_resolution = 16383 # 14 bit self._max_waveform_memory_user_locations = 31 elif np.any(["AFG3022" in a for a in [self._model, self._override_compat]]): self.instrument_limits = { "frequency lims": ({"min": 1e-6, "max": 25e6}, "Hz"), "voltage lims": ( {"50ohm": {"min": -5, "max": 5}, "highZ": {"min": -10, "max": 10}}, "V", ), "amplitude lims": ( { "50ohm": {"min": 0.01, "max": 10}, "highZ": {"min": 0.02, "max": 20}, }, "Vpp", ), } self._arbitrary_waveform_length = [2, 65536] # min length, max length self._arbitrary_waveform_resolution = 16383 # 14 bit self._max_waveform_memory_user_locations = 4 else: msg = ( f"Model {self._model} might not be fully supported!\n" " The module has been tested with AFG1022, AFG1062, and AFG3022.\n" " To initiate and use the module as any of these, call the\n" " class with for instance `override_compatibility='AFG1022'`\n" " Note that this might lead to unexpected behaviour\n" " for custom waveforms and 'MIN'/'MAX' keywords." ) raise NotCompatibleError(msg) def write(self, command: str, custom_err_message: str = None) -> int: """Write a VISA command to the instrument Parameters ---------- command : str The VISA command to be written to the instrument custom_err_message : str, default `None` When `None`, the RuntimeError message is "Writing/querying command {command} failed: pyvisa returned StatusCode ..". Otherwise, if a message is supplied "Could not {message}: pyvisa returned StatusCode .." Returns ------- bytes : int Number of bytes tranferred Raises ------ RuntimeError If status returned by PyVISA write command is not `pyvisa.constants.StatusCode.success` """ num_bytes = self._inst.write(command) self._check_pyvisa_status(command, custom_err_message=custom_err_message) return num_bytes def query(self, command: str, custom_err_message: str = None) -> str: """Query the instrument Parameters ---------- command : str The VISA query command custom_err_message : str, default `None` When `None`, the RuntimeError message is "Writing/querying command {command} failed: pyvisa returned StatusCode ..". Otherwise, if a message is supplied "Could not {message}: pyvisa returned StatusCode .." Returns ------- str The instrument's response Raises ------ RuntimeError If status returned by PyVISA write command is not `pyvisa.constants.StatusCode.success` """ response = self._inst.query(command).strip() self._check_pyvisa_status(command, custom_err_message=custom_err_message) return response def _check_pyvisa_status(self, command: str, custom_err_message: str = None): """Check the last status code of PyVISA Parameters ---------- command : str The VISA write/query command Returns ------- status : pyvisa.constants.StatusCode Return value of the library call Raises ------ RuntimeError If status returned by PyVISA write command is not `pyvisa.constants.StatusCode.success` """ status = self._inst.last_status if not status == pyvisa.constants.StatusCode.success: if custom_err_message is not None: msg = ( f"Could not {custom_err_message}: pyvisa returned " f"StatusCode {status} ({str(status)})" ) raise RuntimeError(msg) msg = ( f"Writing/querying command {command} failed: pyvisa returned " f"StatusCode {status} ({str(status)})" ) raise RuntimeError(msg) return status def get_error(self) -> str: """Get the contents of the Error/Event queue on the device Returns ------- str Error/event number, description of error/event """ return self.query("SYSTEM:ERROR:NEXT?") def _spawn_channel(self, channel: int, impedance: str) -> "FuncGenChannel": """Wrapper function to create a `FuncGenChannel` object for a channel -- see the class docstring""" return FuncGenChannel(self, channel, impedance) def get_settings(self) -> List[dict]: """Get dictionaries of the current settings of the two channels Returns ------- settings : list of dicts [ch1_dict, ch2_dict]: Settings currently in use as a dictionary with keys output, function, amplitude, offset, and frequency with corresponding values """ return [ch.get_settings() for ch in self.channels] def print_settings(self): """Prints table of the current setting for both channels""" settings = self.get_settings() # Find the necessary padding for the table columns # by evaluating the maximum length of the entries key_padding = max([len(key) for key in settings[0].keys()]) ch_paddings = [ max([len(str(val[0])) for val in ch_settings.values()]) for ch_settings in settings ] padding = [key_padding] + ch_paddings print(f"\nCurrent settings for {self._maker} {self._model} {self._serial}\n") row_format = "{:>{padd[0]}s} {:{padd[1]}s} {:{padd[2]}s} {}" table_header = row_format.format("Setting", "Ch1", "Ch2", "Unit", padd=padding) print(table_header) print("=" * len(table_header)) for (ch1key, (ch1val, unit)), (_, (ch2val, _)) in zip( settings[0].items(), settings[1].items() ): print( row_format.format(ch1key, str(ch1val), str(ch2val), unit, padd=padding) ) def set_settings(self, settings: List[dict]): """Set the settings of both channels with settings dictionaries (Each channel is turned off before applying the changes to avoid potentially harmful combinations) Parameteres ----------- settings : list of dicts List of settings dictionaries as returned by `get_settings`, first entry for channel 1, second for channel 2. The dictionaries should have keys output, function, amplitude, offset, and frequency """ for ch, s in zip(self.channels, settings): ch.set_settings(s) def syncronise_waveforms(self): """Syncronise waveforms of the two channels when using the same frequency Note: Does NOT enable the frequency lock that can be enabled on the user interface of the instrument) """ self.write(":PHAS:INIT", custom_err_message="syncronise waveforms") def get_frequency_lock(self) -> bool: """Check if frequency lock is enabled Returns ------- bool `True` if frequency lock enabled """ # If one is locked so is the other, so just need to check one return int(self.query("SOURCE1:FREQuency:CONCurrent?")) == 1 def set_frequency_lock(self, state: str, use_channel: int = 1): """Enable the frequency lock to make the two channels have the same frequency and phase of their signals, also after adjustments. See also `FuncGen.syncronise_waveforms` for one-time sync only. Parameters ---------- state : {"ON", "OFF"} ON to enable, OFF to disable the lock use_channel : int, default 1 Only relevant if turning the lock ON: The channel whose frequency shall be used as the common freqency """ if self.verbose: if state.lower() == "off" and not self.get_frequency_lock(): print( f"(!) {self._model}: Tried to disable frequency lock, but " f"frequency lock was not enabled" ) return if state.lower() == "on" and self.get_frequency_lock(): print( f"(!) {self._model}: Tried to enable frequency lock, but " f"frequency lock was already enabled" ) return # (Sufficient to disable for only one of the channels) cmd = f"SOURCE{use_channel}:FREQuency:CONCurrent {state}" msg = f"turn frequency lock {state}" self.write(cmd, custom_err_message=msg) def software_trig(self): """NOT TESTED: sends a trigger signal to the device (for bursts or modulations)""" self.write("*TRG", custom_err_message="send trigger signal") ## ~~~~~~~~~~~~~~~~~~~~~ CUSTOM WAVEFORM FUNCTIONS ~~~~~~~~~~~~~~~~~~~~~ ## def get_waveform_catalogue(self) -> List[str]: """Get list of the waveforms that are in use (not empty) Returns ------- catalogue : list Strings with the names of the user functions that are not empty """ catalogue = self.query("DATA:CATalog?").split(",") catalogue = [wf[1:-1] for wf in catalogue] # strip off extra quotes return catalogue def get_custom_waveform(self, memory_num: int) -> np.ndarray: """Get the waveform currently stored in USER<memory_num> Parameters ---------- memory_num : str or int {0,...,255}, default 0 Select which user memory to compare with Returns ------- waveform : ndarray Waveform as ints spanning the resolution of the function gen or and empty array if waveform not in use """ # Find the wavefroms in use waveforms_in_use = self.get_waveform_catalogue() if f"USER{memory_num}" in waveforms_in_use: # Copy the waveform to edit memory self.write(f"DATA:COPY EMEMory,USER{memory_num}") # Get the length of the waveform waveform_length = int(self.query("DATA:POINts? EMEMory")) # Get the waveform (returns binary values) waveform = self._inst.query_binary_values( "DATA:DATA? EMEMory", datatype="H", is_big_endian=True, container=np.ndarray, ) msg = ( f"Waveform length from native length command (DATA:POINts?) " f"and the processed binary values do not match, " f"{waveform_length} and {len(waveform)} respectively" ) assert len(waveform) == waveform_length, msg return waveform print(f"Waveform USER{memory_num} is not in use") return np.array([]) def set_custom_waveform( self, waveform: np.ndarray, normalise: bool = True, memory_num: int = 0, verify: bool = True, print_progress: bool = True, ): """Transfer waveform data to edit memory and then user memory. NOTE: Will overwrite without warnings Parameters ---------- waveform : ndarray Either unnormalised arbitrary waveform (then use `normalise=True`), or ints spanning the resolution of the function generator normalise : bool Choose whether to normalise the waveform to ints over the resolution span of the function generator memory_num : str or int {0,...,255}, default 0 Select which user memory to copy to verify : bool, default `True` Verify that the waveform has been transferred and is what was sent print_progress : bool, default `True` Returns ------- waveform : ndarray The normalised waveform transferred Raises ------ ValueError If the waveform is not within the permitted length or value range RuntimeError If the waveform transferred to the instrument is of a different length than the waveform supplied """ if not 0 <= memory_num <= self._max_waveform_memory_user_locations: raise ValueError( f"The memory location {memory_num} is not a valid " "memory location for this model" ) # Check if waveform data is suitable if print_progress: print("Check if waveform data is suitable..", end=" ") self._check_arb_waveform_length(waveform) try: self._check_arb_waveform_type_and_range(waveform) except ValueError as err: if print_progress: print(f"\n {err}") print("Trying again normalising the waveform..", end=" ") waveform = self._normalise_to_waveform(waveform) if print_progress: print("ok") print("Transfer waveform to function generator..", end=" ") # Transfer waveform self._inst.write_binary_values( "DATA:DATA EMEMory,", waveform, datatype="H", is_big_endian=True ) # Check for errors and check lengths are matching transfer_error = self.get_error() emem_wf_length = self.query("DATA:POINts? EMEMory") if emem_wf_length == "" or not int(emem_wf_length) == len(waveform): msg = ( f"Waveform in temporary EMEMory has a length of {emem_wf_length}" f", not of the same length as the waveform ({len(waveform)})." f"\nError from the instrument: {transfer_error}" ) raise RuntimeError(msg) if print_progress: print("ok") print(f"Copy waveform to USER{memory_num}..", end=" ") self.write(f"DATA:COPY USER{memory_num},EMEMory") if print_progress: print("ok") if verify: if print_progress: print(f"Verify waveform USER{memory_num}..") if f"USER{memory_num}" in self.get_waveform_catalogue(): verif = self._verify_waveform( waveform, memory_num, normalise=normalise, print_result=print_progress, ) if not verif[0]: raise RuntimeError( f"USER{memory_num} does not contain the waveform" ) else: raise RuntimeError(f"USER{memory_num} is empty") return waveform def _normalise_to_waveform(self, shape: np.ndarray) -> np.ndarray: """Normalise a shape of any discretisation and range to a waveform that can be transmitted to the function generator .. note:: If you are transferring a flat/constant waveform, do not use this normaisation function. Transfer a waveform like `int(self._arbitrary_waveform_resolution/2)*np.ones(2).astype(np.int32)` without normalising for a well behaved flat function. Parameters ---------- shape : array_like Array to be transformed to waveform, can be ints or floats, any normalisation or discretisation Returns ------- waveform : ndarray Waveform as ints spanning the resolution of the function gen """ # Check if waveform data is suitable self._check_arb_waveform_length(shape) # Normalise waveform = shape - np.min(shape) normalisation_factor = np.max(waveform) waveform = waveform / normalisation_factor * self._arbitrary_waveform_resolution return waveform.astype(np.uint16) def _verify_waveform( self, waveform: np.ndarray, memory_num: int, normalise: bool = True, print_result: bool = True, ) -> Tuple[bool, np.ndarray, list]: """Compare a waveform in user memory to argument waveform Parameters ---------- waveform : ndarray Waveform as ints spanning the resolution of the function gen memory_num : str or int {0,...,255}, default 0 Select which user memory to compare with normalise : bool, default `True` Normalise test waveform Returns ------- bool Boolean according to equal/not equal instrument_waveform The waveform on the instrument list or `None` List of the indices where the waveforms are not equal or `None` if the waveforms were of different lengths """ if normalise: # make sure test waveform is normalised waveform = self._normalise_to_waveform(waveform) # Get the waveform on the instrument instrument_waveform = self.get_custom_waveform(memory_num) # Compare lengths len_inst_wav, len_wav = len(instrument_waveform), len(waveform) if not len_inst_wav == len_wav: if print_result: print( f"The waveform in USER{memory_num} and the compared " f"waveform are not of same length (instrument " f"{len_inst_wav} vs {len_wav})" ) return False, instrument_waveform, None # Compare each element not_equal = [] for i in range(len_wav): if not instrument_waveform[i] == waveform[i]: not_equal.append(i) # Return depending of whether list is empty or not if not not_equal: # if list is empty if print_result: print( f"The waveform in USER{memory_num} and the compared " f"waveform are equal" ) return True, instrument_waveform, not_equal if print_result: print( f"The waveform in USER{memory_num} and the compared " f"waveform are NOT equal" ) return False, instrument_waveform, not_equal def _check_arb_waveform_length(self, waveform: np.ndarray): """Checks if waveform is within the acceptable length Parameters ---------- waveform : array_like Waveform or voltage list to be checked Raises ------ ValueError If the waveform is not within the permitted length """ if (len(waveform) < self._arbitrary_waveform_length[0]) or ( len(waveform) > self._arbitrary_waveform_length[1] ): msg = ( "The waveform is of length {}, which is not within the " "acceptable length {} < len < {}" "".format(len(waveform), *self._arbitrary_waveform_length) ) raise ValueError(msg) def _check_arb_waveform_type_and_range(self, waveform: np.ndarray): """Checks if waveform is of int/np.int32 type and within the resolution of the function generator Parameters ---------- waveform : array_like Waveform or voltage list to be checked Raises ------ ValueError If the waveform values are not int, np.uint16 or np.int32, or the values are not within the permitted range """ for value in waveform: if not isinstance(value, (int, np.uint16, np.int32)): raise ValueError( "The waveform contains values that are not" "int, np.uint16 or np.int32" ) if (value < 0) or (value > self._arbitrary_waveform_resolution): raise ValueError( f"The waveform contains values out of range " f"({value} is not within the resolution " f"[0, {self._arbitrary_waveform_resolution}])" )
Class variables
var instrument_limits
-
dict: Contains the following keys with subdictionaries
frequency lims
Containing the frequency limits for the instrument where the keys "min" and "max" have values corresponding to minimum and maximum frequencies in Hzvoltage lims
Contains the maximum absolute voltage the instrument can output for the keys "50ohm" and "highZ" according to the impedance settingamplitude lims
Contains the smallest and largest possible amplitudes where the keys "50ohm" and "highZ" will have subdictionaries with keys "min" and "max"
Instance variables
var ch1
-
FuncGenChannel
: Short hand forchannels[0]
Object to control channel 1 var ch2
-
FuncGenChannel
: Short hand forchannels[1]
Object to control channel 2 var channels
-
tuple of
FuncGenChannel
: Objects to control the channels var timeout : int
-
The timeout of the PYVISA connection in milliseconds
Expand source code
@property def timeout(self) -> int: """The timeout of the PYVISA connection in milliseconds""" return self._inst.timeout
var verbose
-
bool: Choose whether to print information such as model upon connecting etc
var verify_param_set
-
bool: Verify that a value is successfully set after executing a set function
Methods
def close(self)
-
Close the connection to the instrument
Expand source code
def close(self): """Close the connection to the instrument""" if self._is_connected: self._inst.close() self._is_connected = False
def get_custom_waveform(self, memory_num: int) ‑> numpy.ndarray
-
Get the waveform currently stored in USER
Parameters
memory_num
:str
orint {0,...,255}
, default0
- Select which user memory to compare with
Returns
waveform
:ndarray
- Waveform as ints spanning the resolution of the function gen or and empty array if waveform not in use
Expand source code
def get_custom_waveform(self, memory_num: int) -> np.ndarray: """Get the waveform currently stored in USER<memory_num> Parameters ---------- memory_num : str or int {0,...,255}, default 0 Select which user memory to compare with Returns ------- waveform : ndarray Waveform as ints spanning the resolution of the function gen or and empty array if waveform not in use """ # Find the wavefroms in use waveforms_in_use = self.get_waveform_catalogue() if f"USER{memory_num}" in waveforms_in_use: # Copy the waveform to edit memory self.write(f"DATA:COPY EMEMory,USER{memory_num}") # Get the length of the waveform waveform_length = int(self.query("DATA:POINts? EMEMory")) # Get the waveform (returns binary values) waveform = self._inst.query_binary_values( "DATA:DATA? EMEMory", datatype="H", is_big_endian=True, container=np.ndarray, ) msg = ( f"Waveform length from native length command (DATA:POINts?) " f"and the processed binary values do not match, " f"{waveform_length} and {len(waveform)} respectively" ) assert len(waveform) == waveform_length, msg return waveform print(f"Waveform USER{memory_num} is not in use") return np.array([])
def get_error(self) ‑> str
-
Get the contents of the Error/Event queue on the device
Returns
str
- Error/event number, description of error/event
Expand source code
def get_error(self) -> str: """Get the contents of the Error/Event queue on the device Returns ------- str Error/event number, description of error/event """ return self.query("SYSTEM:ERROR:NEXT?")
def get_frequency_lock(self) ‑> bool
-
Check if frequency lock is enabled
Returns
bool
True
if frequency lock enabled
Expand source code
def get_frequency_lock(self) -> bool: """Check if frequency lock is enabled Returns ------- bool `True` if frequency lock enabled """ # If one is locked so is the other, so just need to check one return int(self.query("SOURCE1:FREQuency:CONCurrent?")) == 1
def get_settings(self) ‑> List[dict]
-
Get dictionaries of the current settings of the two channels
Returns
settings
:list
ofdicts
- [ch1_dict, ch2_dict]: Settings currently in use as a dictionary with keys output, function, amplitude, offset, and frequency with corresponding values
Expand source code
def get_settings(self) -> List[dict]: """Get dictionaries of the current settings of the two channels Returns ------- settings : list of dicts [ch1_dict, ch2_dict]: Settings currently in use as a dictionary with keys output, function, amplitude, offset, and frequency with corresponding values """ return [ch.get_settings() for ch in self.channels]
def get_waveform_catalogue(self) ‑> List[str]
-
Get list of the waveforms that are in use (not empty)
Returns
catalogue
:list
- Strings with the names of the user functions that are not empty
Expand source code
def get_waveform_catalogue(self) -> List[str]: """Get list of the waveforms that are in use (not empty) Returns ------- catalogue : list Strings with the names of the user functions that are not empty """ catalogue = self.query("DATA:CATalog?").split(",") catalogue = [wf[1:-1] for wf in catalogue] # strip off extra quotes return catalogue
def open(self, visa_address: str, timeout: int)
-
Expand source code
def open(self, visa_address: str, timeout: int): try: rm = pyvisa.ResourceManager() self._inst = rm.open_resource(visa_address) except pyvisa.Error: print(f"\nVisaError: Could not connect to '{visa_address}'") raise self._is_connected = True self.timeout = timeout # Clear all the event registers and queues used in the instrument # status and event reporting system self.write("*CLS") # Get information about the connected device self._id = self.query("*IDN?") # Second query might be needed due to unknown reason if self._id == "": self._id = self.query("*IDN?") self._maker, self._model, self._serial = self._id.split(",")[:3] if self.verbose: print( f"Connected to {self._maker} model {self._model}, " f"serial {self._serial}" )
def print_settings(self)
-
Prints table of the current setting for both channels
Expand source code
def print_settings(self): """Prints table of the current setting for both channels""" settings = self.get_settings() # Find the necessary padding for the table columns # by evaluating the maximum length of the entries key_padding = max([len(key) for key in settings[0].keys()]) ch_paddings = [ max([len(str(val[0])) for val in ch_settings.values()]) for ch_settings in settings ] padding = [key_padding] + ch_paddings print(f"\nCurrent settings for {self._maker} {self._model} {self._serial}\n") row_format = "{:>{padd[0]}s} {:{padd[1]}s} {:{padd[2]}s} {}" table_header = row_format.format("Setting", "Ch1", "Ch2", "Unit", padd=padding) print(table_header) print("=" * len(table_header)) for (ch1key, (ch1val, unit)), (_, (ch2val, _)) in zip( settings[0].items(), settings[1].items() ): print( row_format.format(ch1key, str(ch1val), str(ch2val), unit, padd=padding) )
def query(self, command: str, custom_err_message: str = None) ‑> str
-
Query the instrument
Parameters
command
:str
- The VISA query command
custom_err_message
:str
, defaultNone
- When
None
, the RuntimeError message is "Writing/querying command {command} failed: pyvisa returned StatusCode ..". Otherwise, if a message is supplied "Could not {message}: pyvisa returned StatusCode .."
Returns
str
- The instrument's response
Raises
RuntimeError
- If status returned by PyVISA write command is not
pyvisa.constants.StatusCode.success
Expand source code
def query(self, command: str, custom_err_message: str = None) -> str: """Query the instrument Parameters ---------- command : str The VISA query command custom_err_message : str, default `None` When `None`, the RuntimeError message is "Writing/querying command {command} failed: pyvisa returned StatusCode ..". Otherwise, if a message is supplied "Could not {message}: pyvisa returned StatusCode .." Returns ------- str The instrument's response Raises ------ RuntimeError If status returned by PyVISA write command is not `pyvisa.constants.StatusCode.success` """ response = self._inst.query(command).strip() self._check_pyvisa_status(command, custom_err_message=custom_err_message) return response
def set_custom_waveform(self, waveform: numpy.ndarray, normalise: bool = True, memory_num: int = 0, verify: bool = True, print_progress: bool = True)
-
Transfer waveform data to edit memory and then user memory. NOTE: Will overwrite without warnings
Parameters
waveform
:ndarray
- Either unnormalised arbitrary waveform (then use
normalise=True
), or ints spanning the resolution of the function generator normalise
:bool
- Choose whether to normalise the waveform to ints over the resolution span of the function generator
memory_num
:str
orint {0,...,255}
, default0
- Select which user memory to copy to
verify
:bool
, defaultTrue
- Verify that the waveform has been transferred and is what was sent
print_progress
:bool
, defaultTrue
Returns
waveform
:ndarray
- The normalised waveform transferred
Raises
ValueError
- If the waveform is not within the permitted length or value range
RuntimeError
- If the waveform transferred to the instrument is of a different length than the waveform supplied
Expand source code
def set_custom_waveform( self, waveform: np.ndarray, normalise: bool = True, memory_num: int = 0, verify: bool = True, print_progress: bool = True, ): """Transfer waveform data to edit memory and then user memory. NOTE: Will overwrite without warnings Parameters ---------- waveform : ndarray Either unnormalised arbitrary waveform (then use `normalise=True`), or ints spanning the resolution of the function generator normalise : bool Choose whether to normalise the waveform to ints over the resolution span of the function generator memory_num : str or int {0,...,255}, default 0 Select which user memory to copy to verify : bool, default `True` Verify that the waveform has been transferred and is what was sent print_progress : bool, default `True` Returns ------- waveform : ndarray The normalised waveform transferred Raises ------ ValueError If the waveform is not within the permitted length or value range RuntimeError If the waveform transferred to the instrument is of a different length than the waveform supplied """ if not 0 <= memory_num <= self._max_waveform_memory_user_locations: raise ValueError( f"The memory location {memory_num} is not a valid " "memory location for this model" ) # Check if waveform data is suitable if print_progress: print("Check if waveform data is suitable..", end=" ") self._check_arb_waveform_length(waveform) try: self._check_arb_waveform_type_and_range(waveform) except ValueError as err: if print_progress: print(f"\n {err}") print("Trying again normalising the waveform..", end=" ") waveform = self._normalise_to_waveform(waveform) if print_progress: print("ok") print("Transfer waveform to function generator..", end=" ") # Transfer waveform self._inst.write_binary_values( "DATA:DATA EMEMory,", waveform, datatype="H", is_big_endian=True ) # Check for errors and check lengths are matching transfer_error = self.get_error() emem_wf_length = self.query("DATA:POINts? EMEMory") if emem_wf_length == "" or not int(emem_wf_length) == len(waveform): msg = ( f"Waveform in temporary EMEMory has a length of {emem_wf_length}" f", not of the same length as the waveform ({len(waveform)})." f"\nError from the instrument: {transfer_error}" ) raise RuntimeError(msg) if print_progress: print("ok") print(f"Copy waveform to USER{memory_num}..", end=" ") self.write(f"DATA:COPY USER{memory_num},EMEMory") if print_progress: print("ok") if verify: if print_progress: print(f"Verify waveform USER{memory_num}..") if f"USER{memory_num}" in self.get_waveform_catalogue(): verif = self._verify_waveform( waveform, memory_num, normalise=normalise, print_result=print_progress, ) if not verif[0]: raise RuntimeError( f"USER{memory_num} does not contain the waveform" ) else: raise RuntimeError(f"USER{memory_num} is empty") return waveform
def set_frequency_lock(self, state: str, use_channel: int = 1)
-
Enable the frequency lock to make the two channels have the same frequency and phase of their signals, also after adjustments.
See also
FuncGen.syncronise_waveforms()
for one-time sync only.Parameters
state
:{"ON", "OFF"}
- ON to enable, OFF to disable the lock
use_channel
:int
, default1
- Only relevant if turning the lock ON: The channel whose frequency shall be used as the common freqency
Expand source code
def set_frequency_lock(self, state: str, use_channel: int = 1): """Enable the frequency lock to make the two channels have the same frequency and phase of their signals, also after adjustments. See also `FuncGen.syncronise_waveforms` for one-time sync only. Parameters ---------- state : {"ON", "OFF"} ON to enable, OFF to disable the lock use_channel : int, default 1 Only relevant if turning the lock ON: The channel whose frequency shall be used as the common freqency """ if self.verbose: if state.lower() == "off" and not self.get_frequency_lock(): print( f"(!) {self._model}: Tried to disable frequency lock, but " f"frequency lock was not enabled" ) return if state.lower() == "on" and self.get_frequency_lock(): print( f"(!) {self._model}: Tried to enable frequency lock, but " f"frequency lock was already enabled" ) return # (Sufficient to disable for only one of the channels) cmd = f"SOURCE{use_channel}:FREQuency:CONCurrent {state}" msg = f"turn frequency lock {state}" self.write(cmd, custom_err_message=msg)
def set_settings(self, settings: List[dict])
-
Set the settings of both channels with settings dictionaries
(Each channel is turned off before applying the changes to avoid potentially harmful combinations)
Parameteres
settings : list of dicts List of settings dictionaries as returned by
get_settings
, first entry for channel 1, second for channel 2. The dictionaries should have keys output, function, amplitude, offset, and frequencyExpand source code
def set_settings(self, settings: List[dict]): """Set the settings of both channels with settings dictionaries (Each channel is turned off before applying the changes to avoid potentially harmful combinations) Parameteres ----------- settings : list of dicts List of settings dictionaries as returned by `get_settings`, first entry for channel 1, second for channel 2. The dictionaries should have keys output, function, amplitude, offset, and frequency """ for ch, s in zip(self.channels, settings): ch.set_settings(s)
def software_trig(self)
-
NOT TESTED: sends a trigger signal to the device (for bursts or modulations)
Expand source code
def software_trig(self): """NOT TESTED: sends a trigger signal to the device (for bursts or modulations)""" self.write("*TRG", custom_err_message="send trigger signal")
def syncronise_waveforms(self)
-
Syncronise waveforms of the two channels when using the same frequency
Note: Does NOT enable the frequency lock that can be enabled on the user interface of the instrument)
Expand source code
def syncronise_waveforms(self): """Syncronise waveforms of the two channels when using the same frequency Note: Does NOT enable the frequency lock that can be enabled on the user interface of the instrument) """ self.write(":PHAS:INIT", custom_err_message="syncronise waveforms")
def write(self, command: str, custom_err_message: str = None) ‑> int
-
Write a VISA command to the instrument
Parameters
command
:str
- The VISA command to be written to the instrument
custom_err_message
:str
, defaultNone
- When
None
, the RuntimeError message is "Writing/querying command {command} failed: pyvisa returned StatusCode ..". Otherwise, if a message is supplied "Could not {message}: pyvisa returned StatusCode .."
Returns
bytes
:int
- Number of bytes tranferred
Raises
RuntimeError
- If status returned by PyVISA write command is not
pyvisa.constants.StatusCode.success
Expand source code
def write(self, command: str, custom_err_message: str = None) -> int: """Write a VISA command to the instrument Parameters ---------- command : str The VISA command to be written to the instrument custom_err_message : str, default `None` When `None`, the RuntimeError message is "Writing/querying command {command} failed: pyvisa returned StatusCode ..". Otherwise, if a message is supplied "Could not {message}: pyvisa returned StatusCode .." Returns ------- bytes : int Number of bytes tranferred Raises ------ RuntimeError If status returned by PyVISA write command is not `pyvisa.constants.StatusCode.success` """ num_bytes = self._inst.write(command) self._check_pyvisa_status(command, custom_err_message=custom_err_message) return num_bytes
class FuncGenChannel (fgen: FuncGen, channel: int, impedance: str)
-
Class for controlling a channel on a function generator object
Parameters
fgen
:FuncGen
- The function generator object
channel
:{1, 2}
- The channel to be controlled
impedance
:{"50ohm", "highZ"}
- Determines voltage limits associated with high impedance (whether the instrument is using 50ohm or high Z cannot be controlled through VISA)
Attributes
_fgen
:FuncGen
- The function generator object for which the channel exists
_channel
:{1, 2}
- The number of the channel this object is addressing
_source
:str
- "SOURce{i}:" where {i} is the channel number
Expand source code
class FuncGenChannel: """Class for controlling a channel on a function generator object Parameters ---------- fgen : `FuncGen` The function generator object channel : {1, 2} The channel to be controlled impedance : {"50ohm", "highZ"} Determines voltage limits associated with high impedance (whether the instrument is using 50ohm or high Z cannot be controlled through VISA) Attributes ---------- _fgen : `FuncGen` The function generator object for which the channel exists _channel : {1, 2} The number of the channel this object is addressing _source : str "SOURce{i}:" where {i} is the channel number """ _state_to_str = {"1": "ON", "0": "OFF", 1: "ON", 0: "OFF"} """Dictionary for converting output states to "ON" and "OFF" """ def __init__(self, fgen: FuncGen, channel: int, impedance: str): self._fgen = fgen self._channel = channel self._source = f"SOURce{channel}:" self.impedance = impedance """{"50ohm", "highZ"}: Determines voltage limits associated with high impedance (whether the instrument is using 50ohm or high Z cannot be controlled through VISA)""" # Adopt limits dictionary from instrument self.channel_limits = copy.deepcopy(self._fgen.instrument_limits) """Channel limits for the individual channel, same form as `FuncGen.instrument_limits`""" def _impedance_dependent_limit(self, limit_type: str) -> bool: """Check if the limit type is impedance dependent (voltages) or not (frequency) Returns ------- bool `True` if the limit is impedance dependent """ try: # to access the key "min" to check if impedance must be selected _ = self.channel_limits[limit_type][0]["min"] return False except KeyError: # if the key does not exist # The impedance must be selected return True def set_stricter_limits(self): """Set limits for the voltage and frequency limits of the channel output through a series of prompts""" print(f"Set stricter voltage and frequency limits for channel {self._channel}") print("Use enter only to leave a limit unchanged.") # Go through the different limits in the instrument_limits dict for limit_type, (inst_limit_dict, unit) in self._fgen.instrument_limits.items(): use_impedance = self._impedance_dependent_limit(limit_type) print(f"Set {limit_type} in {unit}", end=" ") if use_impedance: inst_limit_dict = inst_limit_dict[self.impedance] print(f"[{self.impedance} impedance limit]") else: print("") # get new line # Go through the min and max for the limit type for key, inst_value in inst_limit_dict.items(): # prompt for new value new_value = input(f" {key} (instrument limit {inst_value}{unit}): ") if new_value == "": # Do not change if empty print("\tLimit not changed") else: try: # to convert to float new_value = float(new_value) except ValueError: print( f"\tLimit unchanged: Could not convert '{new_value}' " f"to float" ) continue # to next item in dict # Set the new limit self.set_limit(limit_type, key, new_value, verbose=True) def set_limit( self, limit_type: str, bound: str, new_value: float, verbose: bool = False ) -> bool: """Set a limit if the new value is within the instrument limits and are self consistent (max larger than min) Parameterers ------------ limit_type : str The name of the limit in the channel_limits dictionary bound : {"min", "max"} Specifies if it is the max or the min limit that is to be set new_value : float The new value to be used for the limit verbose : bool Print confirmation that the limit was set or reason for why the limit was not set Returns ------- bool `True` if new limit set, `False` otherwise """ # Short hand references inst_limit_dict = self._fgen.instrument_limits[limit_type] channel_limit_dict = self.channel_limits[limit_type] # Find the instrument limit and unit use_impedance = self._impedance_dependent_limit(limit_type) if use_impedance: inst_value = inst_limit_dict[0][self.impedance][bound] else: inst_value = inst_limit_dict[0][bound] unit = inst_limit_dict[1] # Check that the new value is within the intrument limits acceptable_min = bound == "min" and new_value > inst_value if use_impedance: current_min = channel_limit_dict[0][self.impedance]["min"] else: current_min = channel_limit_dict[0]["min"] larger_than_min = new_value > current_min acceptable_max = bound == "max" and new_value < inst_value and larger_than_min if acceptable_min or acceptable_max: # within the limits # Set the new channel_limit, using the impedance depending on the # limit type. Beware that the shorthand cannot be used, as this # only changes the shorthand not the dictionary itself if use_impedance: self.channel_limits[limit_type][0][self.impedance][bound] = new_value else: self.channel_limits[limit_type][0][bound] = new_value if verbose: print(f"\tNew limit set {new_value}{unit}") return True if verbose: # print description of why the limit was not set if larger_than_min: reason = "larger" if bound == "max" else "smaller" print( f"\tNew limit NOT set: {new_value}{unit} is {reason} than " f"the instrument limit ({inst_value}{unit})" ) else: print( f"\tNew limit NOT set: {new_value}{unit} is smaller than the " f"current set minimum ({current_min}{unit})" ) return False # Get currently used parameters from function generator def get_output_state(self) -> int: """Returns 0 for "OFF", 1 for "ON" """ return int(self._fgen.query(f"OUTPut{self._channel}:STATe?")) def get_function(self) -> str: """Returns string of function name""" return self._fgen.query(f"{self._source}FUNCtion:SHAPe?") def get_amplitude(self) -> float: """Returns peak-to-peak voltage in volts""" return float(self._fgen.query(f"{self._source}VOLTage:AMPLitude?")) def get_offset(self) -> float: """Returns offset voltage in volts""" return float(self._fgen.query(f"{self._source}VOLTage:OFFSet?")) def get_frequency(self) -> float: """Returns frequency in Hertz""" return float(self._fgen.query(f"{self._source}FREQuency?")) # Get limits set in the channel class def get_frequency_lims(self) -> List[float]: """Returns list of min and max frequency limits""" return [self.channel_limits["frequency lims"][0][key] for key in ["min", "max"]] def get_voltage_lims(self) -> List[float]: """Returns list of min and max voltage limits for the current impedance""" return [ self.channel_limits["voltage lims"][0][self.impedance][key] for key in ["min", "max"] ] def get_amplitude_lims(self) -> List[float]: """Returns list of min and max amplitude limits for the current impedance""" return [ self.channel_limits["amplitude lims"][0][self.impedance][key] for key in ["min", "max"] ] def get_settings(self) -> dict: """Get the settings for the channel Returns ------- current_settings : dict Settings currently in use as a dictionary with keys output, function, amplitude, offset, and frequency and values tuples of the corresponding return and unit """ return { "output": (self._state_to_str[self.get_output_state()], ""), "function": (self.get_function(), ""), "amplitude": (self.get_amplitude(), "Vpp"), "offset": (self.get_offset(), "V"), "frequency": (self.get_frequency(), "Hz"), } def print_settings(self): """Print the settings currently in use for the channel (Recommended to use the `FuncGen.print_settings` for printing both channels) """ settings = self.get_settings() longest_key = max([len(key) for key in settings.keys()]) print("\nCurrent settings for channel {}".format(self._channel)) print("==============================") for key, (val, unit) in settings.items(): print("{:>{num_char}s} {} {}".format(key, val, unit, num_char=longest_key)) def set_settings(self, settings: dict): """Set the settings of the channel with a settings dictionary. Will set the outout to OFF before applyign the settings (and turn the channel ON or leave it OFF depending on the settings dict) Parameteres ----------- settings : dict Settings dictionary as returned by `get_settings`: should have keys output, function, amplitude, offset, and frequency """ # First turn off to ensure no potentially harmful # combination of settings self.set_output_state("OFF") # Set settings according to dictionary self.set_function(settings["function"][0]) self.set_amplitude(settings["amplitude"][0]) self.set_offset(settings["offset"][0]) self.set_frequency(settings["frequency"][0]) self.set_output_state(settings["output"][0]) def set_output_state(self, state: Union[int, str]): """Enables or diables the output of the channel Parameters ---------- state : int or str "ON" or int 1 to enable "OFF" or int 0 to disable Raises ------ NotSetError If `self._fgen.verify_param_set` is `True` and the value after applying the set function does not match the value returned by the get function """ err_msg = f"turn channel {self._channel} to state {state}" self._fgen.write( f"OUTPut{self._channel}:STATe {state}", custom_err_message=err_msg ) if self._fgen.verify_param_set: actual_state = self.get_output_state() if not actual_state == state: msg = ( f"Channel {self._channel} was not turned {state}, it is " f"{self._state_to_str[actual_state]}.\n" f"Error from the instrument: {self._fgen.get_error()}" ) raise NotSetError(msg) def get_output(self) -> int: """Wrapper for get_output_state""" return self.get_output_state() def set_output(self, state: Union[int, str]): """Wrapper for set_output_state""" self.set_output_state(state) def set_function(self, shape: str): """Set the function shape of the output Parameters ---------- shape : {SINusoid, SQUare, PULSe, RAMP, PRNoise, <Built_in>, USER[0], USER1, ..., USER255, EMEMory, EFILe} <Built_in>::={StairDown|StairUp|Stair Up&Dwn|Trapezoid|RoundHalf| AbsSine|AbsHalfSine|ClippedSine|ChoppedSine|NegRamp|OscRise| OscDecay|CodedPulse|PosPulse|NegPulse|ExpRise|ExpDecay|Sinc| Tan|Cotan|SquareRoot|X^2|HaverSine|Lorentz|Ln(x)|X^3|CauchyDistr| BesselJ|BesselY|ErrorFunc|Airy|Rectangle|Gauss|Hamming|Hanning| Bartlett|Blackman|Laylight|Triangle|DC|Heart|Round|Chirp|Rhombus| Cardiac} Raises ------ NotSetError If `self._fgen.verify_param_set` is `True` and the value after applying the set function does not match the value returned by the get function """ cmd = f"{self._source}FUNCtion:SHAPe {shape}" self._fgen.write(cmd, custom_err_message=f"set function {shape}") if self._fgen.verify_param_set: actual_shape = self.get_function() if not actual_shape == shape: msg = ( f"Function {shape} was not set on channel {self._channel}, " f"it is {actual_shape}. Check that the function name is " f"correctly spelt. Look up `set_function.__doc__` to see " f"available shapes.\n Error from the instrument: " f"{self._fgen.get_error()}" ) raise NotSetError(msg) def set_amplitude(self, amplitude: float): """Set the peak-to-peak amplitude in volts Parameters ---------- amplitude : float or {"max", "min"} 0.1mV or four digits resolution, "max" or "min" will set the amplitude to the maximum or minimum limit given in `channel_limits` Raises ------ NotSetError If `self._fgen.verify_param_set` is `True` and the value after applying the set function does not match the value returned by the get function """ # Check if keyword min or max is given if str(amplitude).lower() in ["min", "max"]: unit = "" # no unit for MIN/MAX # Look up what the limit is for this keyword amplitude = self.channel_limits["amplitude lims"][0][self.impedance][ str(amplitude).lower() ] else: unit = "Vpp" # Check if the given amplitude is within the current limits min_ampl, max_ampl = self.get_amplitude_lims() if amplitude < min_ampl or amplitude > max_ampl: msg = ( f"Could not set the amplitude {amplitude}{unit} as it " f"is not within the amplitude limits set for the instrument " f"[{min_ampl}, {max_ampl}]{unit}" ) raise NotSetError(msg) # Check that the new amplitude will not violate voltage limits min_volt, max_volt = self.get_voltage_lims() current_offset = self.get_offset() if ( amplitude / 2 - current_offset < min_volt or amplitude / 2 + current_offset > max_volt ): msg = ( f"Could not set the amplitude {amplitude}{unit} as the amplitude " f"combined with the offset ({current_offset}V) will be outside the " f"absolute voltage limits [{min_volt}, {max_volt}]{unit}" ) raise NotSetError(msg) # Set the amplitude cmd = f"{self._source}VOLTage:LEVel {amplitude}{unit}" err_msg = f"set amplitude {amplitude}{unit}" self._fgen.write(cmd, custom_err_message=err_msg) # Verify that the amplitude has been set if self._fgen.verify_param_set: actual_amplitude = self.get_amplitude() # Multiply with the appropriate factor according to SI prefix, or # if string is empty, use the value looked up from channel_limits earlier if not unit == "": check_amplitude = amplitude * _SI_prefix_to_factor(unit) else: check_amplitude = amplitude if not actual_amplitude == check_amplitude: msg = ( f"Amplitude {amplitude}{unit} was not set on channel " f"{self._channel}, it is {actual_amplitude}Vpp. Check that " f" the number is within the possible range and in the " f"correct format.\nError from the instrument: " f"{self._fgen.get_error()}" ) raise NotSetError(msg) def set_offset(self, offset: float, unit: str = "V"): """Set offset in volts (or mV, see options) Parameters ---------- offset : float Unknown resolution, guessing 0.1mV or four digits resolution unit : {mV, V}, default V Raises ------ NotSetError If `self._fgen.verify_param_set` is `True` and the value after applying the set function does not match the value returned by the get function """ # Check that the new offset will not violate voltage limits min_volt, max_volt = self.get_voltage_lims() current_amplitude = self.get_amplitude() offset = _SI_prefix_to_factor(unit) * offset if ( current_amplitude / 2 - offset < min_volt or current_amplitude / 2 + offset > max_volt ): msg = ( f"Could not set the offset {offset}V as the offset combined " f"with the amplitude ({current_amplitude}V) will be outside " f"the absolute voltage limits [{min_volt}, {max_volt}]V" ) raise NotSetError(msg) # Set the offset cmd = f"{self._source}VOLTage:LEVel:OFFSet {offset}{unit}" err_msg = f"set offset {offset}{unit}" self._fgen.write(cmd, custom_err_message=err_msg) # Verify that the offset has been set if self._fgen.verify_param_set: actual_offset = self.get_offset() # Multiply with the appropriate factor according to SI prefix check_offset = offset * _SI_prefix_to_factor(unit) if not actual_offset == check_offset: msg = ( f"Offset {offset}{unit} was not set on channel " f"{self._channel}, it is {actual_offset}V. Check that the " f"number is within the possible range and in the correct " f"format.\nError from the instrument: {self._fgen.get_error()}" ) raise NotSetError(msg) def set_frequency(self, freq: float, unit: str = "Hz"): """Set the frequency in Hertz (or mHz, kHz, MHz, see options) Parameters ---------- freq : float The resolution is 1 μHz or 12 digits. unit : {mHz, Hz, kHz, MHz}, default Hz Raises ------ NotSetError If `self._fgen.verify_param_set` is `True` and the value after applying the set function does not match the value returned by the get function """ if str(freq).lower() in ["min", "max"]: # handle min and max keywords unit = "" # no unit for MIN/MAX # Look up what the limit is for this keyword freq = self.channel_limits["frequency lims"][0][str(freq).lower()] else: # Check if the given frequency is within the current limits min_freq, max_freq = self.get_frequency_lims() freq = _SI_prefix_to_factor(unit) * freq if freq < min_freq or freq > max_freq: msg = ( f"Could not set the frequency {freq}Hz as it is not " f"within the frequency limits set for the instrument " f"[{min_freq}, {max_freq}]Hz" ) raise NotSetError(msg) # Set the frequency self._fgen.write( f"{self._source}FREQuency:FIXed {freq}{unit}", custom_err_message=f"set frequency {freq}{unit}", ) # Verify that the amplitude has been set if self._fgen.verify_param_set: actual_freq = self.get_frequency() # Multiply with the appropriate factor according to SI prefix, or # if string is empty, use the value looked up from channel_limits earlier if not unit == "": check_freq = freq * _SI_prefix_to_factor(unit) else: check_freq = freq if not actual_freq == check_freq: msg = ( f"Frequency {freq}{unit} was not set on channel {self._channel}" f", it is {actual_freq}Hz. Check that the number is within " f"the possible range and in the correct format.\nError " f"from the instrument: {self._fgen.get_error()}" ) raise NotSetError(msg)
Instance variables
var channel_limits
-
Channel limits for the individual channel, same form as
FuncGen.instrument_limits
var impedance
-
{"50ohm", "highZ"}: Determines voltage limits associated with high impedance (whether the instrument is using 50ohm or high Z cannot be controlled through VISA)
Methods
def get_amplitude(self) ‑> float
-
Returns peak-to-peak voltage in volts
Expand source code
def get_amplitude(self) -> float: """Returns peak-to-peak voltage in volts""" return float(self._fgen.query(f"{self._source}VOLTage:AMPLitude?"))
def get_amplitude_lims(self) ‑> List[float]
-
Returns list of min and max amplitude limits for the current impedance
Expand source code
def get_amplitude_lims(self) -> List[float]: """Returns list of min and max amplitude limits for the current impedance""" return [ self.channel_limits["amplitude lims"][0][self.impedance][key] for key in ["min", "max"] ]
def get_frequency(self) ‑> float
-
Returns frequency in Hertz
Expand source code
def get_frequency(self) -> float: """Returns frequency in Hertz""" return float(self._fgen.query(f"{self._source}FREQuency?"))
def get_frequency_lims(self) ‑> List[float]
-
Returns list of min and max frequency limits
Expand source code
def get_frequency_lims(self) -> List[float]: """Returns list of min and max frequency limits""" return [self.channel_limits["frequency lims"][0][key] for key in ["min", "max"]]
def get_function(self) ‑> str
-
Returns string of function name
Expand source code
def get_function(self) -> str: """Returns string of function name""" return self._fgen.query(f"{self._source}FUNCtion:SHAPe?")
def get_offset(self) ‑> float
-
Returns offset voltage in volts
Expand source code
def get_offset(self) -> float: """Returns offset voltage in volts""" return float(self._fgen.query(f"{self._source}VOLTage:OFFSet?"))
def get_output(self) ‑> int
-
Wrapper for get_output_state
Expand source code
def get_output(self) -> int: """Wrapper for get_output_state""" return self.get_output_state()
def get_output_state(self) ‑> int
-
Returns 0 for "OFF", 1 for "ON"
Expand source code
def get_output_state(self) -> int: """Returns 0 for "OFF", 1 for "ON" """ return int(self._fgen.query(f"OUTPut{self._channel}:STATe?"))
def get_settings(self) ‑> dict
-
Get the settings for the channel
Returns
current_settings
:dict
- Settings currently in use as a dictionary with keys output, function, amplitude, offset, and frequency and values tuples of the corresponding return and unit
Expand source code
def get_settings(self) -> dict: """Get the settings for the channel Returns ------- current_settings : dict Settings currently in use as a dictionary with keys output, function, amplitude, offset, and frequency and values tuples of the corresponding return and unit """ return { "output": (self._state_to_str[self.get_output_state()], ""), "function": (self.get_function(), ""), "amplitude": (self.get_amplitude(), "Vpp"), "offset": (self.get_offset(), "V"), "frequency": (self.get_frequency(), "Hz"), }
def get_voltage_lims(self) ‑> List[float]
-
Returns list of min and max voltage limits for the current impedance
Expand source code
def get_voltage_lims(self) -> List[float]: """Returns list of min and max voltage limits for the current impedance""" return [ self.channel_limits["voltage lims"][0][self.impedance][key] for key in ["min", "max"] ]
def print_settings(self)
-
Print the settings currently in use for the channel (Recommended to use the
FuncGen.print_settings()
for printing both channels)Expand source code
def print_settings(self): """Print the settings currently in use for the channel (Recommended to use the `FuncGen.print_settings` for printing both channels) """ settings = self.get_settings() longest_key = max([len(key) for key in settings.keys()]) print("\nCurrent settings for channel {}".format(self._channel)) print("==============================") for key, (val, unit) in settings.items(): print("{:>{num_char}s} {} {}".format(key, val, unit, num_char=longest_key))
def set_amplitude(self, amplitude: float)
-
Set the peak-to-peak amplitude in volts
Parameters
amplitude
:float
or{"max", "min"}
- 0.1mV or four digits resolution, "max" or "min" will set the
amplitude to the maximum or minimum limit given in
channel_limits
Raises
NotSetError
- If
self._fgen.verify_param_set
isTrue
and the value after applying the set function does not match the value returned by the get function
Expand source code
def set_amplitude(self, amplitude: float): """Set the peak-to-peak amplitude in volts Parameters ---------- amplitude : float or {"max", "min"} 0.1mV or four digits resolution, "max" or "min" will set the amplitude to the maximum or minimum limit given in `channel_limits` Raises ------ NotSetError If `self._fgen.verify_param_set` is `True` and the value after applying the set function does not match the value returned by the get function """ # Check if keyword min or max is given if str(amplitude).lower() in ["min", "max"]: unit = "" # no unit for MIN/MAX # Look up what the limit is for this keyword amplitude = self.channel_limits["amplitude lims"][0][self.impedance][ str(amplitude).lower() ] else: unit = "Vpp" # Check if the given amplitude is within the current limits min_ampl, max_ampl = self.get_amplitude_lims() if amplitude < min_ampl or amplitude > max_ampl: msg = ( f"Could not set the amplitude {amplitude}{unit} as it " f"is not within the amplitude limits set for the instrument " f"[{min_ampl}, {max_ampl}]{unit}" ) raise NotSetError(msg) # Check that the new amplitude will not violate voltage limits min_volt, max_volt = self.get_voltage_lims() current_offset = self.get_offset() if ( amplitude / 2 - current_offset < min_volt or amplitude / 2 + current_offset > max_volt ): msg = ( f"Could not set the amplitude {amplitude}{unit} as the amplitude " f"combined with the offset ({current_offset}V) will be outside the " f"absolute voltage limits [{min_volt}, {max_volt}]{unit}" ) raise NotSetError(msg) # Set the amplitude cmd = f"{self._source}VOLTage:LEVel {amplitude}{unit}" err_msg = f"set amplitude {amplitude}{unit}" self._fgen.write(cmd, custom_err_message=err_msg) # Verify that the amplitude has been set if self._fgen.verify_param_set: actual_amplitude = self.get_amplitude() # Multiply with the appropriate factor according to SI prefix, or # if string is empty, use the value looked up from channel_limits earlier if not unit == "": check_amplitude = amplitude * _SI_prefix_to_factor(unit) else: check_amplitude = amplitude if not actual_amplitude == check_amplitude: msg = ( f"Amplitude {amplitude}{unit} was not set on channel " f"{self._channel}, it is {actual_amplitude}Vpp. Check that " f" the number is within the possible range and in the " f"correct format.\nError from the instrument: " f"{self._fgen.get_error()}" ) raise NotSetError(msg)
def set_frequency(self, freq: float, unit: str = 'Hz')
-
Set the frequency in Hertz (or mHz, kHz, MHz, see options)
Parameters
freq
:float
- The resolution is 1 μHz or 12 digits.
unit
:{mHz, Hz, kHz, MHz}
, defaultHz
Raises
NotSetError
- If
self._fgen.verify_param_set
isTrue
and the value after applying the set function does not match the value returned by the get function
Expand source code
def set_frequency(self, freq: float, unit: str = "Hz"): """Set the frequency in Hertz (or mHz, kHz, MHz, see options) Parameters ---------- freq : float The resolution is 1 μHz or 12 digits. unit : {mHz, Hz, kHz, MHz}, default Hz Raises ------ NotSetError If `self._fgen.verify_param_set` is `True` and the value after applying the set function does not match the value returned by the get function """ if str(freq).lower() in ["min", "max"]: # handle min and max keywords unit = "" # no unit for MIN/MAX # Look up what the limit is for this keyword freq = self.channel_limits["frequency lims"][0][str(freq).lower()] else: # Check if the given frequency is within the current limits min_freq, max_freq = self.get_frequency_lims() freq = _SI_prefix_to_factor(unit) * freq if freq < min_freq or freq > max_freq: msg = ( f"Could not set the frequency {freq}Hz as it is not " f"within the frequency limits set for the instrument " f"[{min_freq}, {max_freq}]Hz" ) raise NotSetError(msg) # Set the frequency self._fgen.write( f"{self._source}FREQuency:FIXed {freq}{unit}", custom_err_message=f"set frequency {freq}{unit}", ) # Verify that the amplitude has been set if self._fgen.verify_param_set: actual_freq = self.get_frequency() # Multiply with the appropriate factor according to SI prefix, or # if string is empty, use the value looked up from channel_limits earlier if not unit == "": check_freq = freq * _SI_prefix_to_factor(unit) else: check_freq = freq if not actual_freq == check_freq: msg = ( f"Frequency {freq}{unit} was not set on channel {self._channel}" f", it is {actual_freq}Hz. Check that the number is within " f"the possible range and in the correct format.\nError " f"from the instrument: {self._fgen.get_error()}" ) raise NotSetError(msg)
def set_function(self, shape: str)
-
Set the function shape of the output
Parameters
shape
:{SINusoid, SQUare, PULSe, RAMP, PRNoise, <Built_in>, USER[0],
- USER1, …, USER255, EMEMory, EFILe}
::={StairDown|StairUp|Stair Up&Dwn|Trapezoid|RoundHalf| AbsSine|AbsHalfSine|ClippedSine|ChoppedSine|NegRamp|OscRise| OscDecay|CodedPulse|PosPulse|NegPulse|ExpRise|ExpDecay|Sinc| Tan|Cotan|SquareRoot|X^2|HaverSine|Lorentz|Ln(x)|X^3|CauchyDistr| BesselJ|BesselY|ErrorFunc|Airy|Rectangle|Gauss|Hamming|Hanning| Bartlett|Blackman|Laylight|Triangle|DC|Heart|Round|Chirp|Rhombus| Cardiac}
Raises
NotSetError
- If
self._fgen.verify_param_set
isTrue
and the value after applying the set function does not match the value returned by the get function
Expand source code
def set_function(self, shape: str): """Set the function shape of the output Parameters ---------- shape : {SINusoid, SQUare, PULSe, RAMP, PRNoise, <Built_in>, USER[0], USER1, ..., USER255, EMEMory, EFILe} <Built_in>::={StairDown|StairUp|Stair Up&Dwn|Trapezoid|RoundHalf| AbsSine|AbsHalfSine|ClippedSine|ChoppedSine|NegRamp|OscRise| OscDecay|CodedPulse|PosPulse|NegPulse|ExpRise|ExpDecay|Sinc| Tan|Cotan|SquareRoot|X^2|HaverSine|Lorentz|Ln(x)|X^3|CauchyDistr| BesselJ|BesselY|ErrorFunc|Airy|Rectangle|Gauss|Hamming|Hanning| Bartlett|Blackman|Laylight|Triangle|DC|Heart|Round|Chirp|Rhombus| Cardiac} Raises ------ NotSetError If `self._fgen.verify_param_set` is `True` and the value after applying the set function does not match the value returned by the get function """ cmd = f"{self._source}FUNCtion:SHAPe {shape}" self._fgen.write(cmd, custom_err_message=f"set function {shape}") if self._fgen.verify_param_set: actual_shape = self.get_function() if not actual_shape == shape: msg = ( f"Function {shape} was not set on channel {self._channel}, " f"it is {actual_shape}. Check that the function name is " f"correctly spelt. Look up `set_function.__doc__` to see " f"available shapes.\n Error from the instrument: " f"{self._fgen.get_error()}" ) raise NotSetError(msg)
def set_limit(self, limit_type: str, bound: str, new_value: float, verbose: bool = False) ‑> bool
-
Set a limit if the new value is within the instrument limits and are self consistent (max larger than min)
Parameterers
limit_type : str The name of the limit in the channel_limits dictionary bound : {"min", "max"} Specifies if it is the max or the min limit that is to be set new_value : float The new value to be used for the limit verbose : bool Print confirmation that the limit was set or reason for why the limit was not set
Returns
bool
True
if new limit set,False
otherwise
Expand source code
def set_limit( self, limit_type: str, bound: str, new_value: float, verbose: bool = False ) -> bool: """Set a limit if the new value is within the instrument limits and are self consistent (max larger than min) Parameterers ------------ limit_type : str The name of the limit in the channel_limits dictionary bound : {"min", "max"} Specifies if it is the max or the min limit that is to be set new_value : float The new value to be used for the limit verbose : bool Print confirmation that the limit was set or reason for why the limit was not set Returns ------- bool `True` if new limit set, `False` otherwise """ # Short hand references inst_limit_dict = self._fgen.instrument_limits[limit_type] channel_limit_dict = self.channel_limits[limit_type] # Find the instrument limit and unit use_impedance = self._impedance_dependent_limit(limit_type) if use_impedance: inst_value = inst_limit_dict[0][self.impedance][bound] else: inst_value = inst_limit_dict[0][bound] unit = inst_limit_dict[1] # Check that the new value is within the intrument limits acceptable_min = bound == "min" and new_value > inst_value if use_impedance: current_min = channel_limit_dict[0][self.impedance]["min"] else: current_min = channel_limit_dict[0]["min"] larger_than_min = new_value > current_min acceptable_max = bound == "max" and new_value < inst_value and larger_than_min if acceptable_min or acceptable_max: # within the limits # Set the new channel_limit, using the impedance depending on the # limit type. Beware that the shorthand cannot be used, as this # only changes the shorthand not the dictionary itself if use_impedance: self.channel_limits[limit_type][0][self.impedance][bound] = new_value else: self.channel_limits[limit_type][0][bound] = new_value if verbose: print(f"\tNew limit set {new_value}{unit}") return True if verbose: # print description of why the limit was not set if larger_than_min: reason = "larger" if bound == "max" else "smaller" print( f"\tNew limit NOT set: {new_value}{unit} is {reason} than " f"the instrument limit ({inst_value}{unit})" ) else: print( f"\tNew limit NOT set: {new_value}{unit} is smaller than the " f"current set minimum ({current_min}{unit})" ) return False
def set_offset(self, offset: float, unit: str = 'V')
-
Set offset in volts (or mV, see options)
Parameters
offset
:float
- Unknown resolution, guessing 0.1mV or four digits resolution
unit
:{mV, V}
, defaultV
Raises
NotSetError
- If
self._fgen.verify_param_set
isTrue
and the value after applying the set function does not match the value returned by the get function
Expand source code
def set_offset(self, offset: float, unit: str = "V"): """Set offset in volts (or mV, see options) Parameters ---------- offset : float Unknown resolution, guessing 0.1mV or four digits resolution unit : {mV, V}, default V Raises ------ NotSetError If `self._fgen.verify_param_set` is `True` and the value after applying the set function does not match the value returned by the get function """ # Check that the new offset will not violate voltage limits min_volt, max_volt = self.get_voltage_lims() current_amplitude = self.get_amplitude() offset = _SI_prefix_to_factor(unit) * offset if ( current_amplitude / 2 - offset < min_volt or current_amplitude / 2 + offset > max_volt ): msg = ( f"Could not set the offset {offset}V as the offset combined " f"with the amplitude ({current_amplitude}V) will be outside " f"the absolute voltage limits [{min_volt}, {max_volt}]V" ) raise NotSetError(msg) # Set the offset cmd = f"{self._source}VOLTage:LEVel:OFFSet {offset}{unit}" err_msg = f"set offset {offset}{unit}" self._fgen.write(cmd, custom_err_message=err_msg) # Verify that the offset has been set if self._fgen.verify_param_set: actual_offset = self.get_offset() # Multiply with the appropriate factor according to SI prefix check_offset = offset * _SI_prefix_to_factor(unit) if not actual_offset == check_offset: msg = ( f"Offset {offset}{unit} was not set on channel " f"{self._channel}, it is {actual_offset}V. Check that the " f"number is within the possible range and in the correct " f"format.\nError from the instrument: {self._fgen.get_error()}" ) raise NotSetError(msg)
def set_output(self, state: Union[int, str])
-
Wrapper for set_output_state
Expand source code
def set_output(self, state: Union[int, str]): """Wrapper for set_output_state""" self.set_output_state(state)
def set_output_state(self, state: Union[int, str])
-
Enables or diables the output of the channel
Parameters
state
:int
orstr
- "ON" or int 1 to enable "OFF" or int 0 to disable
Raises
NotSetError
- If
self._fgen.verify_param_set
isTrue
and the value after applying the set function does not match the value returned by the get function
Expand source code
def set_output_state(self, state: Union[int, str]): """Enables or diables the output of the channel Parameters ---------- state : int or str "ON" or int 1 to enable "OFF" or int 0 to disable Raises ------ NotSetError If `self._fgen.verify_param_set` is `True` and the value after applying the set function does not match the value returned by the get function """ err_msg = f"turn channel {self._channel} to state {state}" self._fgen.write( f"OUTPut{self._channel}:STATe {state}", custom_err_message=err_msg ) if self._fgen.verify_param_set: actual_state = self.get_output_state() if not actual_state == state: msg = ( f"Channel {self._channel} was not turned {state}, it is " f"{self._state_to_str[actual_state]}.\n" f"Error from the instrument: {self._fgen.get_error()}" ) raise NotSetError(msg)
def set_settings(self, settings: dict)
-
Set the settings of the channel with a settings dictionary. Will set the outout to OFF before applyign the settings (and turn the channel ON or leave it OFF depending on the settings dict)
Parameteres
settings : dict Settings dictionary as returned by
get_settings
: should have keys output, function, amplitude, offset, and frequencyExpand source code
def set_settings(self, settings: dict): """Set the settings of the channel with a settings dictionary. Will set the outout to OFF before applyign the settings (and turn the channel ON or leave it OFF depending on the settings dict) Parameteres ----------- settings : dict Settings dictionary as returned by `get_settings`: should have keys output, function, amplitude, offset, and frequency """ # First turn off to ensure no potentially harmful # combination of settings self.set_output_state("OFF") # Set settings according to dictionary self.set_function(settings["function"][0]) self.set_amplitude(settings["amplitude"][0]) self.set_offset(settings["offset"][0]) self.set_frequency(settings["frequency"][0]) self.set_output_state(settings["output"][0])
def set_stricter_limits(self)
-
Set limits for the voltage and frequency limits of the channel output through a series of prompts
Expand source code
def set_stricter_limits(self): """Set limits for the voltage and frequency limits of the channel output through a series of prompts""" print(f"Set stricter voltage and frequency limits for channel {self._channel}") print("Use enter only to leave a limit unchanged.") # Go through the different limits in the instrument_limits dict for limit_type, (inst_limit_dict, unit) in self._fgen.instrument_limits.items(): use_impedance = self._impedance_dependent_limit(limit_type) print(f"Set {limit_type} in {unit}", end=" ") if use_impedance: inst_limit_dict = inst_limit_dict[self.impedance] print(f"[{self.impedance} impedance limit]") else: print("") # get new line # Go through the min and max for the limit type for key, inst_value in inst_limit_dict.items(): # prompt for new value new_value = input(f" {key} (instrument limit {inst_value}{unit}): ") if new_value == "": # Do not change if empty print("\tLimit not changed") else: try: # to convert to float new_value = float(new_value) except ValueError: print( f"\tLimit unchanged: Could not convert '{new_value}' " f"to float" ) continue # to next item in dict # Set the new limit self.set_limit(limit_type, key, new_value, verbose=True)
class NotCompatibleError (*args, **kwargs)
-
Error for when the instrument is not compatible with this module
Expand source code
class NotCompatibleError(Exception): """Error for when the instrument is not compatible with this module"""
Ancestors
- builtins.Exception
- builtins.BaseException
class NotSetError (*args, **kwargs)
-
Error for when a value cannot be written to the instrument
Expand source code
class NotSetError(Exception): """Error for when a value cannot be written to the instrument"""
Ancestors
- builtins.Exception
- builtins.BaseException