Source code for qcodes.instrument_drivers.signal_hound.USB_SA124B

import ctypes as ct
import logging
from enum import IntEnum
from time import sleep
from typing import Any, Dict, Optional, Tuple

import numpy as np

import qcodes.validators as vals
from qcodes.instrument import Instrument
from qcodes.parameters import ArrayParameter, Parameter, ParameterWithSetpoints

log = logging.getLogger(__name__)

[docs]class TraceParameter(Parameter): """ A parameter that used a flag on the instrument to keeps track of if it's value has been synced to the instrument. It is intended that this type of parameter is synced using an external method which resets the flag. This is most likely used similar to a ``ManualParameter`` I.e. calling set/get will not communicate with the instrument. """ def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs)
[docs] def set_raw(self, value: Any) -> None: # pylint: disable=method-hidden if not isinstance(self.instrument, SignalHound_USB_SA124B): raise RuntimeError("TraceParameter only works with " "'SignalHound_USB_SA124B'") self.instrument._parameters_synced = False
[docs]class ExternalRefParameter(TraceParameter): """ Parameter that handles the fact that external reference can only be enabled but not disabled. From the manual: Once a device has successfully switched to an external reference it must remain using it until the device is closed, and it is undefined behavior to disconnect the reference input from the reference BNC port. """
[docs] def set_raw(self, value: bool) -> None: # pylint: disable=method-hidden if self.get_latest() is True and value is False: raise RuntimeError("Signal Hound does not support disabling " "external reference. To switch back to internal " "reference close the device and start again.") super().set_raw(value)
[docs]class ScaleParameter(TraceParameter): """ Parameter that handles changing the unit when the scale is changed. """
[docs] def set_raw(self, value: bool) -> None: # pylint: disable=method-hidden if not isinstance(self.instrument, SignalHound_USB_SA124B): raise RuntimeError("ScaleParameter only works with " "'SignalHound_USB_SA124B'") if value in ('log-scale', 'log-full-scale'): unit = 'dBm' elif value in ('lin-scale', 'lin-full-scale'): unit = 'mV' else: raise RuntimeError("Unsupported scale") self.instrument.trace.unit = unit self.instrument.power.unit = unit super().set_raw(value)
[docs]class SweepTraceParameter(TraceParameter): """ An extension to TraceParameter that keeps track of the trace setpoints in addition to the functionality of `TraceParameter` """ def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs)
[docs] def set_raw(self, value: Any) -> None: # pylint: disable=method-hidden if not isinstance(self.instrument, SignalHound_USB_SA124B): raise RuntimeError("SweepTraceParameter only works with " "'SignalHound_USB_SA124B'") self.instrument._trace_updated = False super().set_raw(value)
[docs]class FrequencySweep(ArrayParameter): """ Hardware controlled parameter class for SignalHound_USB_SA124B. Instrument returns an array of powers for different frequencies Args: name: parameter name instrument: instrument the parameter belongs to sweep_len: Number of steps in sweep start_freq: Starting frequency stepsize: Size of a frequency step Methods: get(): executes a sweep and returns magnitude and phase arrays """ def __init__(self, name: str, instrument: 'SignalHound_USB_SA124B', sweep_len: int, start_freq: float, stepsize: float) -> None: super().__init__(name, shape=(sweep_len,), instrument=instrument, unit='dBm', label='Magnitude', setpoint_units=('Hz',), setpoint_labels=(f'Frequency',), setpoint_names=(f'frequency',)) self.set_sweep(sweep_len, start_freq, stepsize)
[docs] def set_sweep(self, sweep_len: int, start_freq: float, stepsize: float) -> None: """ Set the setpoints of the Array parameter representing a frequency sweep. Args: sweep_len: Number of points in the sweep start_freq: Starting frequency of the sweep stepsize: Size of step between individual points """ if not isinstance(self.instrument, SignalHound_USB_SA124B): raise RuntimeError("'FrequencySweep' is only implemented" "for 'SignalHound_USB_SA124B'") end_freq = start_freq + stepsize*(sweep_len-1) freq_points = tuple(np.linspace(start_freq, end_freq, sweep_len)) self.setpoints = (freq_points,) self.shape = (sweep_len,) self.instrument._trace_updated = True
[docs] def get_raw(self) -> np.ndarray: if self.instrument is None: raise RuntimeError("No instrument is attached to" "'FrequencySweep'") if not isinstance(self.instrument, SignalHound_USB_SA124B): raise RuntimeError("'FrequencySweep' is only implemented" "for 'SignalHound_USB_SA124B'") if not self.instrument._trace_updated: raise RuntimeError('trace not updated, run configure to update') data = self.instrument._get_sweep_data() sleep(2*self.instrument.sleep_time.get()) return data
[docs]class SignalHound_USB_SA124B(Instrument): """ QCoDeS driver for the SignalHound USB SA124B The driver needs Signal Hounds software `Spike <>`_ installed to function. In addition you may need to install Microsoft Visual Studio C++ Redistributable for the driver to function within QCoDeS. At the time of writing the current version of Spike (3.2.3) uses `Microsoft Visual Studio C++ Redistributable 2012 <>`_ """ dll_path = 'C:\\Program Files\\Signal Hound\\Spike\\sa_api.dll' def __init__(self, name: str, dll_path: Optional[str] = None, **kwargs: Any): """ Args: name: Name of the instrument. dll_path: Path to ``sa_api.dll`` Defaults to the default dll within Spike installation **kwargs: """ super().__init__(name, **kwargs) self._parameters_synced = False self._trace_updated = False'Initializing instrument SignalHound USB 124B') self.dll = ct.CDLL(dll_path or self.dll_path) self._set_ctypes_argtypes() self.hf = Constants self.add_parameter('frequency', label='Frequency', unit='Hz', initial_value=5e9, vals=vals.Numbers(), parameter_class=SweepTraceParameter, docstring='Center frequency for sweep.' 'This is the set center, the actual ' 'center may be subject to round off ' 'compared to this value') self.add_parameter('span', label='Span', unit='Hz', initial_value=.25e6, vals=vals.Numbers(), parameter_class=SweepTraceParameter, docstring='Width of frequency span' 'This is the set span, the actual ' 'span may be subject to round off ' 'compared to this value' ) self.add_parameter('npts', label='Number of Points', get_cmd=self._get_npts, set_cmd=False, docstring='Number of points in frequency sweep.') self.add_parameter('avg', label='Averages', initial_value=1, get_cmd=None, set_cmd=None, vals=vals.Ints(), docstring='Number of averages to perform. ' 'Averages are performed in software by ' 'acquiring multiple sweeps') self.add_parameter('ref_lvl', label='Reference power', unit='dBm', initial_value=0, vals=vals.Numbers(max_value=20), parameter_class=TraceParameter, docstring="Setting reference level will " "automatically select gain and attenuation" "optimal for measuring at and below " "this level") self.add_parameter('external_reference', initial_value=False, vals=vals.Bool(), parameter_class=ExternalRefParameter, docstring='Use an external 10 MHz reference source. ' 'Note that Signal Hound does not support ' 'disabling external ref. To disable close ' 'the connection and restart.') self.add_parameter('device_type', set_cmd=False, get_cmd=self._get_device_type) self.add_parameter('device_mode', get_cmd=lambda: 'sweeping', set_cmd=False, docstring='The driver currently only ' 'supports sweeping mode. ' 'It is therefor not possible' 'to set this parameter to anything else') self.add_parameter('acquisition_mode', get_cmd=lambda: 'average', set_cmd=False, docstring="The driver only supports averaging " "mode it is therefor not possible to set" "this parameter to anything else") self.add_parameter('rbw', label='Resolution Bandwidth', unit='Hz', initial_value=1e3, vals=vals.Numbers(0.1, 250e3), parameter_class=TraceParameter, docstring='Resolution Bandwidth (RBW) is' 'the bandwidth of ' 'spectral energy represented in each ' 'frequency bin') self.add_parameter('vbw', label='Video Bandwidth', unit='Hz', initial_value=1e3, vals=vals.Numbers(), parameter_class=TraceParameter, docstring='The video bandwidth (VBW) is applied ' 'after the signal has been converted to ' 'frequency domain as power, voltage, ' 'or log units. It is implemented as a ' 'simple rectangular window, averaging the ' 'amplitude readings for each frequency ' 'bin over several overlapping FFTs. ' 'For best performance use RBW as the VBW.') self.add_parameter('reject_image', label='Reject image', unit='', initial_value=True, parameter_class=TraceParameter, get_cmd=None, docstring="Apply software filter to remove " "undersampling mirroring", vals=vals.Bool()) self.add_parameter('sleep_time', label='Sleep time', unit='s', initial_value=0.1, get_cmd=None, set_cmd=None, docstring="Time to sleep before and after " "getting data from the instrument", vals=vals.Numbers(0)) # We don't know the correct values of # the sweep parameters yet so we supply # some defaults. The correct will be set when we call configure below self.add_parameter(name='trace', sweep_len=1, start_freq=1, stepsize=1, parameter_class=FrequencySweep) self.add_parameter('power', label='Power', unit='dBm', get_cmd=self._get_power_at_freq, set_cmd=False, docstring="The maximum power in a window of 250 kHz" "around the specified frequency with " "Resolution bandwidth set to 1 kHz." "The integration window is specified by " "the VideoBandWidth (set by vbw)") # scale is defined after the trace and power parameter so that # it can change the units of those in it's set method when the # scale changes self.add_parameter('scale', initial_value='log-scale', vals=vals.Enum('log-scale', 'lin-scale', 'log-full-scale', 'lin-full-scale'), parameter_class=ScaleParameter) self.add_parameter('frequency_axis', label='Frequency', unit='Hz', get_cmd=self._get_freq_axis, set_cmd=False, vals=vals.Arrays(shape=(self.npts,)), snapshot_value=False ) self.add_parameter('freq_sweep', label='Power', unit='depends on mode', get_cmd=self._get_sweep_data, set_cmd=False, parameter_class=ParameterWithSetpoints, vals=vals.Arrays(shape=(self.npts,)), setpoints=(self.frequency_axis,), snapshot_value=False) self.openDevice() self.configure() self.connect_message() def _set_ctypes_argtypes(self) -> None: """ Set the expected argtypes for function calls in the sa_api dll These should match the function signatures defined in the sa-api header files included with the signal hound sdk """ self.dll.saConfigCenterSpan.argtypes = [ct.c_int, ct.c_double, ct.c_double] self.dll.saConfigAcquisition.argtypes = [ct.c_int, ct.c_int, ct.c_int] self.dll.saConfigLevel.argtypes = [ct.c_int, ct.c_double] self.dll.saSetTimebase.argtypes = [ct.c_int, ct.c_int] self.dll.saConfigSweepCoupling.argtypes = [ct.c_int, ct.c_double, ct.c_double, ct.c_bool] self.dll.saInitiate.argtypes = [ct.c_int, ct.c_int, ct.c_int] self.dll.saOpenDevice.argtypes = [ct.POINTER(ct.c_int)] self.dll.saCloseDevice.argtypes = [ct.c_int] self.dll.saPreset.argtypes = [ct.c_int] self.dll.saGetDeviceType.argtypes = [ct.c_int, ct.POINTER(ct.c_int)] self.dll.saQuerySweepInfo.argtypes = [ct.c_int, ct.POINTER(ct.c_int), ct.POINTER(ct.c_double), ct.POINTER(ct.c_double)] self.dll.saGetSweep_32f.argtypes = [ct.c_int, ct.POINTER(ct.c_float), ct.POINTER(ct.c_float)] self.dll.saGetSerialNumber.argtypes = [ct.c_int, ct.POINTER(ct.c_int)] self.dll.saGetFirmwareString.argtypes = [ct.c_int, ct.c_char_p] def _get_npts(self) -> int: if not self._parameters_synced: self.sync_parameters() sweep_info = self.QuerySweep() sweep_len = sweep_info[0] return sweep_len def _update_trace(self) -> None: """ Private method to sync changes of the frequency axis to the setpoints of the trace parameter. This also set the units of power and trace. """ sweep_info = self.QuerySweep() self.npts.cache.set(sweep_info[0]) self.trace.set_sweep(*sweep_info)
[docs] def sync_parameters(self) -> None: """ Sync parameters sets the configuration of the instrument using the parameters specified in the Qcodes instrument. Sync parameters consists of five parts 1. Center span configuration (freqs and span) 2. Acquisition configuration lin-scale/log-scale avg/max power 3. Configuring the external 10MHz reference 4. Configuration of the mode that is being used 5. Acquisition mode. At the moment only `sweeping` is implemented This does not currently implement Configuration of the tracking generator used in VNA mode """ # 1. CenterSpan Configuration center = ct.c_double(self.frequency()) span = ct.c_double(self.span())'Setting device CenterSpan configuration.') err = self.dll.saConfigCenterSpan(self.deviceHandle, center, span) self.check_for_error(err, 'saConfigCenterSpan') # 2. Acquisition configuration detectorVals = { 'min-max': ct.c_int(self.hf.sa_MIN_MAX), 'average': ct.c_int(self.hf.sa_AVERAGE) } scaleVals = { 'log-scale': ct.c_int(self.hf.sa_LOG_SCALE), 'lin-scale': ct.c_int(self.hf.sa_LIN_SCALE), 'log-full-scale': ct.c_int(self.hf.sa_LOG_FULL_SCALE), 'lin-full-scale': ct.c_int(self.hf.sa_LIN_FULL_SCALE) } detector = detectorVals[self.acquisition_mode()] scale = scaleVals[self.scale()] err = self.dll.saConfigAcquisition(self.deviceHandle, detector, scale) self.check_for_error(err, 'saConfigAcquisition') # 3. Reference Level configuration'Setting device reference level configuration.') err = self.dll.saConfigLevel( self.deviceHandle, ct.c_double(self.ref_lvl())) self.check_for_error(err, 'saConfigLevel') # 4. External Reference configuration if self.external_reference(): external = self.hf.sa_REF_EXTERNAL_IN'Setting reference frequency from external source.') err = self.dll.saSetTimebase(self.deviceHandle, external) self.check_for_error(err, 'saSetTimebase') reject_var = ct.c_bool(self.reject_image())'Setting device Sweeping configuration.') err = self.dll.saConfigSweepCoupling( self.deviceHandle, ct.c_double(self.rbw()), ct.c_double(self.vbw()), reject_var) self.check_for_error(err, 'saConfigSweepCoupling') modeOpts = { 'sweeping': self.hf.sa_SWEEPING, 'real_time': self.hf.sa_REAL_TIME, # not implemented 'IQ': self.hf.sa_IQ, # not implemented 'idle': self.hf.sa_IDLE } mode = modeOpts[self.device_mode()] # the third argument to saInitiate is a flag that is # currently not used err = self.dll.saInitiate(self.deviceHandle, mode, 0) extrainfo: Optional[str] = None if err == saStatus.saInvalidParameterErr: extrainfo = """ In real-time mode, this value may be returned if the span limits defined in the API header are broken. Also in real-time mode, this error will be returned if the resolution bandwidth is outside the limits defined in the API header. In time-gate analysis mode this error will be returned if span limits defined in the API header are broken. Also in time gate analysis, this error is returned if the bandwidth provided require more samples for processing than is allowed in the gate length. To fix this increase rbw/vbw. """ elif err == saStatus.saBandwidthErr: extrainfo = 'RBW is larger than your span. (Sweep Mode)!' self.check_for_error(err, 'saInitiate', extrainfo) self._parameters_synced = True
[docs] def configure(self) -> None: """ Syncs parameters to the Instrument and updates the setpoint of the trace. """ self.sync_parameters() self._update_trace()
[docs] def openDevice(self) -> None: """ Opens connection to the instrument """'Opening Device') self.deviceHandle = ct.c_int(0) deviceHandlePnt = ct.pointer(self.deviceHandle) err = self.dll.saOpenDevice(deviceHandlePnt) self.check_for_error(err, 'saOpenDevice') self.device_type()
[docs] def close(self) -> None: """ Close connection to the instrument. """'Closing Device with handle num: ' f'{self.deviceHandle.value}') try: self.abort()'Running acquistion aborted.') except Exception as e: # it's ok to catch any exception here # as we are tearing down the instrument we might # as well try to continue log.warning(f'Could not abort acquisition: {e}') err = self.dll.saCloseDevice(self.deviceHandle) self.check_for_error(err, 'saCloseDevice')'Closed Device with handle num: {self.deviceHandle.value}') super().close()
[docs] def abort(self) -> None: """ Abort any running acquisition. """'Stopping acquisition') err = self.dll.saAbort(self.deviceHandle) extrainfo: Optional[str] = None if err == saStatus.saDeviceNotConfiguredErr: extrainfo = 'Device was already idle! Did you call abort ' \ 'without ever calling initiate()' self.check_for_error(err, 'saAbort', extrainfo)
[docs] def preset(self) -> None: """ Like close but performs a hardware reset before closing the connection. """ log.warning('Performing hardware-reset of device!') err = self.dll.saPreset(self.deviceHandle) self.check_for_error(err, 'saPreset') super().close()
def _get_device_type(self) -> str: """ Returns the model string of the Spectrum Analyzer. """'Querying device for model information') devType = ct.c_int32(0) devTypePnt = ct.pointer(devType) err = self.dll.saGetDeviceType(self.deviceHandle, devTypePnt) self.check_for_error(err, 'saGetDeviceType') if devType.value == self.hf.saDeviceTypeNone: dev = 'No device' elif devType.value == self.hf.saDeviceTypeSA44: dev = 'sa44' elif devType.value == self.hf.saDeviceTypeSA44B: dev = 'sa44B' elif devType.value == self.hf.saDeviceTypeSA124A: dev = 'sa124A' elif devType.value == self.hf.saDeviceTypeSA124B: dev = 'sa124B' else: raise ValueError('Unknown device type!') return dev ########################################################################
[docs] def QuerySweep(self) -> Tuple[int, float, float]: """ Queries the sweep for information on the parameters that defines the x axis of the sweep Returns: number of points in sweep, start frequency and step size """ sweep_len = ct.c_int(0) start_freq = ct.c_double(0) stepsize = ct.c_double(0) err = self.dll.saQuerySweepInfo(self.deviceHandle, ct.pointer(sweep_len), ct.pointer(start_freq), ct.pointer(stepsize)) self.check_for_error(err, 'saQuerySweepInfo') return sweep_len.value, start_freq.value, stepsize.value
def _get_sweep_data(self) -> np.ndarray: """ This function performs a sweep over the configured ranges. The result of the sweep is returned along with the sweep points returns: datamin numpy array """ if not self._parameters_synced: self.sync_parameters() sweep_len, _, _ = self.QuerySweep() data = np.zeros(sweep_len) Navg = self.avg() for i in range(Navg): datamin = np.zeros((sweep_len), dtype=np.float32) datamax = np.zeros((sweep_len), dtype=np.float32) minarr = datamin.ctypes.data_as(ct.POINTER(ct.c_float)) maxarr = datamax.ctypes.data_as(ct.POINTER(ct.c_float)) sleep(self.sleep_time.get()) # Added extra sleep for updating issue err = self.dll.saGetSweep_32f(self.deviceHandle, minarr, maxarr) self.check_for_error(err, 'saGetSweep_32f') data += datamin return data / Navg def _get_power_at_freq(self) -> float: """ Returns the maximum power in a window of 250 kHz around the specified frequency with Resolution bandwidth set to 1 kHz. The integration window is specified by the VideoBandWidth (set by vbw) """ original_span = self.span() original_rbw = self.rbw() needs_reset = False if not (original_span == 0.25e6 and original_rbw == 1e3): needs_reset = True self.span(0.25e6) self.rbw(1e3) if not self._parameters_synced: # call configure to update both # the parameters on the device and the # setpoints and units self.configure() data = self._get_sweep_data() max_power = np.max(data) if needs_reset: self.span(original_span) self.rbw(original_rbw) self.configure() sleep(2*self.sleep_time.get()) return max_power
[docs] @staticmethod def check_for_error(err: int, source: str, extrainfo: Optional[str] = None) -> None: if err != saStatus.saNoError: err_str = saStatus(err).name if err > 0: msg = (f'During call of {source} the following' f'Warning: {err_str} was raised') if extrainfo is not None: msg = msg + f'\n Extra info: {extrainfo}' log.warning(msg) else: msg = (f'During call of {source} the following Error: ' f'{err_str} was raised') if extrainfo is not None: msg = msg + f'\n Extra info: {extrainfo}' raise OSError(msg) else: msg = 'Call to {source} was successful' if extrainfo is not None: msg = msg + f'\n Extra info: {extrainfo}'
[docs] def get_idn(self) -> Dict[str, Optional[str]]: output: Dict[str, Optional[str]] = {} output['vendor'] = 'Signal Hound' output['model'] = self._get_device_type() serialnumber = ct.c_int32() err = self.dll.saGetSerialNumber(self.deviceHandle, ct.pointer(serialnumber)) self.check_for_error(err, 'saGetSerialNumber') output['serial'] = str(serialnumber.value) fw_version = (ct.c_char*17)() # the manual says that this must be at least 16 char # but not clear if that includes a termination zero so # make it 17 just in case err = self.dll.saGetFirmwareString(self.deviceHandle, fw_version) self.check_for_error(err, 'saGetFirmwareString') output['firmware'] = fw_version.value.decode('ascii') return output
def _get_freq_axis(self) -> np.ndarray: if not self._parameters_synced: self.sync_parameters() sweep_len, start_freq, stepsize = self.QuerySweep() end_freq = start_freq + stepsize*(sweep_len-1) freq_points = np.linspace(start_freq, end_freq, sweep_len) return freq_points
[docs]class Constants: """ These constants are defined in sa_api.h as part of the the Signal Hound SDK """ SA_MAX_DEVICES = 8 saDeviceTypeNone = 0 saDeviceTypeSA44 = 1 saDeviceTypeSA44B = 2 saDeviceTypeSA124A = 3 saDeviceTypeSA124B = 4 sa44_MIN_FREQ = 1.0 sa124_MIN_FREQ = 100.0e3 sa44_MAX_FREQ = 4.4e9 sa124_MAX_FREQ = 13.0e9 sa_MIN_SPAN = 1.0 sa_MAX_REF = 20 sa_MAX_ATTEN = 3 sa_MAX_GAIN = 2 sa_MIN_RBW = 0.1 sa_MAX_RBW = 6.0e6 sa_MIN_RT_RBW = 100.0 sa_MAX_RT_RBW = 10000.0 sa_MIN_IQ_BANDWIDTH = 100.0 sa_MAX_IQ_DECIMATION = 128 sa_IQ_SAMPLE_RATE = 486111.111 sa_IDLE = -1 sa_SWEEPING = 0x0 sa_REAL_TIME = 0x1 sa_IQ = 0x2 sa_AUDIO = 0x3 sa_TG_SWEEP = 0x4 sa_MIN_MAX = 0x0 sa_AVERAGE = 0x1 sa_LOG_SCALE = 0x0 sa_LIN_SCALE = 0x1 sa_LOG_FULL_SCALE = 0x2 sa_LIN_FULL_SCALE = 0x3 sa_AUTO_ATTEN = -1 sa_AUTO_GAIN = -1 sa_LOG_UNITS = 0x0 sa_VOLT_UNITS = 0x1 sa_POWER_UNITS = 0x2 sa_BYPASS = 0x3 sa_AUDIO_AM = 0x0 sa_AUDIO_FM = 0x1 sa_AUDIO_USB = 0x2 sa_AUDIO_LSB = 0x3 sa_AUDIO_CW = 0x4 TG_THRU_0DB = 0x1 TG_THRU_20DB = 0x2 sa_REF_UNUSED = 0 sa_REF_INTERNAL_OUT = 1 sa_REF_EXTERNAL_IN = 2
[docs]class saStatus(IntEnum): saUnknownErr = -666 saFrequencyRangeErr = 99 saInvalidDetectorErr = -95 saInvalidScaleErr = -94 saBandwidthErr = -91 saExternalReferenceNotFound = -89 # Device specific errors saOvenColdErr = -20 # Data errors saInternetErr = -12 saUSBCommErr = -11 # General configuration errors saTrackingGeneratorNotFound = -10 saDeviceNotIdleErr = -9 saDeviceNotFoundErr = -8 saInvalidModeErr = -7 saNotConfiguredErr = -6 saDeviceNotConfiguredErr = -6 # Added because key error raised saTooManyDevicesErr = -5 saInvalidParameterErr = -4 saDeviceNotOpenErr = -3 saInvalidDeviceErr = -2 saNullPtrErr = -1 # No error saNoError = 0 # Warnings saNoCorrections = 1 saCompressionWarning = 2 saParameterClamped = 3 saBandwidthClamped = 4