diff --git a/qcodes_contrib_drivers/drivers/Keysight/SD_common/SD_AWG.py b/qcodes_contrib_drivers/drivers/Keysight/SD_common/SD_AWG.py index 479103c28..c24f53873 100644 --- a/qcodes_contrib_drivers/drivers/Keysight/SD_common/SD_AWG.py +++ b/qcodes_contrib_drivers/drivers/Keysight/SD_common/SD_AWG.py @@ -1,11 +1,15 @@ import logging from functools import partial from threading import RLock -from typing import List, Union, Optional, Any +from typing import List, Union, Optional, Dict from qcodes import validators as validator from .SD_Module import SD_Module, result_parser, keysightSD1, is_sd1_3x -from keysightSD1 import SD_Wave +from keysightSD1 import ( + SD_Wave, SD_Waveshapes, SD_TriggerExternalSources, SD_FpgaTriggerDirection, + SD_TriggerPolarity, SD_SyncModes, SD_DigitalFilterModes + ) + class SD_AWG(SD_Module): """ @@ -39,6 +43,7 @@ def __init__(self, name: str, chassis: int, slot: int, channels: int, module_class=keysightSD1.SD_AOU, **kwargs) self.awg = self.SD_module + self.legacy_channel_numbering = legacy_channel_numbering # Lock to avoid concurrent access of waveformLoad()/waveformReLoad() self._lock = RLock() @@ -127,6 +132,13 @@ def __init__(self, name: str, chassis: int, slot: int, channels: int, f'channel {ch}', vals=validator.Enum(-1, 0, 1, 2, 4, 5, 6, 8)) + # initialize settings cache + cache: Dict[str,Dict[int,Optional[float]]] = dict() + for setting in ['offset', 'amplitude']: + cache[setting] = {(i+index_offset):None for i in range(4)} + + self._settings_cache = cache + # # Get-commands # @@ -188,7 +200,7 @@ def set_clock_frequency(self, frequency: float, return result_parser(set_frequency, value_name, verbose) def set_channel_frequency(self, frequency: int, channel_number: int, - verbose: bool = False) -> Any: + verbose: bool = False) -> None: """ Sets the frequency for the specified channel. The frequency is used for the periodic signals generated by the @@ -199,12 +211,13 @@ def set_channel_frequency(self, frequency: int, channel_number: int, frequency: frequency in Hz verbose: boolean indicating verbose mode """ - value = self.awg.channelFrequency(channel_number, frequency) + with self._lock: + value = self.awg.channelFrequency(channel_number, frequency) value_name = f'set frequency channel {channel_number} to {frequency} Hz' - return result_parser(value, value_name, verbose) + result_parser(value, value_name, verbose) def set_channel_phase(self, phase: int, channel_number: int, - verbose: bool = False) -> Any: + verbose: bool = False) -> None: """ Sets the phase for the specified channel. @@ -213,12 +226,13 @@ def set_channel_phase(self, phase: int, channel_number: int, phase: phase in degrees verbose: boolean indicating verbose mode """ - value = self.awg.channelPhase(channel_number, phase) + with self._lock: + value = self.awg.channelPhase(channel_number, phase) value_name = f'set phase channel {channel_number} to {phase} degrees' - return result_parser(value, value_name, verbose) + result_parser(value, value_name, verbose) - def set_channel_amplitude(self, amplitude: int, channel_number: int, - verbose: bool = False) -> Any: + def set_channel_amplitude(self, amplitude: float, channel_number: int, + verbose: bool = False) -> None: """ Sets the amplitude for the specified channel. @@ -227,12 +241,18 @@ def set_channel_amplitude(self, amplitude: int, channel_number: int, amplitude: amplitude in Volts verbose: boolean indicating verbose mode """ - value = self.awg.channelAmplitude(channel_number, amplitude) + if self._settings_cache['amplitude'][channel_number] == amplitude: + return + + with self._lock: + value = self.awg.channelAmplitude(channel_number, amplitude) value_name = f'set amplitude channel {channel_number} to {amplitude} V' - return result_parser(value, value_name, verbose) + result_parser(value, value_name, verbose) + self._settings_cache['amplitude'][channel_number] = amplitude - def set_channel_offset(self, offset: int, channel_number: int, - verbose: bool = False) -> Any: + + def set_channel_offset(self, offset: float, channel_number: int, + verbose: bool = False) -> None: """ Sets the DC offset for the specified channel. @@ -241,12 +261,16 @@ def set_channel_offset(self, offset: int, channel_number: int, offset: DC offset in Volts verbose: boolean indicating verbose mode """ - value = self.awg.channelOffset(channel_number, offset) + if self._settings_cache['offset'][channel_number] == offset: + return + with self._lock: + value = self.awg.channelOffset(channel_number, offset) value_name = f'set offset channel {channel_number} to {offset} V' - return result_parser(value, value_name, verbose) + result_parser(value, value_name, verbose) + self._settings_cache['offset'][channel_number] = offset def set_channel_wave_shape(self, wave_shape: int, channel_number: int, - verbose: bool = False) -> Any: + verbose: bool = False) -> None: """ Sets output waveform type for the specified channel. HiZ : -1 (only available for M3202A) @@ -263,15 +287,24 @@ def set_channel_wave_shape(self, wave_shape: int, channel_number: int, wave_shape: wave shape type verbose: boolean indicating verbose mode """ - value = self.awg.channelWaveShape(channel_number, wave_shape) + with self._lock: + value = self.awg.channelWaveShape(channel_number, wave_shape) value_name = f'set wave shape channel {channel_number} to {wave_shape}' - return result_parser(value, value_name, verbose) + result_parser(value, value_name, verbose) + + def set_digital_filter_mode(self, filter_mode: SD_DigitalFilterModes) -> None: + """ + Sets digital filter mode. + + Args: + filter_mode: AOU_FILTER_OFF (0), AOU_FILTER_FLATNESS (1), AOU_FILTER_FIFTEEN_TAP (3) - def set_digital_filter_mode(self, filter_mode) -> None: + Note: AOU_FILTER_FIFTEEN_TAP is anti-ringing filter + """ result_parser(self.awg.setDigitalFilterMode(filter_mode), f'filter_mode({filter_mode})') - def set_trigger_io(self, value: int, verbose: bool = False) -> Any: + def set_trigger_io(self, value: int, verbose: bool = False) -> None: """ Sets the trigger output. The trigger must be configured as output using config_trigger_io @@ -282,26 +315,27 @@ def set_trigger_io(self, value: int, verbose: bool = False) -> Any: """ result = self.awg.triggerIOwrite(value) value_name = f'set io trigger output to {value}' - return result_parser(result, value_name, verbose) + result_parser(result, value_name, verbose) # # The methods below are useful for controlling the device, but are not # used for setting or getting parameters # - def off(self) -> None: """ Stops the AWGs and sets the waveform of all channels to 'No Signal' """ + index_offset = 0 if self.legacy_channel_numbering else 1 for i in range(self.channels): - awg_response = self.awg.AWGstop(i) - result_parser(awg_response, f'AWGstop({i})') - channel_response = self.awg.channelWaveShape(i, 0) - result_parser(channel_response, f'channelWaveShape({i}, 0)') + ch = i + index_offset + awg_response = self.awg.AWGstop(ch) + result_parser(awg_response, f'AWGstop({ch})') + channel_response = self.awg.channelWaveShape(ch, SD_Waveshapes.AOU_OFF) + result_parser(channel_response, f'channelWaveShape({ch}, SD_Waveshapes.AOU_OFF)') def reset_clock_phase(self, trigger_behaviour: int, trigger_source: int, - skew: float = 0.0, verbose: bool = False) -> Any: + skew: float = 0.0, verbose: bool = False) -> None: """ Sets the module in a sync state, waiting for the first trigger to reset the phase of the internal clocks CLKsync and CLKsys @@ -325,10 +359,10 @@ def reset_clock_phase(self, trigger_behaviour: int, trigger_source: int, value_name = f'reset_clock_phase trigger_behaviour:' \ f' {trigger_behaviour}, trigger_source:' \ f' {trigger_source}, skew: {skew}' - return result_parser(value, value_name, verbose) + result_parser(value, value_name, verbose) def reset_channel_phase(self, channel_number: int, - verbose: bool = False) -> Any: + verbose: bool = False) -> None: """ Resets the accumulated phase of the selected channel. This accumulated phase is the result of the phase continuous operation of @@ -340,10 +374,10 @@ def reset_channel_phase(self, channel_number: int, """ value = self.awg.channelPhaseReset(channel_number) value_name = f'reset phase of channel {channel_number}' - return result_parser(value, value_name, verbose) + result_parser(value, value_name, verbose) def reset_multiple_channel_phase(self, channel_mask: int, - verbose: bool = False) -> Any: + verbose: bool = False) -> None: """ Resets the accumulated phase of the selected channels simultaneously. @@ -358,11 +392,11 @@ def reset_multiple_channel_phase(self, channel_mask: int, """ value = self.awg.channelPhaseResetMultiple(channel_mask) value_name = f'reset phase with channel mask {channel_mask}' - return result_parser(value, value_name, verbose) + result_parser(value, value_name, verbose) def config_angle_modulation(self, channel_number: int, modulation_type: int, deviation_gain: int, - verbose: bool = False) -> Any: + verbose: bool = False) -> None: """ Configures the modulation in frequency/phase for the selected channel @@ -380,11 +414,11 @@ def config_angle_modulation(self, channel_number: int, value_name = f'configure angle modulation of' \ f' channel {channel_number} modulation_type: ' \ f'{modulation_type}, deviation_gain: {deviation_gain}' - return result_parser(value, value_name, verbose) + result_parser(value, value_name, verbose) def config_amplitude_modulation(self, channel_number: int, modulation_type: int, deviation_gain: int, - verbose: bool = False) -> Any: + verbose: bool = False) -> None: """ Configures the modulation in amplitude/offset for the selected channel @@ -403,10 +437,10 @@ def config_amplitude_modulation(self, channel_number: int, value_name = f'configure amplitude modulation of channel' \ f' {channel_number} modulation_type: {modulation_type}, ' \ f'deviation_gain: {deviation_gain}' - return result_parser(value, value_name, verbose) + result_parser(value, value_name, verbose) def set_iq_modulation(self, channel_number: int, enable: int, - verbose: bool = False) -> Any: + verbose: bool = False) -> None: """ Sets the IQ modulation for the selected channel @@ -419,9 +453,9 @@ def set_iq_modulation(self, channel_number: int, enable: int, status = 'Enabled (1)' if enable == 1 else 'Disabled (0)' value_name = f'set IQ modulation for channel {channel_number} to ' \ f'{status}' - return result_parser(value, value_name, verbose) + result_parser(value, value_name, verbose) - def config_clock_io(self, clock_config: int, verbose: bool = False) -> Any: + def config_clock_io(self, clock_config: int, verbose: bool = False) -> None: """ Configures the operation of the clock output connector (CLK) @@ -435,10 +469,10 @@ def config_clock_io(self, clock_config: int, verbose: bool = False) -> Any: value = self.awg.clockIOconfig(clock_config) status = 'CLKref Output (1)' if clock_config == 1 else 'Disabled (0)' value_name = f'configure clock output connector to {status}' - return result_parser(value, value_name, verbose) + result_parser(value, value_name, verbose) def config_trigger_io(self, direction: int, sync_mode: int, - verbose: bool = False) -> Any: + verbose: bool = False) -> None: """ Configures the trigger connector/line direction and synchronization/sampling method @@ -455,7 +489,7 @@ def config_trigger_io(self, direction: int, sync_mode: int, status = 'input (1)' if direction == 1 else 'output (0)' value_name = f'configure trigger io port to direction: {status}, ' \ f'sync_mode: {sync_mode}' - return result_parser(value, value_name, verbose) + result_parser(value, value_name, verbose) # # Waveform related functions @@ -572,7 +606,7 @@ def reload_waveform_int16(self, waveform_type: int, data_raw: List[int], value_name = f'reload_waveform_int16({waveform_number})' return result_parser(value, value_name, verbose) - def flush_waveform(self, verbose: bool = False) -> Any: + def flush_waveform(self, verbose: bool = False) -> None: """ Deletes all waveforms from the module onboard RAM and flushes all the AWG queues. @@ -581,7 +615,7 @@ def flush_waveform(self, verbose: bool = False) -> Any: with self._lock: value = self.awg.waveformFlush() value_name = 'flushed AWG queue and RAM' - return result_parser(value, value_name, verbose) + result_parser(value, value_name, verbose) # # AWG related functions @@ -675,9 +709,10 @@ def awg_queue_waveform(self, awg_number: int, waveform_number: int, Queues the specified waveform in one of the AWGs of the module. The waveform must be already loaded in the module onboard RAM. """ - result = self.awg.AWGqueueWaveform(awg_number, waveform_number, - trigger_mode, start_delay, cycles, - prescaler) + with self._lock: + result = self.awg.AWGqueueWaveform(awg_number, waveform_number, + trigger_mode, start_delay, cycles, + prescaler) result_parser(result, f'AWGqueueWaveform({awg_number}, {waveform_number})') @@ -690,7 +725,8 @@ def awg_queue_config(self, awg_number: int, mode: int) -> None: awg_number: awg number where the waveform is queued mode: operation mode of the queue: One Shot (0), Cyclic (1) """ - result = self.awg.AWGqueueConfig(awg_number, mode) + with self._lock: + result = self.awg.AWGqueueConfig(awg_number, mode) result_parser(result, f'AWGqueueConfig({awg_number}, {mode})') def awg_flush(self, awg_number: int) -> None: @@ -698,7 +734,8 @@ def awg_flush(self, awg_number: int) -> None: Empties the queue of the selected AWG. Waveforms are not removed from the onboard RAM. """ - result = self.awg.AWGflush(awg_number) + with self._lock: + result = self.awg.AWGflush(awg_number) result_parser(result, f'AWGflush({awg_number})') def awg_start(self, awg_number: int) -> None: @@ -708,7 +745,8 @@ def awg_start(self, awg_number: int) -> None: depending on the trigger selection of the first waveform in the queue and provided that at least one waveform is queued in the AWG. """ - result = self.awg.AWGstart(awg_number) + with self._lock: + result = self.awg.AWGstart(awg_number) result_parser(result, f'AWGstart({awg_number})') def awg_start_multiple(self, awg_mask: int) -> None: @@ -722,7 +760,8 @@ def awg_start_multiple(self, awg_mask: int) -> None: awg_mask: Mask to select the awgs to start (LSB is awg 0, bit 1 is awg 1 etc.) """ - result = self.awg.AWGstartMultiple(awg_mask) + with self._lock: + result = self.awg.AWGstartMultiple(awg_mask) result_parser(result, f'AWGstartMultiple({awg_mask})') def awg_pause(self, awg_number: int) -> None: @@ -731,7 +770,8 @@ def awg_pause(self, awg_number: int) -> None: and ignoring all incoming triggers. The waveform generation can be resumed calling awg_resume """ - result = self.awg.AWGpause(awg_number) + with self._lock: + result = self.awg.AWGpause(awg_number) result_parser(result, f'AWGpause({awg_number})') def awg_pause_multiple(self, awg_mask: int) -> None: @@ -744,14 +784,16 @@ def awg_pause_multiple(self, awg_mask: int) -> None: awg_mask: Mask to select the awgs to pause (LSB is awg 0, bit 1 is awg 1 etc.) """ - result = self.awg.AWGpauseMultiple(awg_mask) + with self._lock: + result = self.awg.AWGpauseMultiple(awg_mask) result_parser(result, f'AWGpauseMultiple({awg_mask})') def awg_resume(self, awg_number: int) -> None: """ Resumes the selected AWG, from the current position of the queue. """ - result = self.awg.AWGresume(awg_number) + with self._lock: + result = self.awg.AWGresume(awg_number) result_parser(result, f'AWGresume({awg_number})') def awg_resume_multiple(self, awg_mask: int) -> None: @@ -763,7 +805,8 @@ def awg_resume_multiple(self, awg_mask: int) -> None: awg_mask: Mask to select the awgs to resume (LSB is awg 0, bit 1 is awg 1 etc.) """ - result = self.awg.AWGresumeMultiple(awg_mask) + with self._lock: + result = self.awg.AWGresumeMultiple(awg_mask) result_parser(result, f'AWGresumeMultiple({awg_mask})') def awg_stop(self, awg_number: int) -> None: @@ -772,7 +815,8 @@ def awg_stop(self, awg_number: int) -> None: AWG queue to its initial position. All following incoming triggers are ignored. """ - result = self.awg.AWGstop(awg_number) + with self._lock: + result = self.awg.AWGstop(awg_number) result_parser(result, f'AWGstop({awg_number})') def awg_stop_multiple(self, awg_mask: int) -> None: @@ -785,7 +829,8 @@ def awg_stop_multiple(self, awg_mask: int) -> None: awg_mask: Mask to select the awgs to stop (LSB is awg 0, bit 1 is awg 1 etc.) """ - result = self.awg.AWGstopMultiple(awg_mask) + with self._lock: + result = self.awg.AWGstopMultiple(awg_mask) result_parser(result, f'AWGstopMultiple({awg_mask})') def awg_jump_next_waveform(self, awg_number: int) -> None: @@ -794,7 +839,8 @@ def awg_jump_next_waveform(self, awg_number: int) -> None: The jump is executed once the current waveform has finished a complete cycle. """ - result = self.awg.AWGjumpNextWaveform(awg_number) + with self._lock: + result = self.awg.AWGjumpNextWaveform(awg_number) result_parser(result, f'AWGjumpNextWaveform({awg_number})') def awg_config_external_trigger(self, awg_number: int, external_source: int, @@ -845,7 +891,8 @@ def awg_is_running(self, channel: int) -> bool: """ Returns True if awg on `channel` is running. """ - return self.awg.AWGisRunning(channel) + with self._lock: + return self.awg.AWGisRunning(channel) # # Functions related to creation of SD_Wave objects @@ -917,30 +964,52 @@ def new_waveform_from_int(waveform_type: int, return wave @staticmethod - def get_waveform_status(waveform: SD_Wave, verbose: bool = False) -> Any: + def get_waveform_status(waveform: SD_Wave, verbose: bool = False) -> int: value = waveform.getStatus() value_name = 'waveform_status' return result_parser(value, value_name, verbose) @staticmethod - def get_waveform_type(waveform: SD_Wave, verbose: bool = False) -> Any: + def get_waveform_type(waveform: SD_Wave, verbose: bool = False) -> int: value = waveform.getType() value_name = 'waveform_type' return result_parser(value, value_name, verbose) def load_fpga_image(self, filename: str) -> None: + """ + Loads specified image file in FPGA. + + Args: + filename: name of image file to load + """ with self._lock: logging.info(f'loading fpga image "{filename}" ...') super().load_fpga_image(filename) logging.info(f'loaded fpga image.') - def write_fpga(self, reg_name, value): + def write_fpga(self, reg_name:str, value:int) -> None: + """ + Writes a single 32-bit value to the specified FPGA register. + + Args: + reg_name: name of register + value: 32-bit value + """ with self._lock: reg = result_parser(self.awg.FPGAgetSandBoxRegister(reg_name), reg_name) reg.writeRegisterInt32(value) - def read_fpga(self, reg_name): + def read_fpga(self, reg_name:str) -> int: + """ + Reads a single 32-bit value from the specified FPGA register. + + Args: + reg_name: name of register + + Returns: + 32-bit register value + """ with self._lock: reg = result_parser(self.awg.FPGAgetSandBoxRegister(reg_name), reg_name) @@ -949,30 +1018,83 @@ def read_fpga(self, reg_name): f'({reg.Length:6}) {reg_name}') return reg.readRegisterInt32() - def write_fpga_array(self, reg_name, offset, data): + def write_fpga_array(self, reg_name:str, offset:int, data:List[int], + fixed_address:bool=False) -> None: + """ + Writes a list of 32-bit values to the specified FPGA memory. + + Args: + reg_name: name of memory + offset: offset in memory block + data: list of 32-bit values + fixed_address: + if True all data will be written sequentially to address specified by offset, + else data will be written to consecutive addresses starting at offset + """ with self._lock: reg = result_parser(self.awg.FPGAgetSandBoxRegister(reg_name), reg_name) result_parser( reg.writeRegisterBuffer( - offset, data, keysightSD1.SD_AddressingMode.AUTOINCREMENT, - keysightSD1.SD_AccessMode.NONDMA - ) + offset, data, + keysightSD1.SD_AddressingMode.FIXED + if fixed_address else keysightSD1.SD_AddressingMode.AUTOINCREMENT, + keysightSD1.SD_AccessMode.DMA + ), + f'write_fpga_array({reg_name})' ) - def read_fpga_array(self, reg_name, offset, data_size): + def read_fpga_array(self, reg_name:str, offset:int, data_size:int, + fixed_address:bool=False) -> List[int]: + """ + Reads a list of 32-bit values from the specified FPGA memory. + + Args: + reg_name: name of memory + offset: offset in memory block + fixed_address: + if True all data will be read sequentially from address specified by offset, + else data will be read from consecutive addresses starting at offset + + Returns: + list of 32-bit values + """ with self._lock: reg = result_parser(self.awg.FPGAgetSandBoxRegister(reg_name), reg_name) data = result_parser( reg.readRegisterBuffer( offset, data_size, - keysightSD1.SD_AddressingMode.AUTOINCREMENT, - keysightSD1.SD_AccessMode.NONDMA - ) + keysightSD1.SD_AddressingMode.FIXED + if fixed_address else keysightSD1.SD_AddressingMode.AUTOINCREMENT, + keysightSD1.SD_AccessMode.DMA + ), + f'read_fpga_array({reg_name})' ) return data + def config_fpga_trigger(self, + trigger:SD_TriggerExternalSources, + direction:SD_FpgaTriggerDirection, + polarity:SD_TriggerPolarity=SD_TriggerPolarity.ACTIVE_HIGH, + sync_mode:SD_SyncModes=SD_SyncModes.SYNC_NONE, + delay:int=0) -> None: + """ + Configures external trigger for use in FPGA. + + Args: + trigger: external trigger + direction: IN = (0), INOUT (1) + polarity: ACTIVE_LOW (0), ACTIVE_HIGH (1) + sync_mode: SYNC_NONE (0), SYNC_CLK10 (1) + delay: delay in steps of 5 ns. + """ + with self._lock: + result_parser( + self.awg.FPGATriggerConfig(trigger, direction, polarity, sync_mode, delay), + f'config_fpga_trigger({trigger})' + ) + def convert_sample_rate_to_prescaler(self, sample_rate: float) -> int: """ Returns: @@ -980,7 +1102,8 @@ def convert_sample_rate_to_prescaler(self, sample_rate: float) -> int: """ if is_sd1_3x: # 0 = 1000e6, 1 = 200e6, 2 = 100e6, 3=66.7e6 - prescaler = int(200e6/sample_rate) + # round towards the lower prescaler, unless the value is close to the higher one (margin 0.5 ns) + prescaler = int(200e6/sample_rate + 0.1) else: # 0 = 1000e6, 1 = 200e6, 2 = 50e6, 3=33.3e6 if sample_rate > 200e6: @@ -988,7 +1111,8 @@ def convert_sample_rate_to_prescaler(self, sample_rate: float) -> int: elif sample_rate > 50e6: prescaler = 1 else: - prescaler = int(100e6/sample_rate) + # round towards the lower prescaler, unless the value is close to the higher one + prescaler = int(100e6/sample_rate + 0.1) return prescaler diff --git a/qcodes_contrib_drivers/drivers/Keysight/SD_common/SD_AWG_Async.py b/qcodes_contrib_drivers/drivers/Keysight/SD_common/SD_AWG_Async.py index 0cdac4fc1..c636e9bfa 100644 --- a/qcodes_contrib_drivers/drivers/Keysight/SD_common/SD_AWG_Async.py +++ b/qcodes_contrib_drivers/drivers/Keysight/SD_common/SD_AWG_Async.py @@ -2,8 +2,7 @@ import threading import queue import sys -from dataclasses import dataclass -from typing import Dict, List, Union, Optional, TypeVar, Callable, Any +from typing import Dict, List, Union, Optional, TypeVar, Callable, Any, cast import time import logging from functools import wraps @@ -17,10 +16,16 @@ F = TypeVar('F', bound=Callable[..., Any]) -def switchable(switch, enabled:bool) -> Callable[[F], F]: +def switchable(switch: Callable[[Any], bool], enabled: bool) -> Callable[[F], F]: """ This decorator enables or disables a method depending on the value of an object's method. It throws an exception when the invoked method is disabled. + The wrapped method is enabled when `switch(self) == enabled`. + + Args: + switch: method indicating switch status. + enabled: value the switch status must have to enable the wrapped method. + """ def switchable_decorator(func): @@ -38,6 +43,80 @@ def func_wrapper(self, *args, **kwargs): return switchable_decorator +class Task: + """ + Task to be executed asynchronously. + + Args: + f: function to execute + instance: object function `f` belongs to + args: argument list to pass to function + kwargs: keyword arguments to pass to function + """ + + verbose = False + ''' Enables verbose logging ''' + + def __init__(self, f:F, instance: Any, *args, **kwargs) -> None: + self._event = threading.Event() + self._f = f + self._instance = instance + self._args = args + self._kwargs = kwargs + + def run(self) -> None: + """ + Executes the function. The function result can be retrieved with property `result`. + """ + start = time.perf_counter() + if not self._instance._start_time: + self._instance._start_time = start + + if Task.verbose: + logging.debug(f'[{self._instance.name}] > {self._f.__name__}') + self._result = self._f(self._instance, *self._args, **self._kwargs) + if Task.verbose: + total = time.perf_counter() - self._instance._start_time + logging.debug(f'[{self._instance.name}] < {self._f.__name__} ({(time.perf_counter()-start)*1000:5.2f} ms ' + f'/ {total*1000:5.2f} ms)') + self._event.set() + + @property + def result(self) -> Any: + """ + Returns the result of the executed function. + Waits till function has been executed. + """ + self._event.wait() + return self._result + + +def threaded(wait: bool = False) -> Callable[[F], F]: + """ + Decoractor to execute the wrapped method in the background thread. + + Args: + wait: if True waits till the function has been executed. + """ + + def threaded_decorator(func): + + @wraps(func) + def func_wrapper(self, *args, **kwargs): + + task = Task(func, self, *args, **kwargs) + self._task_queue.put(task) + if wait: + result = task.result + self._start_time = None + return result + return (None, 'async task') + + return func_wrapper + + return threaded_decorator + + class WaveformReference: """ This is a reference to a waveform (being) uploaded to the AWG. @@ -46,38 +125,38 @@ class WaveformReference: wave_number: number refering to the wave in AWG memory awg_name: name of the awg the waveform is uploaded to """ - def __init__(self, wave_number: int, awg_name: str): + def __init__(self, wave_number: int, awg_name: str) -> None: self._wave_number = wave_number self._awg_name = awg_name @property - def wave_number(self): + def wave_number(self) -> int: """ Number of the wave in AWG memory. """ return self._wave_number @property - def awg_name(self): + def awg_name(self) -> str: """ Name of the AWG the waveform is uploaded to """ return self._awg_name - def release(self): + def release(self) -> None: """ Releases the AWG memory for reuse. """ raise NotImplementedError() - def wait_uploaded(self): + def wait_uploaded(self) -> None: """ Waits till waveform has been loaded. Returns immediately if waveform is already uploaded. """ raise NotImplementedError() - def is_uploaded(self): + def is_uploaded(self) -> bool: """ Returns True if waveform has been loaded. """ @@ -85,17 +164,24 @@ def is_uploaded(self): class _WaveformReferenceInternal(WaveformReference): + """ + Reference to waveform in AWG memory. + + Args: + allocated_slot: memory slot containing reference to address in AWG memory. + awg_name: name of the AWG + """ - def __init__(self, allocated_slot: MemoryManager.AllocatedSlot, awg_name: str): + def __init__(self, allocated_slot: MemoryManager.AllocatedSlot, awg_name: str) -> None: super().__init__(allocated_slot.number, awg_name) self._allocated_slot = allocated_slot self._uploaded = threading.Event() self._upload_error: Optional[str] = None self._released: bool = False - self._queued_count = 0 + self._queued_count: int = 0 - def release(self): + def release(self) -> None: """ Releases the memory for reuse. """ @@ -106,7 +192,7 @@ def release(self): self._try_release_slot() - def wait_uploaded(self): + def wait_uploaded(self) -> None: """ Waits till waveform is loaded. Returns immediately if waveform is already uploaded. @@ -123,7 +209,7 @@ def wait_uploaded(self): raise Exception(f'Error loading wave: {self._upload_error}') - def is_uploaded(self): + def is_uploaded(self) -> bool: """ Returns True if waveform has been loaded. """ @@ -133,21 +219,21 @@ def is_uploaded(self): return self._uploaded.is_set() - def enqueued(self): + def enqueued(self) -> None: self._queued_count += 1 - def dequeued(self): + def dequeued(self) -> None: self._queued_count -= 1 self._try_release_slot() - def _try_release_slot(self): + def _try_release_slot(self) -> None: if self._released and self._queued_count <= 0: self._allocated_slot.release() - def __del__(self): + def __del__(self) -> None: if not self._released: logging.warning(f'WaveformReference was not released ' f'({self.awg_name}:{self.wave_number}). Automatic ' @@ -199,25 +285,16 @@ class SD_AWG_Async(SD_AWG): asynchronous (bool): if False the memory manager and asynchronous functionality are disabled. """ - @dataclass - class UploadAction: - action: str - wave: Optional[Union[List[float], List[int], np.ndarray]] - wave_ref: Optional[WaveformReference] - - - _ACTION_STOP = UploadAction('stop', None, None) - _ACTION_INIT_AWG_MEMORY = UploadAction('init', None, None) - _modules: Dict[str, 'SD_AWG_Async'] = {} """ All async modules by unique module id. """ def __init__(self, name, chassis, slot, channels, triggers, waveform_size_limit=1e6, - asynchronous=True, **kwargs): + asynchronous=True, **kwargs) -> None: super().__init__(name, chassis, slot, channels, triggers, **kwargs) self._asynchronous = False self._waveform_size_limit = waveform_size_limit + self._start_time = None module_id = self._get_module_id() if module_id in SD_AWG_Async._modules: @@ -229,14 +306,18 @@ def __init__(self, name, chassis, slot, channels, triggers, waveform_size_limit= self.set_asynchronous(asynchronous) - def asynchronous(self): + def asynchronous(self) -> bool: + ''' Returns True if module is in asynchronous mode. ''' return self._asynchronous - def set_asynchronous(self, asynchronous): + def set_asynchronous(self, asynchronous: bool) -> None: """ Enables asynchronous loading and memory manager if `asynchronous` is True. Otherwise disables both. + + Args: + asynchronous: new asynchronous state. """ if asynchronous == self._asynchronous: return @@ -252,65 +333,218 @@ def set_asynchronous(self, asynchronous): # disable synchronous method of parent class, when wave memory is managed by this class. # @switchable(asynchronous, enabled=False) - def load_waveform(self, waveform_object, waveform_number, verbose=False): - super().load_waveform(waveform_object, waveform_number, verbose) + def load_waveform(self, waveform_object: keysightSD1.SD_Wave, waveform_number: int, + verbose: bool = False) -> int: + """ + Loads the specified waveform into the module onboard RAM. + Waveforms must be created first as an instance of the SD_Wave class. + + Args: + waveform_object: pointer to the waveform object + waveform_number: waveform number to identify the waveform in + subsequent related function calls. + verbose: boolean indicating verbose mode + + Returns: + available onboard RAM in waveform points, or negative numbers for + errors + """ + return super().load_waveform(waveform_object, waveform_number, verbose) @switchable(asynchronous, enabled=False) - def load_waveform_int16(self, waveform_type, data_raw, waveform_number, verbose=False): - super().load_waveform_int16(waveform_type, data_raw, waveform_number, verbose) + def load_waveform_int16(self, waveform_type: int, data_raw: List[int], + waveform_number: int, verbose: bool = False) -> int: + """ + Loads the specified waveform into the module onboard RAM. + Waveforms must be created first as an instance of the SD_Wave class. + + Args: + waveform_type: waveform type + data_raw: array with waveform points + waveform_number: waveform number to identify the waveform + in subsequent related function calls. + verbose: boolean indicating verbose mode + + Returns: + available onboard RAM in waveform points, or negative numbers for + errors + """ + return super().load_waveform_int16(waveform_type, data_raw, waveform_number, verbose) @switchable(asynchronous, enabled=False) - def reload_waveform(self, waveform_object, waveform_number, padding_mode=0, verbose=False): - super().reload_waveform(waveform_object, waveform_number, padding_mode, verbose) + def reload_waveform(self, waveform_object: keysightSD1.SD_Wave, waveform_number: int, + padding_mode: int = 0, verbose: bool = False) -> int: + """ + Replaces a waveform located in the module onboard RAM. + The size of the new waveform must be smaller than or + equal to the existing waveform. + + Args: + waveform_object: pointer to the waveform object + waveform_number: waveform number to identify the waveform + in subsequent related function calls. + padding_mode: + 0: the waveform is loaded as it is, zeros are added at the + end if the number of points is not a multiple of the number + required by the AWG. + 1: the waveform is loaded n times (using DMA) until the total + number of points is multiple of the number required by the + AWG. (only works for waveforms with even number of points) + verbose: boolean indicating verbose mode + + Returns: + available onboard RAM in waveform points, or negative numbers for + errors + """ + return super().reload_waveform(waveform_object, waveform_number, padding_mode, verbose) @switchable(asynchronous, enabled=False) - def reload_waveform_int16(self, waveform_type, data_raw, waveform_number, padding_mode=0, verbose=False): - super().reload_waveform_int16(waveform_type, data_raw, waveform_number, padding_mode, verbose) + def reload_waveform_int16(self, waveform_type: int, data_raw: List[int], + waveform_number: int, padding_mode: int = 0, + verbose: bool = False) -> int: + """ + Replaces a waveform located in the module onboard RAM. + The size of the new waveform must be smaller than or + equal to the existing waveform. + + Args: + waveform_type: waveform type + data_raw: array with waveform points + waveform_number: waveform number to identify the waveform + in subsequent related function calls. + padding_mode: + 0: the waveform is loaded as it is, zeros are added at the + end if the number of points is not a multiple of the number + required by the AWG. + 1: the waveform is loaded n times (using DMA) until the total + number of points is multiple of the number required by the + AWG. (only works for waveforms with even number of points) + verbose: boolean indicating verbose mode + + Returns: + available onboard RAM in waveform points, or negative numbers for + errors + """ + return super().reload_waveform_int16(waveform_type, data_raw, waveform_number, padding_mode, verbose) @switchable(asynchronous, enabled=False) - def flush_waveform(self, verbose=False): + def flush_waveform(self, verbose: bool = False) -> None: + """ + Deletes all waveforms from the module onboard RAM and flushes all the + AWG queues. + """ super().flush_waveform(verbose) @switchable(asynchronous, enabled=False) - def awg_from_file(self, awg_number, waveform_file, trigger_mode, start_delay, cycles, prescaler, padding_mode=0, - verbose=False): - super().awg_from_file(awg_number, waveform_file, trigger_mode, start_delay, cycles, prescaler, padding_mode, - verbose) + def awg_from_file(self, awg_number: int, waveform_file: str, + trigger_mode: int, start_delay: int, cycles: int, + prescaler: int, padding_mode: int = 0, + verbose: bool = False) -> int: + """ + Provides a one-step method to load, queue and start a single waveform + in one of the module AWGs. + + Loads a waveform from file. + + Args: + awg_number: awg number where the waveform is queued + waveform_file: file containing the waveform points + trigger_mode: trigger method to launch the waveform + Auto : 0 + Software/HVI : 1 + Software/HVI (per cycle) : 5 + External trigger : 2 + External trigger (per cycle): 6 + start_delay: defines the delay between trigger and wf launch + given in multiples of 10ns. + cycles: number of times the waveform is repeated once launched + zero = infinite repeats + prescaler: waveform prescaler value, to reduce eff. sampling rate + + Returns: + available onboard RAM in waveform points, or negative numbers for + errors + """ + return super().awg_from_file(awg_number, waveform_file, trigger_mode, start_delay, + cycles, prescaler, padding_mode, verbose) @switchable(asynchronous, enabled=False) - def awg_from_array(self, awg_number, trigger_mode, start_delay, cycles, prescaler, waveform_type, waveform_data_a, - waveform_data_b=None, padding_mode=0, verbose=False): - super().awg_from_array(awg_number, trigger_mode, start_delay, cycles, prescaler, waveform_type, waveform_data_a, - waveform_data_b, padding_mode, verbose) + def awg_from_array(self, awg_number: int, trigger_mode: int, + start_delay: int, cycles: int, prescaler: int, + waveform_type: int, + waveform_data_a: List[Union[int, float]], + waveform_data_b: Optional[ + List[Union[int, float]]] = None, + padding_mode: int = 0, verbose: bool = False) -> int: + """ + Provides a one-step method to load, queue and start a single waveform + in one of the module AWGs. + Loads a waveform from array. - def awg_flush(self, awg_number): + Args: + awg_number: awg number where the waveform is queued + trigger_mode: trigger method to launch the waveform + Auto : 0 + Software/HVI : 1 + Software/HVI (per cycle) : 5 + External trigger : 2 + External trigger (per cycle): 6 + start_delay: defines the delay between trigger and wf launch + given in multiples of 10ns. + cycles: number of times the waveform is repeated once launched + zero = infinite repeats + prescaler: waveform prescaler value, to reduce eff. sampling rate + waveform_type: waveform type + waveform_data_a: array with waveform points + waveform_data_b: array with waveform points, only for the waveforms + which have a second component + + Returns: + available onboard RAM in waveform points, or negative numbers for + errors + """ + return super().awg_from_array(awg_number, trigger_mode, start_delay, cycles, prescaler, + waveform_type, waveform_data_a,waveform_data_b, padding_mode, verbose) + + def awg_flush(self, awg_number: int) -> None: + """ + Empties the queue of the selected AWG. + Waveforms are not removed from the onboard RAM. + """ super().awg_flush(awg_number) if self._asynchronous: self._release_waverefs_awg(awg_number) + @threaded(wait=True) + def uploader_ready(self) -> bool: + """ Waits until uploader thread is ready with tasks queued before this call. """ + return True - def awg_queue_waveform(self, awg_number, waveform_ref, trigger_mode, start_delay, cycles, prescaler): + def awg_queue_waveform(self, awg_number: int, + waveform_ref: Union[int, _WaveformReferenceInternal], + trigger_mode: int, start_delay: int, cycles: int, + prescaler: int) -> None: """ Enqueus the waveform. Args: - awg_number (int): awg number (channel) where the waveform is queued - waveform_ref (Union[int, _WaveformReferenceInternal)]: - reference to a waveform - trigger_mode (int): trigger method to launch the waveform + awg_number: awg number (channel) where the waveform is queued + waveform_ref: reference to a waveform + trigger_mode: trigger method to launch the waveform Auto : 0 Software/HVI : 1 Software/HVI (per cycle) : 5 External trigger : 2 External trigger (per cycle): 6 - start_delay (int): defines the delay between trigger and wf launch + start_delay: defines the delay between trigger and wf launch given in multiples of 10ns. cycles (int): number of times the waveform is repeated once launched zero = infinite repeats - prescaler (int): waveform prescaler value, to reduce eff. sampling rate + prescaler: waveform prescaler value, to reduce eff. sampling rate """ if self.asynchronous(): + waveform_ref = cast(_WaveformReferenceInternal, waveform_ref) if waveform_ref.awg_name != self.name: raise Exception(f'Waveform not uploaded to this AWG ({self.name}). ' f'It is uploaded to {waveform_ref.awg_name}') @@ -327,13 +561,13 @@ def awg_queue_waveform(self, awg_number, waveform_ref, trigger_mode, start_delay self._enqueued_waverefs[awg_number].append(waveform_ref) wave_number = waveform_ref.wave_number else: - wave_number = waveform_ref + wave_number = cast(int, waveform_ref) super().awg_queue_waveform(awg_number, wave_number, trigger_mode, start_delay, cycles, prescaler) @switchable(asynchronous, enabled=True) - def set_waveform_limit(self, requested_waveform_size_limit: int): + def set_waveform_limit(self, requested_waveform_size_limit: int) -> None: """ Increases the maximum size of waveforms that can be uploaded. @@ -341,15 +575,14 @@ def set_waveform_limit(self, requested_waveform_size_limit: int): Limit can not be reduced, because reservation cannot be undone. Args: - requested_waveform_size_limit (int): maximum size of waveform that can be uploaded + requested_waveform_size_limit: maximum size of waveform that can be uploaded """ self._memory_manager.set_waveform_limit(requested_waveform_size_limit) - self._upload_queue.put(SD_AWG_Async._ACTION_INIT_AWG_MEMORY) + self._init_awg_memory() @switchable(asynchronous, enabled=True) - def upload_waveform(self, wave: Union[List[float], - List[int], np.ndarray] + def upload_waveform(self, wave: Union[List[float], List[int], np.ndarray] ) -> _WaveformReferenceInternal: """ Upload the wave using the uploader thread for this AWG. @@ -364,14 +597,11 @@ def upload_waveform(self, wave: Union[List[float], allocated_slot = self._memory_manager.allocate(len(wave)) ref = _WaveformReferenceInternal(allocated_slot, self.name) self.log.debug(f'upload: {ref.wave_number}') - - entry = SD_AWG_Async.UploadAction('upload', wave, ref) - self._upload_queue.put(entry) - + self._upload(wave, ref) return ref - def close(self): + def close(self) -> None: """ Closes the module and stops background thread. """ @@ -384,57 +614,60 @@ def close(self): super().close() - def _get_module_id(self): + def _get_module_id(self) -> str: """ Generates a unique name for this module. """ return f'{self.module_name}:{self.chassis_number()}-{self.slot_number()}' - def _start_asynchronous(self): + def _start_asynchronous(self) -> None: """ Starts the asynchronous upload thread and memory manager. """ super().flush_waveform() - self._memory_manager = MemoryManager(self.log, self._waveform_size_limit) - self._enqueued_waverefs = {} + self._memory_manager: MemoryManager = MemoryManager(self.log, self._waveform_size_limit) + self._enqueued_waverefs:Dict[int, List[_WaveformReferenceInternal]] = {} for i in range(self.channels): self._enqueued_waverefs[i+1] = [] - self._upload_queue = queue.Queue() - self._thread = threading.Thread(target=self._run, name=f'uploader-{self.module_id}') + self._task_queue: queue.Queue = queue.Queue() + self._init_awg_memory() + self._thread: threading.Thread = threading.Thread(target=self._run, name=f'uploader-{self.module_id}') self._thread.start() - def _stop_asynchronous(self): + def _stop_asynchronous(self) -> None: """ Stops the asynchronous upload thread and memory manager. """ - self._upload_queue.put(SD_AWG_Async._ACTION_STOP) + if self._task_queue: + self._task_queue.put('STOP') + # wait at most 15 seconds. Should be more enough for normal scenarios self._thread.join(15) if self._thread.is_alive(): self.log.error(f'AWG upload thread {self.module_id} stop failed. Thread still running.') self._release_waverefs() - self._memory_manager = None - self._upload_queue = None - self._thread = None + del self._memory_manager + del self._task_queue + del self._thread - def _release_waverefs(self): + def _release_waverefs(self) -> None: for i in range(self.channels): self._release_waverefs_awg(i + 1) - def _release_waverefs_awg(self, awg_number): + def _release_waverefs_awg(self, awg_number: int) -> None: for waveref in self._enqueued_waverefs[awg_number]: waveref.dequeued() self._enqueued_waverefs[awg_number] = [] - - def _init_awg_memory(self): + @threaded() + def _init_awg_memory(self) -> None: """ Initialize memory on the AWG by uploading waveforms with all zeros. """ @@ -444,65 +677,64 @@ def _init_awg_memory(self): self.log.info(f'Reserving awg memory for {len(new_slots)} slots') - zeros = [] + zeros: np.ndarray = np.zeros(0) wave = None total_size = 0 - total_duration = 0 + total_duration = 0.0 for slot in new_slots: start = time.perf_counter() if len(zeros) != slot.size or wave is None: - zeros = np.zeros(slot.size, np.float) + zeros = np.zeros(slot.size, float) wave = keysightSD1.SD_Wave() result_parser(wave.newFromArrayDouble(keysightSD1.SD_WaveformTypes.WAVE_ANALOG, zeros)) super().load_waveform(wave, slot.number) duration = time.perf_counter() - start + # self.log.debug(f'uploaded {slot.size} in {duration*1000:5.2f} ms ({slot.size/duration/1e6:5.2f} MSa/s)') total_duration += duration total_size += slot.size self.log.info(f'Awg memory reserved: {len(new_slots)} slots, {total_size/1e6} MSa in ' f'{total_duration*1000:5.2f} ms ({total_size/total_duration/1e6:5.2f} MSa/s)') + @threaded() + def _upload(self, + wave_data: Union[List[float], List[int], np.ndarray], + wave_ref: _WaveformReferenceInternal) -> None: + # self.log.debug(f'Uploading {wave_ref.wave_number}') + try: + start = time.perf_counter() - def _run(self): - self._init_awg_memory() + wave = keysightSD1.SD_Wave() + result_parser(wave.newFromArrayDouble(keysightSD1.SD_WaveformTypes.WAVE_ANALOG, wave_data)) + super().reload_waveform(wave, wave_ref.wave_number) + + duration = time.perf_counter() - start + speed = len(wave_data)/duration + self.log.debug(f'Uploaded {wave_ref.wave_number} in {duration*1000:5.2f} ms ({speed/1e6:5.2f} MSa/s)') + except Exception as ex: + msg = f'{type(ex).__name__}:{ex}' + min_value = np.min(wave_data) + max_value = np.max(wave_data) + if min_value < -1.0 or max_value > 1.0: + msg += ': Voltage out of range' + self.log.error(f'Failure load waveform {wave_ref.wave_number}: {msg}' ) + wave_ref._upload_error = msg + + # signal upload done, either successful or with error + wave_ref._uploaded.set() + + + def _run(self) -> None: self.log.info('Uploader ready') while True: - entry: SD_AWG_Async.UploadItem = self._upload_queue.get() - if entry == SD_AWG_Async._ACTION_STOP: + task: Task = self._task_queue.get() + if task == 'STOP': break - - if entry == SD_AWG_Async._ACTION_INIT_AWG_MEMORY: - self._init_awg_memory() - continue - - wave_ref = entry.wave_ref - self.log.debug(f'Uploading {wave_ref.wave_number}') try: - start = time.perf_counter() - - wave = keysightSD1.SD_Wave() - result_parser(wave.newFromArrayDouble(keysightSD1.SD_WaveformTypes.WAVE_ANALOG, entry.wave)) - super().reload_waveform(wave, wave_ref.wave_number) - - duration = time.perf_counter() - start - speed = len(entry.wave)/duration - self.log.debug(f'Uploaded {wave_ref.wave_number} in {duration*1000:5.2f} ms ({speed/1e6:5.2f} MSa/s)') + task.run() except: - ex = sys.exc_info() - msg = f'{ex[0].__name__}:{ex[1]}' - min_value = np.min(entry.wave) - max_value = np.max(entry.wave) - if min_value < -1.0 or max_value > 1.0: - msg += ': Voltage out of range' - self.log.error(f'Failure load waveform {wave_ref.wave_number}: {msg}' ) - wave_ref._upload_error = msg - - # signal upload done, either successful or with error - wave_ref._uploaded.set() - - # release memory - wave = None - entry = None + logging.error('Task thread error', exc_info=True) + del task self.log.info('Uploader terminated') diff --git a/qcodes_contrib_drivers/drivers/Keysight/SD_common/SD_Module.py b/qcodes_contrib_drivers/drivers/Keysight/SD_common/SD_Module.py index fca219756..e4a02bf2c 100644 --- a/qcodes_contrib_drivers/drivers/Keysight/SD_common/SD_Module.py +++ b/qcodes_contrib_drivers/drivers/Keysight/SD_common/SD_Module.py @@ -130,61 +130,61 @@ def __init__(self, name: str, chassis: int, slot: int, # Get-commands # - def get_module_count(self, verbose: bool = False) -> Any: + def get_module_count(self, verbose: bool = False) -> int: """Returns the number of SD modules installed in the system""" value = self.SD_module.moduleCount() value_name = 'module_count' return result_parser(value, value_name, verbose) - def get_product_name(self, verbose: bool = False) -> Any: + def get_product_name(self, verbose: bool = False) -> str: """Returns the product name of the device""" value = self.SD_module.getProductName() value_name = 'product_name' return result_parser(value, value_name, verbose) - def get_serial_number(self, verbose: bool = False) -> Any: + def get_serial_number(self, verbose: bool = False) -> str: """Returns the serial number of the device""" value = self.SD_module.getSerialNumber() value_name = 'serial_number' return result_parser(value, value_name, verbose) - def get_chassis(self, verbose: bool = False) -> Any: + def get_chassis(self, verbose: bool = False) -> int: """Returns the chassis number where the device is located""" value = self.SD_module.getChassis() value_name = 'chassis_number' return result_parser(value, value_name, verbose) - def get_slot(self, verbose: bool = False) -> Any: + def get_slot(self, verbose: bool = False) -> int: """Returns the slot number where the device is located""" value = self.SD_module.getSlot() value_name = 'slot_number' return result_parser(value, value_name, verbose) - def get_status(self, verbose: bool = False) -> Any: + def get_status(self, verbose: bool = False) -> int: """Returns the status of the device""" value = self.SD_module.getStatus() value_name = 'status' return result_parser(value, value_name, verbose) - def get_firmware_version(self, verbose: bool = False) -> Any: + def get_firmware_version(self, verbose: bool = False) -> str: """Returns the firmware version of the device""" value = self.SD_module.getFirmwareVersion() value_name = 'firmware_version' return result_parser(value, value_name, verbose) - def get_hardware_version(self, verbose: bool = False) -> Any: + def get_hardware_version(self, verbose: bool = False) -> str: """Returns the hardware version of the device""" value = self.SD_module.getHardwareVersion() value_name = 'hardware_version' return result_parser(value, value_name, verbose) - def get_type(self, verbose: bool = False) -> Any: + def get_type(self, verbose: bool = False) -> int: """Returns the type of the device""" value = self.SD_module.getType() value_name = 'type' return result_parser(value, value_name, verbose) - def get_open(self, verbose: bool = False) -> Any: + def get_open(self, verbose: bool = False) -> bool: """Returns whether the device is open (True) or not (False)""" value = self.SD_module.isOpen() value_name = 'open' @@ -211,7 +211,7 @@ def get_pxi_trigger(self, pxi_trigger: int, verbose: bool = False) -> int: # def set_pxi_trigger(self, value: int, pxi_trigger: int, - verbose: bool = False) -> Any: + verbose: bool = False) -> None: """ Sets the digital value of the specified PXI trigger @@ -222,7 +222,7 @@ def set_pxi_trigger(self, value: int, pxi_trigger: int, """ result = self.SD_module.PXItriggerWrite(pxi_trigger, value) value_name = f'set pxi_trigger {pxi_trigger} to {value}' - return result_parser(result, value_name, verbose) + result_parser(result, value_name, verbose) # # FPGA related functions @@ -241,6 +241,8 @@ def get_fpga_pc_port(self, port: int, data_size: int, address: int, address_mode: auto-increment (0), or fixed (1) access_mode: non-dma (0), or dma (1) verbose: boolean indicating verbose mode + Returns: + register data. """ data = self.SD_module.FPGAreadPCport(port, data_size, address, address_mode, access_mode) @@ -249,7 +251,7 @@ def get_fpga_pc_port(self, port: int, data_size: int, address: int, def set_fpga_pc_port(self, port: int, data: List[int], address: int, address_mode: int, access_mode: int, - verbose: bool = False) -> Any: + verbose: bool = False) -> None: """ Writes data at the PCport FPGA Block @@ -267,23 +269,26 @@ def set_fpga_pc_port(self, port: int, data: List[int], address: int, value_name = f'set fpga PCport {port} to data:{data}, ' \ f'address:{address}, address_mode:{address_mode}, ' \ f'access_mode:{access_mode}' - return result_parser(result, value_name, verbose) + result_parser(result, value_name, verbose) + + def load_fpga_image(self, filename: str) -> None: + """ + Loads FPGA binary image in module. - def load_fpga_image(self, filename: str) -> Any: + Args: + filename: name of the FPGA image. + """ if not os.path.exists(filename): raise Exception(f'FPGA bitstream {filename} not found') - result = result_parser(self.SD_module.FPGAload(filename), - f'loading FPGA bitstream: {filename}') - if isinstance(result, int) and result < 0: - raise Exception(f'Failed to load FPGA bitstream') - return result + result_parser(self.SD_module.FPGAload(filename), + f'loading FPGA bitstream: {filename}') # # HVI related functions # def set_hvi_register(self, register: Union[int, str], value: int, - verbose: bool = False) -> Any: + verbose: bool = False) -> None: """ Sets value of specified HVI register. @@ -326,19 +331,19 @@ def get_hvi_register(self, register: Union[int, str], # def get_product_name_by_slot(self, chassis: int, slot: int, - verbose: bool = False) -> Any: + verbose: bool = False) -> str: value = self.SD_module.getProductNameBySlot(chassis, slot) value_name = 'product_name' return result_parser(value, value_name, verbose) def get_product_name_by_index(self, index: Any, - verbose: bool = False) -> Any: + verbose: bool = False) -> str: value = self.SD_module.getProductNameByIndex(index) value_name = 'product_name' return result_parser(value, value_name, verbose) def get_serial_number_by_slot(self, chassis: int, slot: int, - verbose: bool = False) -> Any: + verbose: bool = False) -> str: warnings.warn('Returns faulty serial number due to error in Keysight ' 'lib v.2.01.00', UserWarning) value = self.SD_module.getSerialNumberBySlot(chassis, slot) @@ -346,7 +351,7 @@ def get_serial_number_by_slot(self, chassis: int, slot: int, return result_parser(value, value_name, verbose) def get_serial_number_by_index(self, index: Any, - verbose: bool = False) -> Any: + verbose: bool = False) -> str: warnings.warn('Returns faulty serial number due to error in Keysight ' 'lib v.2.01.00', UserWarning) value = self.SD_module.getSerialNumberByIndex(index) @@ -354,12 +359,12 @@ def get_serial_number_by_index(self, index: Any, return result_parser(value, value_name, verbose) def get_type_by_slot(self, chassis: int, slot: int, - verbose: bool = False) -> Any: + verbose: bool = False) -> int: value = self.SD_module.getTypeBySlot(chassis, slot) value_name = 'type' return result_parser(value, value_name, verbose) - def get_type_by_index(self, index: Any, verbose: bool = False) -> Any: + def get_type_by_index(self, index: Any, verbose: bool = False) -> int: value = self.SD_module.getTypeByIndex(index) value_name = 'type' return result_parser(value, value_name, verbose) @@ -394,7 +399,7 @@ def open_with_slot(self, name: str, chassis: int, slot: int) -> Any: result = self.SD_module.openWithSlot(name, chassis, slot) return result_parser(result, f'openWithSlot({name}, {chassis}, {slot})') - def run_self_test(self) -> Any: + def run_self_test(self) -> int: value = self.SD_module.runSelfTest() print(f'Did self test and got result: {value}') return value diff --git a/qcodes_contrib_drivers/drivers/Keysight/SD_common/memory_manager.py b/qcodes_contrib_drivers/drivers/Keysight/SD_common/memory_manager.py index 9cc591b81..54418b85a 100644 --- a/qcodes_contrib_drivers/drivers/Keysight/SD_common/memory_manager.py +++ b/qcodes_contrib_drivers/drivers/Keysight/SD_common/memory_manager.py @@ -2,7 +2,7 @@ from dataclasses import dataclass from typing import List, Dict import logging - +from datetime import datetime class MemoryManager: """ @@ -21,6 +21,7 @@ class MemoryManager: Args: waveform_size_limit: maximum waveform size to support. """ + verbose = False @dataclass class AllocatedSlot: @@ -41,6 +42,7 @@ class _MemorySlot: '''Unique reference value when allocated. Used to check for incorrect or missing release calls. ''' + allocation_time: str = '' # Note (M3202A): size must be multiples of 10 and >= 2000 memory_sizes = [ @@ -81,7 +83,7 @@ def set_waveform_limit(self, waveform_size_limit: int) -> None: self._max_waveform_size = waveform_size_limit self._create_memory_slots(waveform_size_limit) - def get_uninitialized_slots(self) -> List[_MemorySlot]: + def get_uninitialized_slots(self) -> List['MemoryManager._MemorySlot']: """ Returns list of slots that must be initialized (reserved in AWG) """ @@ -95,7 +97,7 @@ def get_uninitialized_slots(self) -> List[_MemorySlot]: return new_slots - def allocate(self, wave_size: int) -> AllocatedSlot: + def allocate(self, wave_size: int) -> 'MemoryManager.AllocatedSlot': """ Allocates a memory slot with at least the specified wave size. @@ -120,7 +122,9 @@ def allocate(self, wave_size: int) -> AllocatedSlot: self._allocation_ref_count += 1 self._slots[slot].allocation_ref = self._allocation_ref_count self._slots[slot].allocated = True - self._log.debug(f'Allocated slot {slot}') + self._slots[slot].allocation_time = datetime.now().strftime('%H:%M:%S.%f') + if MemoryManager.verbose: + self._log.debug(f'Allocated slot {slot}') return MemoryManager.AllocatedSlot(slot, self._slots[slot].allocation_ref, self) raise Exception(f'No free memory slots left for waveform with' @@ -145,11 +149,12 @@ def release(self, allocated_slot: AllocatedSlot) -> None: slot.allocation_ref = 0 self._free_memory_slots[slot.size].append(slot_number) - try: - self._log.debug(f'Released slot {slot_number}') - except: - # self._log throws exception when instrument has been closed. - logging.debug(f'Released slot {slot_number}') + if MemoryManager.verbose: + try: + self._log.debug(f'Released slot {slot_number}') + except: + # self._log throws exception when instrument has been closed. + logging.debug(f'Released slot {slot_number}') def _create_memory_slots(self, max_size: int) -> None: @@ -180,3 +185,29 @@ def _get_slot_size(self, size: int) -> int: return slot_size raise Exception(f'Requested waveform size {size} is too big') + + def mem_usage(self): + ''' + Example: + pprint(awg._memory_manager.mem_usage(), sort_dicts=False) + ''' + result = {} + result[' Block size'] = ['Created', 'Allocated'] + for size in self._slot_sizes: + result[str(size)] = [0,0,0] + for slot in self._slots: + stats = result[str(slot.size)] + stats[0] += 1 + stats[1] += slot.allocated + result['Free'] = {size:len(slots) for size,slots in self._free_memory_slots.items()} + return result + + def allocation_state(self): + ''' + Example: + pprint(awg._memory_manager.allocation_state()) + ''' + result = {} + result[' Free'] = {size:len(slots) for size,slots in self._free_memory_slots.items()} + result['Allocated'] = [slot for slot in self._slots if slot.allocated] + return result