From a6585823a0d88065c16b0c05aff11d12493bccc2 Mon Sep 17 00:00:00 2001 From: Carter Turn Date: Mon, 31 Mar 2025 08:03:37 -0400 Subject: [PATCH 01/22] Initial draft of DDS sweeper --- .../AD9959DDSSweeper/blacs_tabs.py | 56 ++++++ .../AD9959DDSSweeper/blacs_workers.py | 172 ++++++++++++++++++ .../AD9959DDSSweeper/labscript_devices.py | 154 ++++++++++++++++ .../AD9959DDSSweeper/register_classes.py | 20 ++ .../AD9959DDSSweeper/runviewer_parser.py | 57 ++++++ 5 files changed, 459 insertions(+) create mode 100644 labscript_devices/AD9959DDSSweeper/blacs_tabs.py create mode 100644 labscript_devices/AD9959DDSSweeper/blacs_workers.py create mode 100644 labscript_devices/AD9959DDSSweeper/labscript_devices.py create mode 100644 labscript_devices/AD9959DDSSweeper/register_classes.py create mode 100644 labscript_devices/AD9959DDSSweeper/runviewer_parser.py diff --git a/labscript_devices/AD9959DDSSweeper/blacs_tabs.py b/labscript_devices/AD9959DDSSweeper/blacs_tabs.py new file mode 100644 index 0000000..1810c07 --- /dev/null +++ b/labscript_devices/AD9959DDSSweeper/blacs_tabs.py @@ -0,0 +1,56 @@ +##################################################################### +# # +# /labscript_devices/AD9959DDSSweeper/blacs_tabs.py # +# # +# Copyright 2025, Carter Turnbaugh # +# # +# This file is part of the module labscript_devices, in the # +# labscript suite (see http://labscriptsuite.org), and is # +# licensed under the Simplified BSD License. See the license.txt # +# file in the root of the project for the full license. # +# # +##################################################################### + +from blacs.device_base_class import DeviceTab + +class AD9959DDSSweeperTab(DeviceTab): + def initialise_GUI(self): + # Capabilities + self.base_units = {'freq':'Hz', 'amp':'Arb', 'phase':'Degrees'} + self.base_min = {'freq':0.0, 'amp':0, 'phase':0} + self.base_max = {'freq':250.0*10.0**6, 'amp':1, 'phase':360} + self.base_step = {'freq':10**6, 'amp':1/1023., 'phase':1} + self.base_decimals = {'freq':1, 'amp':4, 'phase':3} + self.num_DDS = 4 + + dds_prop = {} + for i in range(self.num_DDS): + dds_prop['channel %d' % i] = {} + for subchnl in ['freq', 'amp', 'phase']: + dds_prop['channel %d' % i][subchnl] = {'base_unit':self.base_units[subchnl], + 'min':self.base_min[subchnl], + 'max':self.base_max[subchnl], + 'step':self.base_step[subchnl], + 'decimals':self.base_decimals[subchnl] + } + + self.create_dds_outputs(dds_prop) + dds_widgets, _, _ = self.auto_create_widgets() + self.auto_place_widgets(('DDS Outputs', dds_widgets)) + + device = self.settings['connection_table'].find_by_name(self.device_name) + + self.com_port = device.properties['com_port'] + + self.supports_remote_value_check(False) + self.supports_smart_programming(True) + + def initialise_workers(self): + self.create_worker( + "main_worker", + "labscript_devices.AD9959DDSSweeper.blacs_workers.AD9959DDSSweeperWorker", + { + 'com_port': self.com_port, + }, + ) + self.primary_worker = "main_worker" diff --git a/labscript_devices/AD9959DDSSweeper/blacs_workers.py b/labscript_devices/AD9959DDSSweeper/blacs_workers.py new file mode 100644 index 0000000..70f262b --- /dev/null +++ b/labscript_devices/AD9959DDSSweeper/blacs_workers.py @@ -0,0 +1,172 @@ +##################################################################### +# # +# /labscript_devices/AD9959DDSSweeper/blacs_workers.py # +# # +# Copyright 2025, Carter Turnbaugh # +# # +# This file is part of the module labscript_devices, in the # +# labscript suite (see http://labscriptsuite.org), and is # +# licensed under the Simplified BSD License. See the license.txt # +# file in the root of the project for the full license. # +# # +##################################################################### + +from blacs.tab_base_classes import Worker +import labscript_utils.h5_lock, h5py + +class AD9959DDSSweeperInterface(object): + def __init__(self, com_port, ref_clock_frequency, pll_mult): + global serial; import serial + + self.timeout = 0.1 + self.conn = serial.Serial(com_port, 10000000, timeout=self.timeout) + + version = self.get_version() + print(f'Connected to version: {version}') + + current_status = self.get_status() + print(f'Current status is {current_status}') + + self.conn.write(b'reset\n') + self.assert_OK() + self.conn.write(b'setclock 0 %d\n' % ref_clock_frequency) + self.assert_OK() + self.conn.write(b'mode 0 0\n') + self.assert_OK() + self.conn.write(b'setmult %d\n' % pll_mult) + self.assert_OK() + self.conn.write(b'debug off\n') + self.assert_OK() + + def assert_OK(self): + resp = self.conn.readline().decode().strip() + assert resp == "ok", 'Exepcted "ok", received "%s"' % resp + + def get_version(self): + '''Sends 'version' command, which retrieves the Pico firmware version. + Returns response, throws serial exception on disconnect.''' + self.conn.write(b'version\n') + version_str = self.conn.readline().decode() + version = tuple(int(i) for i in version_str.split('.')) + assert len(version) == 3 + return version + + def abort(self): + '''Stops buffered execution immediately.''' + self.conn.write(b'abort\n') + self.assert_OK() + + def start(self): + '''Starts buffered execution.''' + self.conn.write(b'start\n') + self.assert_OK() + + def get_status(self): + '''Reads the status of the AD9959 DDS Sweeper + Returns int status code.`''' + self.conn.write(b'status\n') + status_str = int(self.conn.readline().decode()) + if status_str == 0: + return 'STOPPED' + elif status_str == 1: + return 'RUNNING' + elif status_str == 2: + return 'ABORTING' + else: + raise LabscriptError(f'PrawnDO invalid status, returned {status_str}') + + def get_freqs(self): + '''Responds with a dictionary containing + the current operating frequencies (in kHz) of various clocks.''' + self.conn.write(b'getfreqs\n') + freqs = {} + while True: + resp = self.conn.readline().decode() + if resp == "ok": + break + resp = resp.split('=') + freqs[resp[0].strip()] = int(resp[1].strip()[:-3]) + return freqs + + def set_output(self, channel, frequency, amplitude, phase): + '''Set frequency, amplitude, and phase of a channel.''' + self.conn.write(b'setfreq %d %f\n' % (channel, frequency)) + self.assert_OK() + self.conn.write(b'setamp %d %f\n' % (channel, amplitude)) + self.assert_OK() + self.conn.write(b'setphase %d %f\n' % (channel, phase)) + self.assert_OK() + + def set_channels(self, channels): + '''Set number of channels to use in buffered sequence.''' + self.conn.write(b'setchannels %d\n' % channels) + self.assert_OK() + + def set(self, channel, addr, frequency, amplitude, phase): + '''Set frequency, phase, and amplitude of a channel + for address addr in buffered sequence from integer values.''' + self.conn.write(b'seti %d %d %f %f %f\n' % (channel, addr, frequency, amplitude, phase)) + self.assert_OK() + + def set_batch(self, table): + '''Set frequency, phase, and amplitude of a channel + for address addr in buffered sequence.''' + self.conn.write(b'setb 0 %d\n' % len(table)) + resp = self.conn.readline().decode() + if not resp.startswith('ready'): + resp += ''.join([r.decode() for r in self.conn.readlines()]) + raise LabscriptError(f'setb command failed, got response {repr(resp)}') + self.conn.write(table.tobytes()) + self.assert_OK() + + def stop(self, count): + self.conn.write(b'set 4 %d\n' % count) + self.assert_OK() + + def close(self): + self.conn.close() + +class AD9959DDSSweeperWorker(Worker): + def init(self): + self.intf = AD9959DDSSweeperInterface(self.com_port, self.ref_clock_frequency, self.pll_mult) + + def program_manual(self, values): + self.intf.abort() + + for chan in values: + chan_int = int(chan[8:]) + self.intf.set_output(chan_int, values[chan]['freq'], values[chan]['amp'], values[chan]['phase']) + + def transition_to_buffered(self, device_name, h5file, initial_values, fresh): + self.final_values = initial_values + + with h5py.File(h5file, 'r') as hdf5_file: + group = hdf5_file['devices'][device_name] + dds_data = group['dds_data'] + + if len(dds_data) == 0: + # Don't bother transitioning to buffered if no data + return {} + + channels = set([int(n[4:]) for n in dds_data.dtype.names if n.startswith('freq')]) + self.intf.set_channels(max(channels) + 1) + self.intf.set_batch(dds_data[()]) + self.intf.stop(len(dds_data[()])) + + self.intf.start() + + return {} + + def transition_to_manual(self): + if self.final_values: + self.program_manual(self.final_values) + return True + + def abort_buffered(self): + return self.transition_to_manual() + + def abort_transition_to_buffered(self): + return self.transition_to_manual() + + def shutdown(self): + self.intf.close() diff --git a/labscript_devices/AD9959DDSSweeper/labscript_devices.py b/labscript_devices/AD9959DDSSweeper/labscript_devices.py new file mode 100644 index 0000000..f16a0c8 --- /dev/null +++ b/labscript_devices/AD9959DDSSweeper/labscript_devices.py @@ -0,0 +1,154 @@ +##################################################################### +# # +# /labscript_devices/AD9959DDSSweeper/labscript_devices.py # +# # +# Copyright 2025, Carter Turnbaugh # +# # +# This file is part of the module labscript_devices, in the # +# labscript suite (see http://labscriptsuite.org), and is # +# licensed under the Simplified BSD License. See the license.txt # +# file in the root of the project for the full license. # +# # +##################################################################### + +from labscript import DDS, StaticDDS, IntermediateDevice, set_passed_properties, LabscriptError, config +from labscript_utils.unitconversions import NovaTechDDS9mFreqConversion, NovaTechDDS9mAmpConversion + + +import numpy as np +import sys + +class AD9959DDSSweeper(IntermediateDevice): + allowed_children = [DDS, StaticDDS] + + @set_passed_properties( + property_names={ + 'connection_table_properties': [ + 'name', + 'com_port', + 'ref_clock_frequency', + 'pll_mult', + ] + } + ) + + def __init__(self, name, parent_device, com_port, + ref_clock_frequency=125e6, pll_mult=4, **kwargs): + '''Labscript device class for AD9959 eval board controlled by a Raspberry Pi Pico. + ''' + IntermediateDevice.__init__(self, name, parent_device, **kwargs) + self.BLACS_connection = '%s' % com_port + + # Check clocking + if ref_clock_frequency * pll_mult > 500e6: + raise ValueError('DDS system clock frequency must be less than 500 MHz') + elif pll_mult > 1 and ref_clock_frequency * pll_mult < 100e6: + raise ValueError('DDS system clock frequency must be greater than 100 MHz when using PLL') + + self.dds_clock = ref_clock_frequency * pll_mult + self.clk_scale = 2**32 / self.dds_clock + + def get_default_unit_conversion_classes(self, device): + """Child devices call this during their __init__ (with themselves + as the argument) to check if there are certain unit calibration + classes that they should apply to their outputs, if the user has + not otherwise specified a calibration class""" + if device.connection in ['channel 0', 'channel 1', 'channel 2', 'channel 3']: + # Default calibration classes for the non-static channels: + return NovaTechDDS9mFreqConversion, NovaTechDDS9mAmpConversion, None + else: + return None, None, None + + def quantise_freq(self, data, device): + """Provides bounds error checking and scales input values to instrument + units (0.1 Hz) before ensuring uint32 integer type.""" + if not isinstance(data, np.ndarray): + data = np.array(data) + # Ensure that frequencies are within bounds: + if np.any(data > self.dds_clock/2.) or np.any(data < 0.0): + raise LabscriptError('%s %s ' % (device.description, device.name) + + 'can only have frequencies between 0.0Hz and %f MHz, ' + + 'the limit imposed by %s.' % (self.name, self.dds_clock/2e6)) + scale_factor = self.clk_scale # Need to multiply by clk scale factor + + # It's faster to add 0.5 then typecast than to round to integers first: + data = np.array((scale_factor*data)+0.5,dtype=' 1 ) or np.any(data < 0): + raise LabscriptError('%s %s ' % (device.description, device.name) + + 'can only have amplitudes between 0 and 1 (Volts peak to peak approx), ' + + 'the limit imposed by %s.' % self.name) + # It's faster to add 0.5 then typecast than to round to integers first: + data = np.array((1023*data)+0.5,dtype=' 4032 - 2: # -2 to include space for dummy instructions + raise LabscriptError('%s can only support 4030 instructions. ' % self.name + + 'Please decrease the sample rates of devices on the same clock, ' + + 'or connect %s to a different pseudoclock.' % self.name) + try: + prefix, channel = output.connection.split() + channel = int(channel) + except: + raise LabscriptError('%s %s has invalid connection string: \'%s\'. ' % (output.description,output.name,str(output.connection)) + + 'Format must be \'channel n\' with n from 0 to 4.') + DDSs[channel] = output + + if not DDSs: + # if no channels are being used, no need to continue + return + + for connection in DDSs: + if connection in range(4): + dds = DDSs[connection] + dds.frequency.raw_output, dds.frequency.scale_factor = self.quantise_freq(dds.frequency.raw_output, dds) + dds.phase.raw_output, dds.phase.scale_factor = self.quantise_phase(dds.phase.raw_output, dds) + dds.amplitude.raw_output, dds.amplitude.scale_factor = self.quantise_amp(dds.amplitude.raw_output, dds) + else: + raise LabscriptError('%s %s has invalid connection string: \'%s\'. ' % (dds.description,dds.name,str(dds.connection)) + + 'Format must be \'channel n\' with n from 0 to 4.') + + dtypes = {'names':['%s%d' % (k, i) for i in DDSs for k in ['freq', 'amp', 'phase'] ], + 'formats':[f for i in DDSs for f in (' Date: Tue, 15 Apr 2025 14:03:43 -0400 Subject: [PATCH 02/22] updating worker functions to match current firmware --- .../AD9959DDSSweeper/__init__.py | 12 +++ .../AD9959DDSSweeper/blacs_tabs.py | 80 +++++++++---------- .../AD9959DDSSweeper/blacs_workers.py | 27 ++++--- .../AD9959DDSSweeper/labscript_devices.py | 2 + 4 files changed, 70 insertions(+), 51 deletions(-) create mode 100644 labscript_devices/AD9959DDSSweeper/__init__.py diff --git a/labscript_devices/AD9959DDSSweeper/__init__.py b/labscript_devices/AD9959DDSSweeper/__init__.py new file mode 100644 index 0000000..39fd099 --- /dev/null +++ b/labscript_devices/AD9959DDSSweeper/__init__.py @@ -0,0 +1,12 @@ +##################################################################### +# # +# /labscript_devices/AD9959DDSSweeper/blacs_tabs.py # +# # +# Copyright 2025, Carter Turnbaugh # +# # +# This file is part of the module labscript_devices, in the # +# labscript suite (see http://labscriptsuite.org), and is # +# licensed under the Simplified BSD License. See the license.txt # +# file in the root of the project for the full license. # +# # +##################################################################### \ No newline at end of file diff --git a/labscript_devices/AD9959DDSSweeper/blacs_tabs.py b/labscript_devices/AD9959DDSSweeper/blacs_tabs.py index 1810c07..e6b11e6 100644 --- a/labscript_devices/AD9959DDSSweeper/blacs_tabs.py +++ b/labscript_devices/AD9959DDSSweeper/blacs_tabs.py @@ -14,43 +14,43 @@ from blacs.device_base_class import DeviceTab class AD9959DDSSweeperTab(DeviceTab): - def initialise_GUI(self): - # Capabilities - self.base_units = {'freq':'Hz', 'amp':'Arb', 'phase':'Degrees'} - self.base_min = {'freq':0.0, 'amp':0, 'phase':0} - self.base_max = {'freq':250.0*10.0**6, 'amp':1, 'phase':360} - self.base_step = {'freq':10**6, 'amp':1/1023., 'phase':1} - self.base_decimals = {'freq':1, 'amp':4, 'phase':3} - self.num_DDS = 4 - - dds_prop = {} - for i in range(self.num_DDS): - dds_prop['channel %d' % i] = {} - for subchnl in ['freq', 'amp', 'phase']: - dds_prop['channel %d' % i][subchnl] = {'base_unit':self.base_units[subchnl], - 'min':self.base_min[subchnl], - 'max':self.base_max[subchnl], - 'step':self.base_step[subchnl], - 'decimals':self.base_decimals[subchnl] - } - - self.create_dds_outputs(dds_prop) - dds_widgets, _, _ = self.auto_create_widgets() - self.auto_place_widgets(('DDS Outputs', dds_widgets)) - - device = self.settings['connection_table'].find_by_name(self.device_name) - - self.com_port = device.properties['com_port'] - - self.supports_remote_value_check(False) - self.supports_smart_programming(True) - - def initialise_workers(self): - self.create_worker( - "main_worker", - "labscript_devices.AD9959DDSSweeper.blacs_workers.AD9959DDSSweeperWorker", - { - 'com_port': self.com_port, - }, - ) - self.primary_worker = "main_worker" + def initialise_GUI(self): + # Capabilities + self.base_units = {'freq':'Hz', 'amp':'Arb', 'phase':'Degrees'} + self.base_min = {'freq':0.0, 'amp':0, 'phase':0} + self.base_max = {'freq':250.0*10.0**6, 'amp':1, 'phase':360} + self.base_step = {'freq':10**6, 'amp':1/1023., 'phase':1} + self.base_decimals = {'freq':1, 'amp':4, 'phase':3} + self.num_DDS = 4 + + dds_prop = {} + for i in range(self.num_DDS): + dds_prop['channel %d' % i] = {} + for subchnl in ['freq', 'amp', 'phase']: + dds_prop['channel %d' % i][subchnl] = {'base_unit':self.base_units[subchnl], + 'min':self.base_min[subchnl], + 'max':self.base_max[subchnl], + 'step':self.base_step[subchnl], + 'decimals':self.base_decimals[subchnl] + } + + self.create_dds_outputs(dds_prop) + dds_widgets, _, _ = self.auto_create_widgets() + self.auto_place_widgets(('DDS Outputs', dds_widgets)) + + device = self.settings['connection_table'].find_by_name(self.device_name) + + self.com_port = device.properties['com_port'] + + self.supports_remote_value_check(False) + self.supports_smart_programming(True) + + def initialise_workers(self): + self.create_worker( + "main_worker", + "labscript_devices.AD9959DDSSweeper.blacs_workers.AD9959DDSSweeperWorker", + { + 'com_port': self.com_port, + }, + ) + self.primary_worker = "main_worker" diff --git a/labscript_devices/AD9959DDSSweeper/blacs_workers.py b/labscript_devices/AD9959DDSSweeper/blacs_workers.py index 70f262b..e6e8048 100644 --- a/labscript_devices/AD9959DDSSweeper/blacs_workers.py +++ b/labscript_devices/AD9959DDSSweeper/blacs_workers.py @@ -29,18 +29,17 @@ def __init__(self, com_port, ref_clock_frequency, pll_mult): self.conn.write(b'reset\n') self.assert_OK() - self.conn.write(b'setclock 0 %d\n' % ref_clock_frequency) - self.assert_OK() + self.conn.write(b'setclock 0 %d %d\n' % (ref_clock_frequency, pll_mult)) + # self.assert_OK() self.conn.write(b'mode 0 0\n') self.assert_OK() - self.conn.write(b'setmult %d\n' % pll_mult) self.assert_OK() self.conn.write(b'debug off\n') self.assert_OK() def assert_OK(self): resp = self.conn.readline().decode().strip() - assert resp == "ok", 'Exepcted "ok", received "%s"' % resp + assert resp == "ok", 'Expected "ok", received "%s"' % resp def get_version(self): '''Sends 'version' command, which retrieves the Pico firmware version. @@ -66,14 +65,20 @@ def get_status(self): Returns int status code.`''' self.conn.write(b'status\n') status_str = int(self.conn.readline().decode()) - if status_str == 0: - return 'STOPPED' - elif status_str == 1: - return 'RUNNING' - elif status_str == 2: - return 'ABORTING' + status_map = { + 0: 'STOPPED', + 1: 'TRANSITION_TO_RUNNING', + 2: 'RUNNING', + 3: 'ABORTING', + 4: 'ABORTED', + 5: 'TRANSITION_TO_STOPPED' + } + self.conn.write(b'status\n') + status_str = int(self.conn.readline().decode()) + if status_str in status_map: + return status_map[status_str] else: - raise LabscriptError(f'PrawnDO invalid status, returned {status_str}') + raise LabscriptError(f'Invalid status, returned {status_str}') def get_freqs(self): '''Responds with a dictionary containing diff --git a/labscript_devices/AD9959DDSSweeper/labscript_devices.py b/labscript_devices/AD9959DDSSweeper/labscript_devices.py index f16a0c8..66b519b 100644 --- a/labscript_devices/AD9959DDSSweeper/labscript_devices.py +++ b/labscript_devices/AD9959DDSSweeper/labscript_devices.py @@ -106,6 +106,8 @@ def generate_code(self, hdf5_file): DDSs = {} for output in self.child_devices: # Check that the instructions will fit into RAM: + + # TODO: Change number of instructions if isinstance(output, DDS) and len(output.frequency.raw_output) > 4032 - 2: # -2 to include space for dummy instructions raise LabscriptError('%s can only support 4030 instructions. ' % self.name + 'Please decrease the sample rates of devices on the same clock, ' + From 2ffb2ba24a00213fa73ff3341039d1891292ccda Mon Sep 17 00:00:00 2001 From: Jason Pruitt <55477390+Json-To-String@users.noreply.github.com> Date: Fri, 18 Apr 2025 16:42:18 -0400 Subject: [PATCH 03/22] added logic for max instructions per channel, getting pico board, and for user to specify mode. Currently defaults to single steps --- .../AD9959DDSSweeper/blacs_tabs.py | 80 +++++++++---------- .../AD9959DDSSweeper/blacs_workers.py | 32 +++++++- .../AD9959DDSSweeper/labscript_devices.py | 44 ++++++++-- 3 files changed, 106 insertions(+), 50 deletions(-) diff --git a/labscript_devices/AD9959DDSSweeper/blacs_tabs.py b/labscript_devices/AD9959DDSSweeper/blacs_tabs.py index e6b11e6..ee60034 100644 --- a/labscript_devices/AD9959DDSSweeper/blacs_tabs.py +++ b/labscript_devices/AD9959DDSSweeper/blacs_tabs.py @@ -14,43 +14,43 @@ from blacs.device_base_class import DeviceTab class AD9959DDSSweeperTab(DeviceTab): - def initialise_GUI(self): - # Capabilities - self.base_units = {'freq':'Hz', 'amp':'Arb', 'phase':'Degrees'} - self.base_min = {'freq':0.0, 'amp':0, 'phase':0} - self.base_max = {'freq':250.0*10.0**6, 'amp':1, 'phase':360} - self.base_step = {'freq':10**6, 'amp':1/1023., 'phase':1} - self.base_decimals = {'freq':1, 'amp':4, 'phase':3} - self.num_DDS = 4 - - dds_prop = {} - for i in range(self.num_DDS): - dds_prop['channel %d' % i] = {} - for subchnl in ['freq', 'amp', 'phase']: - dds_prop['channel %d' % i][subchnl] = {'base_unit':self.base_units[subchnl], - 'min':self.base_min[subchnl], - 'max':self.base_max[subchnl], - 'step':self.base_step[subchnl], - 'decimals':self.base_decimals[subchnl] - } - - self.create_dds_outputs(dds_prop) - dds_widgets, _, _ = self.auto_create_widgets() - self.auto_place_widgets(('DDS Outputs', dds_widgets)) - - device = self.settings['connection_table'].find_by_name(self.device_name) - - self.com_port = device.properties['com_port'] - - self.supports_remote_value_check(False) - self.supports_smart_programming(True) - - def initialise_workers(self): - self.create_worker( - "main_worker", - "labscript_devices.AD9959DDSSweeper.blacs_workers.AD9959DDSSweeperWorker", - { - 'com_port': self.com_port, - }, - ) - self.primary_worker = "main_worker" + def initialise_GUI(self): + # Capabilities + self.base_units = {'freq':'Hz', 'amp':'Arb', 'phase':'Degrees'} + self.base_min = {'freq':0.0, 'amp':0, 'phase':0} + self.base_max = {'freq':250.0*10.0**6, 'amp':1, 'phase':360} + self.base_step = {'freq':10**6, 'amp':1/1023., 'phase':1} + self.base_decimals = {'freq':1, 'amp':4, 'phase':3} + self.num_DDS = 4 + + dds_prop = {} + for i in range(self.num_DDS): + dds_prop['channel %d' % i] = {} + for subchnl in ['freq', 'amp', 'phase']: + dds_prop['channel %d' % i][subchnl] = {'base_unit':self.base_units[subchnl], + 'min':self.base_min[subchnl], + 'max':self.base_max[subchnl], + 'step':self.base_step[subchnl], + 'decimals':self.base_decimals[subchnl] + } + + self.create_dds_outputs(dds_prop) + dds_widgets, _, _ = self.auto_create_widgets() + self.auto_place_widgets(('DDS Outputs', dds_widgets)) + + device = self.settings['connection_table'].find_by_name(self.device_name) + + self.com_port = device.properties['com_port'] + + self.supports_remote_value_check(False) + self.supports_smart_programming(True) + + def initialise_workers(self): + self.create_worker( + "main_worker", + "labscript_devices.AD9959DDSSweeper.blacs_workers.AD9959DDSSweeperWorker", + { + 'com_port': self.com_port, + }, + ) + self.primary_worker = "main_worker" diff --git a/labscript_devices/AD9959DDSSweeper/blacs_workers.py b/labscript_devices/AD9959DDSSweeper/blacs_workers.py index e6e8048..28f9ad2 100644 --- a/labscript_devices/AD9959DDSSweeper/blacs_workers.py +++ b/labscript_devices/AD9959DDSSweeper/blacs_workers.py @@ -15,7 +15,14 @@ import labscript_utils.h5_lock, h5py class AD9959DDSSweeperInterface(object): - def __init__(self, com_port, ref_clock_frequency, pll_mult): + def __init__( + self, + com_port, + sweep_mode, + timing_mode, + ref_clock_frequency, + pll_mult + ): global serial; import serial self.timeout = 0.1 @@ -24,6 +31,9 @@ def __init__(self, com_port, ref_clock_frequency, pll_mult): version = self.get_version() print(f'Connected to version: {version}') + board = self.get_board() + print(f'Connected to board: {board}') + current_status = self.get_status() print(f'Current status is {current_status}') @@ -31,7 +41,7 @@ def __init__(self, com_port, ref_clock_frequency, pll_mult): self.assert_OK() self.conn.write(b'setclock 0 %d %d\n' % (ref_clock_frequency, pll_mult)) # self.assert_OK() - self.conn.write(b'mode 0 0\n') + self.conn.write(b'mode %d %d\n' % (sweep_mode, timing_mode)) self.assert_OK() self.assert_OK() self.conn.write(b'debug off\n') @@ -48,6 +58,9 @@ def get_version(self): version_str = self.conn.readline().decode() version = tuple(int(i) for i in version_str.split('.')) assert len(version) == 3 + + # may be better logic for semantic versioning w/o version pkg + assert version[1] >= 4, f'Version {version} too low' return version def abort(self): @@ -79,6 +92,12 @@ def get_status(self): return status_map[status_str] else: raise LabscriptError(f'Invalid status, returned {status_str}') + + def get_board(self): + '''Responds with pico board version.''' + self.conn.write(b'board\n') + resp = self.conn.readline().decode() + return(resp) def get_freqs(self): '''Responds with a dictionary containing @@ -133,8 +152,13 @@ def close(self): class AD9959DDSSweeperWorker(Worker): def init(self): - self.intf = AD9959DDSSweeperInterface(self.com_port, self.ref_clock_frequency, self.pll_mult) - + self.intf = AD9959DDSSweeperInterface( + self.com_port, + self.sweep_mode, + self.timing_mode, + self.ref_clock_frequency, + self.pll_mult + ) def program_manual(self, values): self.intf.abort() diff --git a/labscript_devices/AD9959DDSSweeper/labscript_devices.py b/labscript_devices/AD9959DDSSweeper/labscript_devices.py index 66b519b..96bba44 100644 --- a/labscript_devices/AD9959DDSSweeper/labscript_devices.py +++ b/labscript_devices/AD9959DDSSweeper/labscript_devices.py @@ -26,6 +26,8 @@ class AD9959DDSSweeper(IntermediateDevice): 'connection_table_properties': [ 'name', 'com_port', + 'sweep_mode', + 'timing_mode', 'ref_clock_frequency', 'pll_mult', ] @@ -33,11 +35,16 @@ class AD9959DDSSweeper(IntermediateDevice): ) def __init__(self, name, parent_device, com_port, + sweep_mode=0, timing_mode=0, ref_clock_frequency=125e6, pll_mult=4, **kwargs): '''Labscript device class for AD9959 eval board controlled by a Raspberry Pi Pico. ''' IntermediateDevice.__init__(self, name, parent_device, **kwargs) self.BLACS_connection = '%s' % com_port + + # store mode data + self.sweep_mode = sweep_mode + self.timing_mode = timing_mode # Check clocking if ref_clock_frequency * pll_mult > 500e6: @@ -103,15 +110,40 @@ def quantise_amp(self, data, device): return data, scale_factor def generate_code(self, hdf5_file): + + # external timing + max_instructions_map = { + 'pico1' : + { + 'steps' : [16656, 8615, 5810, 4383], + 'sweeps' : [8614, 4382, 2938, 2210] + }, + 'pico2' : + { + 'steps' : [34132, 17654, 11905, 8981], + 'sweeps' : [17654, 8981, 6022, 4529] + } + } + DDSs = {} + + num_channels = len(self.child_devices) + + # later we will need something better to support the other modes + if self.sweep_mode > 0: + mode = 'sweeps' + else: + mode = 'steps' + for output in self.child_devices: # Check that the instructions will fit into RAM: - - # TODO: Change number of instructions - if isinstance(output, DDS) and len(output.frequency.raw_output) > 4032 - 2: # -2 to include space for dummy instructions - raise LabscriptError('%s can only support 4030 instructions. ' % self.name + - 'Please decrease the sample rates of devices on the same clock, ' + - 'or connect %s to a different pseudoclock.' % self.name) + max_instructions = max_instructions_map['pico1'][mode][num_channels-1] + max_instructions -= 2 # -2 to include space for dummy instructions + if isinstance(output, DDS) and len(output.frequency.raw_output) > max_instructions: + raise LabscriptError( + f'{self.name} can only support {max_instructions} instructions. \ + Please decrease the sample rates of devices on the same clock, \ + or connect {self.name} to a different pseudoclock.') try: prefix, channel = output.connection.split() channel = int(channel) From 24e00ae4f2c48d9570a080746ab06230e60d1e55 Mon Sep 17 00:00:00 2001 From: Jason Pruitt <55477390+Json-To-String@users.noreply.github.com> Date: Mon, 21 Apr 2025 09:47:26 -0400 Subject: [PATCH 04/22] change assert_OK() calls in __init__ to match current firmware --- labscript_devices/AD9959DDSSweeper/blacs_workers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/labscript_devices/AD9959DDSSweeper/blacs_workers.py b/labscript_devices/AD9959DDSSweeper/blacs_workers.py index 28f9ad2..495ff68 100644 --- a/labscript_devices/AD9959DDSSweeper/blacs_workers.py +++ b/labscript_devices/AD9959DDSSweeper/blacs_workers.py @@ -40,9 +40,8 @@ def __init__( self.conn.write(b'reset\n') self.assert_OK() self.conn.write(b'setclock 0 %d %d\n' % (ref_clock_frequency, pll_mult)) - # self.assert_OK() - self.conn.write(b'mode %d %d\n' % (sweep_mode, timing_mode)) self.assert_OK() + self.conn.write(b'mode %d %d\n' % (sweep_mode, timing_mode)) self.assert_OK() self.conn.write(b'debug off\n') self.assert_OK() From 40487458d3cf832a2b84307403ab3b4fddc87e45 Mon Sep 17 00:00:00 2001 From: Jason Pruitt <55477390+Json-To-String@users.noreply.github.com> Date: Wed, 23 Apr 2025 10:05:45 -0400 Subject: [PATCH 05/22] Some initial docs additions --- docs/source/devices.rst | 1 + docs/source/devices/AD9959DDSSweeper.rst | 94 ++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 docs/source/devices/AD9959DDSSweeper.rst diff --git a/docs/source/devices.rst b/docs/source/devices.rst index 1b42ec0..1e4fb66 100644 --- a/docs/source/devices.rst +++ b/docs/source/devices.rst @@ -58,6 +58,7 @@ These devices cover various frequency sources that provide either hardware-timed .. toctree:: :maxdepth: 2 + devices/AD9959DDSSweeper devices/novatechDDS9m devices/phasematrixquicksyn diff --git a/docs/source/devices/AD9959DDSSweeper.rst b/docs/source/devices/AD9959DDSSweeper.rst new file mode 100644 index 0000000..817a076 --- /dev/null +++ b/docs/source/devices/AD9959DDSSweeper.rst @@ -0,0 +1,94 @@ +AD9959DDSSweeper +================ + + +Specifications +~~~~~~~~~~~~~~ + + +Installation +~~~~~~~~~~~~ + +Download the latest [dds-sweeper_rp2040.uf2 file for Pi Pico](https://github.com/QTC-UMD/dds-sweeper/releases/latest/download/dds-sweeper_rp2040.uf2) or [dds-sweeper_rp2350.uf2 file for Pi Pico 2](https://github.com/QTC-UMD/dds-sweeper/releases/latest/download/dds-sweeper_rp2350.uf2). +On your Raspberry Pi Pico, hold down the "bootsel" button while plugging the Pico into USB port on a PC (that must already be turned on). +The Pico should mount as a mass storage device (if it doesn't, try again or consult the Pico documentation). +Drag and drop the `.uf2` file into the mounted mass storage device. +The mass storage device should unmount after the copy completes. Your Pico is now running the DDS Sweeper firmware! + +Note that this device communicates using a virtual COM port. +The number is assigned by the controlling computer and will need to be determined in order for BLACS to connect to the PrawnDO. + + +Usage +~~~~~ + + +An example connection table that uses the PrawnBlaster and sweeper: + +.. code-block:: python + +from labscript import start, stop, add_time_marker, AnalogOut, DigitalOut, DDS +from labscript_devices.PrawnBlaster.labscript_devices import PrawnBlaster +from labscript_devices.AD9959DDSSweeper.labscript_devices import AD9959DDSSweeper + +# prawnblaster for external timing +prawn = PrawnBlaster( + name='prawn', + com_port='COM7', + num_pseudoclocks=1 + ) + +AD9959 = AD9959DDSSweeper( + name='AD9959', + parent_device=prawn.clocklines[0], + com_port='COM11', + ref_clock_frequency=125e6, + pll_mult=4 + ) + + +chann0 = DDS( 'chann0', AD9959, 'channel 0') +chann1 = DDS( 'chann1', AD9959, 'channel 1') +chann2 = DDS( 'chann2', AD9959, 'channel 2') +chann3 = DDS( 'chann3', AD9959, 'channel 3') + + +t = 0 +start() + +stop(t) + +.. note:: + +Detailed Documentation +~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: labscript_devices.AD9959DDSSweeper + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.AD9959DDSSweeper.labscript_devices + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.AD9959DDSSweeper.blacs_tabs + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.AD9959DDSSweeper.blacs_workers + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.AD9959DDSSweeper.runviewer_parsers + :members: + :undoc-members: + :show-inheritance: + :private-members: \ No newline at end of file From b02ec9ba52559faeee397c380f6c6e23e85b7bf0 Mon Sep 17 00:00:00 2001 From: Jason Pruitt <55477390+Json-To-String@users.noreply.github.com> Date: Wed, 23 Apr 2025 10:09:29 -0400 Subject: [PATCH 06/22] Indenting python block properly --- docs/source/devices/AD9959DDSSweeper.rst | 47 ++++++++++++------------ 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/docs/source/devices/AD9959DDSSweeper.rst b/docs/source/devices/AD9959DDSSweeper.rst index 817a076..0a4c003 100644 --- a/docs/source/devices/AD9959DDSSweeper.rst +++ b/docs/source/devices/AD9959DDSSweeper.rst @@ -27,36 +27,35 @@ An example connection table that uses the PrawnBlaster and sweeper: .. code-block:: python -from labscript import start, stop, add_time_marker, AnalogOut, DigitalOut, DDS -from labscript_devices.PrawnBlaster.labscript_devices import PrawnBlaster -from labscript_devices.AD9959DDSSweeper.labscript_devices import AD9959DDSSweeper - -# prawnblaster for external timing -prawn = PrawnBlaster( - name='prawn', - com_port='COM7', - num_pseudoclocks=1 - ) - -AD9959 = AD9959DDSSweeper( - name='AD9959', - parent_device=prawn.clocklines[0], - com_port='COM11', - ref_clock_frequency=125e6, - pll_mult=4 + from labscript import start, stop, add_time_marker, AnalogOut, DigitalOut, DDS + from labscript_devices.PrawnBlaster.labscript_devices import PrawnBlaster + from labscript_devices.AD9959DDSSweeper.labscript_devices import AD9959DDSSweeper + + # prawnblaster for external timing + prawn = PrawnBlaster( + name='prawn', + com_port='COM7', + num_pseudoclocks=1 ) + AD9959 = AD9959DDSSweeper( + name='AD9959', + parent_device=prawn.clocklines[0], + com_port='COM11', + ref_clock_frequency=125e6, + pll_mult=4 + ) -chann0 = DDS( 'chann0', AD9959, 'channel 0') -chann1 = DDS( 'chann1', AD9959, 'channel 1') -chann2 = DDS( 'chann2', AD9959, 'channel 2') -chann3 = DDS( 'chann3', AD9959, 'channel 3') + chann0 = DDS( 'chann0', AD9959, 'channel 0') + chann1 = DDS( 'chann1', AD9959, 'channel 1') + chann2 = DDS( 'chann2', AD9959, 'channel 2') + chann3 = DDS( 'chann3', AD9959, 'channel 3') -t = 0 -start() -stop(t) + start() + + stop(1) .. note:: From 71e107b36ab2d2329272166442ae9613dcdad3d7 Mon Sep 17 00:00:00 2001 From: Jason Pruitt <55477390+Json-To-String@users.noreply.github.com> Date: Wed, 23 Apr 2025 10:24:55 -0400 Subject: [PATCH 07/22] fix uf2 link rendering and renamed runviewer parser to plural --- docs/source/devices/AD9959DDSSweeper.rst | 7 ++++++- .../{runviewer_parser.py => runviewer_parsers.py} | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) rename labscript_devices/AD9959DDSSweeper/{runviewer_parser.py => runviewer_parsers.py} (97%) diff --git a/docs/source/devices/AD9959DDSSweeper.rst b/docs/source/devices/AD9959DDSSweeper.rst index 0a4c003..8c922b7 100644 --- a/docs/source/devices/AD9959DDSSweeper.rst +++ b/docs/source/devices/AD9959DDSSweeper.rst @@ -9,7 +9,12 @@ Specifications Installation ~~~~~~~~~~~~ -Download the latest [dds-sweeper_rp2040.uf2 file for Pi Pico](https://github.com/QTC-UMD/dds-sweeper/releases/latest/download/dds-sweeper_rp2040.uf2) or [dds-sweeper_rp2350.uf2 file for Pi Pico 2](https://github.com/QTC-UMD/dds-sweeper/releases/latest/download/dds-sweeper_rp2350.uf2). +- **For Pi Pico (RP2040)**: + `dds-sweeper_rp2040.uf2 `_ + +- **For Pi Pico 2 (RP2350)**: + `dds-sweeper_rp2350.uf2 `_ + On your Raspberry Pi Pico, hold down the "bootsel" button while plugging the Pico into USB port on a PC (that must already be turned on). The Pico should mount as a mass storage device (if it doesn't, try again or consult the Pico documentation). Drag and drop the `.uf2` file into the mounted mass storage device. diff --git a/labscript_devices/AD9959DDSSweeper/runviewer_parser.py b/labscript_devices/AD9959DDSSweeper/runviewer_parsers.py similarity index 97% rename from labscript_devices/AD9959DDSSweeper/runviewer_parser.py rename to labscript_devices/AD9959DDSSweeper/runviewer_parsers.py index 98d54dd..78e06aa 100644 --- a/labscript_devices/AD9959DDSSweeper/runviewer_parser.py +++ b/labscript_devices/AD9959DDSSweeper/runviewer_parsers.py @@ -1,6 +1,6 @@ ##################################################################### # # -# /labscript_devices/AD9959DDSSweeper/register_classes.py # +# /labscript_devices/AD9959DDSSweeper/runview_parsers.py # # # # Copyright 2025, Carter Turnbaugh # # # From 03352fd4cd259bd7376b006ceeefc8df8a0d1727 Mon Sep 17 00:00:00 2001 From: Jason Pruitt <55477390+Json-To-String@users.noreply.github.com> Date: Thu, 24 Apr 2025 14:53:22 -0400 Subject: [PATCH 08/22] adding support for simultaneous dynamic and static instructions --- docs/source/devices/AD9959DDSSweeper.rst | 6 +-- .../AD9959DDSSweeper/blacs_workers.py | 18 +++++++-- .../AD9959DDSSweeper/labscript_devices.py | 37 ++++++++++++++++++- 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/docs/source/devices/AD9959DDSSweeper.rst b/docs/source/devices/AD9959DDSSweeper.rst index 8c922b7..33ce044 100644 --- a/docs/source/devices/AD9959DDSSweeper.rst +++ b/docs/source/devices/AD9959DDSSweeper.rst @@ -32,7 +32,7 @@ An example connection table that uses the PrawnBlaster and sweeper: .. code-block:: python - from labscript import start, stop, add_time_marker, AnalogOut, DigitalOut, DDS + from labscript import start, stop, add_time_marker, AnalogOut, DigitalOut, DDS, StaticDDS from labscript_devices.PrawnBlaster.labscript_devices import PrawnBlaster from labscript_devices.AD9959DDSSweeper.labscript_devices import AD9959DDSSweeper @@ -53,8 +53,8 @@ An example connection table that uses the PrawnBlaster and sweeper: chann0 = DDS( 'chann0', AD9959, 'channel 0') - chann1 = DDS( 'chann1', AD9959, 'channel 1') - chann2 = DDS( 'chann2', AD9959, 'channel 2') + chann1 = StaticDDS( 'chann1', AD9959, 'channel 1') + #chann2 = DDS( 'chann2', AD9959, 'channel 2') chann3 = DDS( 'chann3', AD9959, 'channel 3') diff --git a/labscript_devices/AD9959DDSSweeper/blacs_workers.py b/labscript_devices/AD9959DDSSweeper/blacs_workers.py index 495ff68..9b49543 100644 --- a/labscript_devices/AD9959DDSSweeper/blacs_workers.py +++ b/labscript_devices/AD9959DDSSweeper/blacs_workers.py @@ -172,12 +172,24 @@ def transition_to_buffered(self, device_name, h5file, initial_values, fresh): group = hdf5_file['devices'][device_name] dds_data = group['dds_data'] + if 'static_data' in group: + stat_data = group['static_data'] + stat_chans = set([int(n[4:]) for n in stat_data.dtype.names if n.startswith('freq')]) + if len(dds_data) == 0: # Don't bother transitioning to buffered if no data return {} - - channels = set([int(n[4:]) for n in dds_data.dtype.names if n.startswith('freq')]) - self.intf.set_channels(max(channels) + 1) + + if len(stat_data) > 0: + stat_array = stat_data[()] + for chan in sorted(stat_chans): + freq = stat_array[f'freq{chan}'] + amp = stat_array[f'amp{chan}'] + phase = stat_array[f'phase{chan}'] + self.intf.set_output(chan, freq, amp, phase) + + dyn_chans = set([int(n[4:]) for n in dds_data.dtype.names if n.startswith('freq')]) + self.intf.set_channels(len(dyn_chans)) self.intf.set_batch(dds_data[()]) self.intf.stop(len(dds_data[()])) diff --git a/labscript_devices/AD9959DDSSweeper/labscript_devices.py b/labscript_devices/AD9959DDSSweeper/labscript_devices.py index 96bba44..bd95498 100644 --- a/labscript_devices/AD9959DDSSweeper/labscript_devices.py +++ b/labscript_devices/AD9959DDSSweeper/labscript_devices.py @@ -126,7 +126,7 @@ def generate_code(self, hdf5_file): } DDSs = {} - + stat_DDSs = {} num_channels = len(self.child_devices) # later we will need something better to support the other modes @@ -150,7 +150,12 @@ def generate_code(self, hdf5_file): except: raise LabscriptError('%s %s has invalid connection string: \'%s\'. ' % (output.description,output.name,str(output.connection)) + 'Format must be \'channel n\' with n from 0 to 4.') - DDSs[channel] = output + + # separate dynamic from static + if isinstance(output, DDS): + DDSs[channel] = output + elif isinstance(output, StaticDDS): + stat_DDSs[channel] = output if not DDSs: # if no channels are being used, no need to continue @@ -180,9 +185,37 @@ def generate_code(self, hdf5_file): out_table['amp%d' % i][:] = dds.amplitude.raw_output out_table['phase%d' % i][:] = dds.phase.raw_output + if stat_DDSs: + # conversion to AD9959 units + for connection in stat_DDSs: + if connection in range(4): + dds = stat_DDSs[connection] + dds.frequency.raw_output, dds.frequency.scale_factor = self.quantise_freq(dds.frequency.raw_output, dds) + dds.phase.raw_output, dds.phase.scale_factor = self.quantise_phase(dds.phase.raw_output, dds) + dds.amplitude.raw_output, dds.amplitude.scale_factor = self.quantise_amp(dds.amplitude.raw_output, dds) + else: + raise LabscriptError('%s %s has invalid connection string: \'%s\'. ' % (dds.description,dds.name,str(dds.connection)) + + 'Format must be \'channel n\' with n from 0 to 4.') + + static_dtypes = { + 'names':['%s%d' % (k, i) for i in stat_DDSs for k in ['freq', 'amp', 'phase'] ], + 'formats':[f for i in stat_DDSs for f in (' Date: Sun, 27 Apr 2025 12:41:12 -0400 Subject: [PATCH 09/22] Add check that device is ready for the number of bytes we will send in binary mode. At the moment, if it fails it will simply send all zeros and throw an error. I think this is the best option, as it prevents the device from getting locked up waiting for bytes (if we restart the blacs tab). --- labscript_devices/AD9959DDSSweeper/blacs_workers.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/labscript_devices/AD9959DDSSweeper/blacs_workers.py b/labscript_devices/AD9959DDSSweeper/blacs_workers.py index 9b49543..ed08248 100644 --- a/labscript_devices/AD9959DDSSweeper/blacs_workers.py +++ b/labscript_devices/AD9959DDSSweeper/blacs_workers.py @@ -139,6 +139,11 @@ def set_batch(self, table): if not resp.startswith('ready'): resp += ''.join([r.decode() for r in self.conn.readlines()]) raise LabscriptError(f'setb command failed, got response {repr(resp)}') + ready_for_bytes = int(resp[len('ready for '):-len(' bytes\n')]) + if ready_for_bytes != len(table.tobytes()): + self.conn.write(b'\0'*ready_for_bytes) + self.assert_OK() + raise LabscriptError(f'Device expected {ready_for_bytes}, but we only had {len(table.tobytes())}. Device mode likely incorrect.' self.conn.write(table.tobytes()) self.assert_OK() From 4387a42b6fa5ddd16a32e1adc73d104685d8a752 Mon Sep 17 00:00:00 2001 From: Carter Turn Date: Sun, 27 Apr 2025 13:16:39 -0400 Subject: [PATCH 10/22] Add overview and specifications for docs. --- docs/source/devices/AD9959DDSSweeper.rst | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/source/devices/AD9959DDSSweeper.rst b/docs/source/devices/AD9959DDSSweeper.rst index 33ce044..f170823 100644 --- a/docs/source/devices/AD9959DDSSweeper.rst +++ b/docs/source/devices/AD9959DDSSweeper.rst @@ -1,10 +1,27 @@ AD9959DDSSweeper ================ +This labscript device controls the `DDSSweeper `_, an interface to the `AD9959 eval board `_ four channel direct digital synthesizer (DDS) using the `Raspberry Pi Pico `_ platform. Specifications ~~~~~~~~~~~~~~ +The AD9959 evaluation board provides the following: + +* 4 DDS channels + + - 100 kHz - 250 MHz output frequency with 32 bit frequency resolution (~0.1 Hz) + - Up to 0 dBm output power with 10 bit amplitude resolution + - Phase control with 16 bit resolution (~48 uRad) + +The Pico interface allows the evaluation board parameters to be reprogrammed during a sequence. +At this time, stepping of frequency, amplitude, and phase parameters is supported. +Parameter ramping is possible, but not currently supported by the labscript device (if support for this is of interest, please `open an issue `). +The Pico interface provides the following: + +* 16,656 instructions distributed evenly among the configured channels; 16,656, 8,615, 5,810, and 4,383 for 1, 2, 3, 4 channels respectively. +* External timing via a pseudoclock clockline. Interal timing is also possible, but not currently supported (if this is of interest, please `open an issue `). +* The Pi Pico can be used as a (low quality) clock for the AD9959 evaluation board. Installation ~~~~~~~~~~~~ @@ -95,4 +112,4 @@ Detailed Documentation :members: :undoc-members: :show-inheritance: - :private-members: \ No newline at end of file + :private-members: From 0595dd41c5258b466ea65f0784c3b338d0e8e076 Mon Sep 17 00:00:00 2001 From: Carter Turn Date: Sun, 27 Apr 2025 13:57:18 -0400 Subject: [PATCH 11/22] Add option for external reference clock --- labscript_devices/AD9959DDSSweeper/blacs_workers.py | 3 ++- labscript_devices/AD9959DDSSweeper/labscript_devices.py | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/labscript_devices/AD9959DDSSweeper/blacs_workers.py b/labscript_devices/AD9959DDSSweeper/blacs_workers.py index ed08248..7efd632 100644 --- a/labscript_devices/AD9959DDSSweeper/blacs_workers.py +++ b/labscript_devices/AD9959DDSSweeper/blacs_workers.py @@ -20,6 +20,7 @@ def __init__( com_port, sweep_mode, timing_mode, + ref_clock_external, ref_clock_frequency, pll_mult ): @@ -39,7 +40,7 @@ def __init__( self.conn.write(b'reset\n') self.assert_OK() - self.conn.write(b'setclock 0 %d %d\n' % (ref_clock_frequency, pll_mult)) + self.conn.write(b'setclock %d %d %d\n' % (ref_clock_external, ref_clock_frequency, pll_mult)) self.assert_OK() self.conn.write(b'mode %d %d\n' % (sweep_mode, timing_mode)) self.assert_OK() diff --git a/labscript_devices/AD9959DDSSweeper/labscript_devices.py b/labscript_devices/AD9959DDSSweeper/labscript_devices.py index bd95498..4e08d5e 100644 --- a/labscript_devices/AD9959DDSSweeper/labscript_devices.py +++ b/labscript_devices/AD9959DDSSweeper/labscript_devices.py @@ -28,6 +28,7 @@ class AD9959DDSSweeper(IntermediateDevice): 'com_port', 'sweep_mode', 'timing_mode', + 'ref_clock_external', 'ref_clock_frequency', 'pll_mult', ] @@ -36,8 +37,9 @@ class AD9959DDSSweeper(IntermediateDevice): def __init__(self, name, parent_device, com_port, sweep_mode=0, timing_mode=0, - ref_clock_frequency=125e6, pll_mult=4, **kwargs): - '''Labscript device class for AD9959 eval board controlled by a Raspberry Pi Pico. + ref_clock_external=0, ref_clock_frequency=125e6, pll_mult=4, **kwargs): + '''Labscript device class for AD9959 eval board controlled by a Raspberry Pi Pico running the DDS Sweeper firmware (https://github.com/QTC-UMD/dds-sweeper). + ''' IntermediateDevice.__init__(self, name, parent_device, **kwargs) self.BLACS_connection = '%s' % com_port @@ -51,6 +53,8 @@ def __init__(self, name, parent_device, com_port, raise ValueError('DDS system clock frequency must be less than 500 MHz') elif pll_mult > 1 and ref_clock_frequency * pll_mult < 100e6: raise ValueError('DDS system clock frequency must be greater than 100 MHz when using PLL') + elif not ref_clock_external and ref_clock_frequency > 133e6: + raise ValueError('ref_clock_frequency must be less than 133 MHz when clock is provided by Pi Pico') self.dds_clock = ref_clock_frequency * pll_mult self.clk_scale = 2**32 / self.dds_clock From 68c25c5d8332925ee80540dcc90ceeb899deeda1 Mon Sep 17 00:00:00 2001 From: Carter Turn Date: Sun, 27 Apr 2025 13:57:31 -0400 Subject: [PATCH 12/22] Expand docstring for labscript device --- .../AD9959DDSSweeper/labscript_devices.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/labscript_devices/AD9959DDSSweeper/labscript_devices.py b/labscript_devices/AD9959DDSSweeper/labscript_devices.py index 4e08d5e..9d2c31e 100644 --- a/labscript_devices/AD9959DDSSweeper/labscript_devices.py +++ b/labscript_devices/AD9959DDSSweeper/labscript_devices.py @@ -40,6 +40,23 @@ def __init__(self, name, parent_device, com_port, ref_clock_external=0, ref_clock_frequency=125e6, pll_mult=4, **kwargs): '''Labscript device class for AD9959 eval board controlled by a Raspberry Pi Pico running the DDS Sweeper firmware (https://github.com/QTC-UMD/dds-sweeper). + This labscript device provides up to four channels of DDS outputs. It is designed to be connected to a pseudoclock clockline. + + Args: + name (str): python variable name to assign to the AD9959DDSSweeper + parent_device (:class:`~.ClockLine`): + Pseudoclock clockline used to clock DDS parameter changes. + com_port (str): COM port assigned to the AD9959DDSSweeper by the OS. + On Windows, takes the form of `COMd` where `d` is an integer. + sweep_mode (int): + The DDS Sweeper firmware can set the DDS outputs in either fixed steps or sweeps of the amplitude, frequency, or phase. + At this time, only steps are supported, so sweep_mode must be 0. + timing_mode (int): + The DDS Sweeper firmware can determine its own update times internally (in which case it only requires a starting trigger). + At this time, interal timing is not supported, so timing_mode must be 0. + ref_clock_external (int): Set to 0 to have Pi Pico provide the reference clock to the AD9959 eval board. Set to 1 for another source of reference clock for the AD9959 eval board. + ref_clock_frequency (float): Frequency of the reference clock. If ref_clock_external is 0, the Pi Pico system clock will be set to this frequency. If the PLL is used, ref_clock_frequency * pll_mult must be between 100 MHz and 500 MHz. If the PLL is not used, ref_clock_frequency must be less than 500 MHz. + pll_mult: the AD9959 has a PLL to multiply the reference clock frequency. Allowed values are 1 or 4-20. ''' IntermediateDevice.__init__(self, name, parent_device, **kwargs) self.BLACS_connection = '%s' % com_port From 9469d0b898f63e8cdce2cdf80d899da4c1ab724c Mon Sep 17 00:00:00 2001 From: Jason Pruitt <55477390+Json-To-String@users.noreply.github.com> Date: Mon, 28 Apr 2025 10:41:28 -0400 Subject: [PATCH 13/22] removing internal timing, added a parentheses to table size check --- labscript_devices/AD9959DDSSweeper/blacs_workers.py | 8 ++++---- labscript_devices/AD9959DDSSweeper/labscript_devices.py | 7 +------ 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/labscript_devices/AD9959DDSSweeper/blacs_workers.py b/labscript_devices/AD9959DDSSweeper/blacs_workers.py index 7efd632..3956197 100644 --- a/labscript_devices/AD9959DDSSweeper/blacs_workers.py +++ b/labscript_devices/AD9959DDSSweeper/blacs_workers.py @@ -19,7 +19,6 @@ def __init__( self, com_port, sweep_mode, - timing_mode, ref_clock_external, ref_clock_frequency, pll_mult @@ -42,7 +41,7 @@ def __init__( self.assert_OK() self.conn.write(b'setclock %d %d %d\n' % (ref_clock_external, ref_clock_frequency, pll_mult)) self.assert_OK() - self.conn.write(b'mode %d %d\n' % (sweep_mode, timing_mode)) + self.conn.write(b'mode %d 0\n' % sweep_mode) self.assert_OK() self.conn.write(b'debug off\n') self.assert_OK() @@ -144,7 +143,7 @@ def set_batch(self, table): if ready_for_bytes != len(table.tobytes()): self.conn.write(b'\0'*ready_for_bytes) self.assert_OK() - raise LabscriptError(f'Device expected {ready_for_bytes}, but we only had {len(table.tobytes())}. Device mode likely incorrect.' + raise LabscriptError(f'Device expected {ready_for_bytes}, but we only had {len(table.tobytes())}. Device mode likely incorrect.') self.conn.write(table.tobytes()) self.assert_OK() @@ -160,10 +159,11 @@ def init(self): self.intf = AD9959DDSSweeperInterface( self.com_port, self.sweep_mode, - self.timing_mode, + self.ref_clock_external, self.ref_clock_frequency, self.pll_mult ) + def program_manual(self, values): self.intf.abort() diff --git a/labscript_devices/AD9959DDSSweeper/labscript_devices.py b/labscript_devices/AD9959DDSSweeper/labscript_devices.py index 9d2c31e..93e5ac9 100644 --- a/labscript_devices/AD9959DDSSweeper/labscript_devices.py +++ b/labscript_devices/AD9959DDSSweeper/labscript_devices.py @@ -27,7 +27,6 @@ class AD9959DDSSweeper(IntermediateDevice): 'name', 'com_port', 'sweep_mode', - 'timing_mode', 'ref_clock_external', 'ref_clock_frequency', 'pll_mult', @@ -36,7 +35,7 @@ class AD9959DDSSweeper(IntermediateDevice): ) def __init__(self, name, parent_device, com_port, - sweep_mode=0, timing_mode=0, + sweep_mode=0, ref_clock_external=0, ref_clock_frequency=125e6, pll_mult=4, **kwargs): '''Labscript device class for AD9959 eval board controlled by a Raspberry Pi Pico running the DDS Sweeper firmware (https://github.com/QTC-UMD/dds-sweeper). @@ -51,9 +50,6 @@ def __init__(self, name, parent_device, com_port, sweep_mode (int): The DDS Sweeper firmware can set the DDS outputs in either fixed steps or sweeps of the amplitude, frequency, or phase. At this time, only steps are supported, so sweep_mode must be 0. - timing_mode (int): - The DDS Sweeper firmware can determine its own update times internally (in which case it only requires a starting trigger). - At this time, interal timing is not supported, so timing_mode must be 0. ref_clock_external (int): Set to 0 to have Pi Pico provide the reference clock to the AD9959 eval board. Set to 1 for another source of reference clock for the AD9959 eval board. ref_clock_frequency (float): Frequency of the reference clock. If ref_clock_external is 0, the Pi Pico system clock will be set to this frequency. If the PLL is used, ref_clock_frequency * pll_mult must be between 100 MHz and 500 MHz. If the PLL is not used, ref_clock_frequency must be less than 500 MHz. pll_mult: the AD9959 has a PLL to multiply the reference clock frequency. Allowed values are 1 or 4-20. @@ -63,7 +59,6 @@ def __init__(self, name, parent_device, com_port, # store mode data self.sweep_mode = sweep_mode - self.timing_mode = timing_mode # Check clocking if ref_clock_frequency * pll_mult > 500e6: From 7b2081183ad074f45fd9782efa755fb8a0530514 Mon Sep 17 00:00:00 2001 From: Jason Pruitt <55477390+Json-To-String@users.noreply.github.com> Date: Mon, 28 Apr 2025 10:43:53 -0400 Subject: [PATCH 14/22] pluralism --- labscript_devices/AD9959DDSSweeper/register_classes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/labscript_devices/AD9959DDSSweeper/register_classes.py b/labscript_devices/AD9959DDSSweeper/register_classes.py index fc06824..6a53052 100644 --- a/labscript_devices/AD9959DDSSweeper/register_classes.py +++ b/labscript_devices/AD9959DDSSweeper/register_classes.py @@ -16,5 +16,5 @@ register_classes( 'AD9959DDSSweeper', BLACS_tab='labscript_devices.AD9959DDSSweeper.blacs_tabs.AD9959DDSSweeperTab', - runviewer_parser='labscript_devices.AD9959DDSSweeper.runviewer_parser.AD9959DDSSweeperParser', + runviewer_parser='labscript_devices.AD9959DDSSweeper.runviewer_parsers.AD9959DDSSweeperParser', ) From 1f6bc6ba519a072d637dd15fce480ac295c96b7e Mon Sep 17 00:00:00 2001 From: Carter Turn Date: Mon, 28 Apr 2025 23:37:26 -0400 Subject: [PATCH 15/22] Some documentation and docstring updates --- docs/source/devices/AD9959DDSSweeper.rst | 23 ++++--- .../AD9959DDSSweeper/blacs_workers.py | 64 +++++++++++++++++-- 2 files changed, 72 insertions(+), 15 deletions(-) diff --git a/docs/source/devices/AD9959DDSSweeper.rst b/docs/source/devices/AD9959DDSSweeper.rst index f170823..ddb70ac 100644 --- a/docs/source/devices/AD9959DDSSweeper.rst +++ b/docs/source/devices/AD9959DDSSweeper.rst @@ -3,6 +3,8 @@ AD9959DDSSweeper This labscript device controls the `DDSSweeper `_, an interface to the `AD9959 eval board `_ four channel direct digital synthesizer (DDS) using the `Raspberry Pi Pico `_ platform. +The DDS Sweeper is described in more detail in E. Huegler, J. C. Hill, and D. H. Meyer, An agile radio-frequency source using internal sweeps of a direct digital synthesizer, *Review of Scientific Instruments*, **94**, 094705 (2023) https://doi.org/10.1063/5.0163342 . + Specifications ~~~~~~~~~~~~~~ @@ -20,8 +22,8 @@ Parameter ramping is possible, but not currently supported by the labscript devi The Pico interface provides the following: * 16,656 instructions distributed evenly among the configured channels; 16,656, 8,615, 5,810, and 4,383 for 1, 2, 3, 4 channels respectively. -* External timing via a pseudoclock clockline. Interal timing is also possible, but not currently supported (if this is of interest, please `open an issue `). -* The Pi Pico can be used as a (low quality) clock for the AD9959 evaluation board. +* External timing via a pseudoclock clockline. +* By default, the AD9959 system reference clock is taken from the Pi Pico. If a higher quality clock is needed, the user can provide an external system reference clock to the AD9959. For more details on clocking, see the Usage section. Installation ~~~~~~~~~~~~ @@ -45,7 +47,7 @@ Usage ~~~~~ -An example connection table that uses the PrawnBlaster and sweeper: +An example connection table that uses the PrawnBlaster and sweeper with an external, 100 MHz clock: .. code-block:: python @@ -64,15 +66,16 @@ An example connection table that uses the PrawnBlaster and sweeper: name='AD9959', parent_device=prawn.clocklines[0], com_port='COM11', - ref_clock_frequency=125e6, - pll_mult=4 + ref_clock_external=1, + ref_clock_frequency=100e6, + pll_mult=5 ) chann0 = DDS( 'chann0', AD9959, 'channel 0') - chann1 = StaticDDS( 'chann1', AD9959, 'channel 1') - #chann2 = DDS( 'chann2', AD9959, 'channel 2') - chann3 = DDS( 'chann3', AD9959, 'channel 3') + chann1 = DDS( 'chann1', AD9959, 'channel 1') + chann2 = DDS( 'chann2', AD9959, 'channel 2') + chann3 = StaticDDS( 'chann3', AD9959, 'channel 3') start() @@ -81,6 +84,10 @@ An example connection table that uses the PrawnBlaster and sweeper: .. note:: +**Clocking** + +If the Pi Pico is used as the AD9959 system reference clock, pin 21 of the Pi Pico should be connected to the REF CLK input (J9) of the AD9959 eval board. Otherwise, another clock source should be connected to REF CLK input and its frequency provided as the ref_clock_frequency. + Detailed Documentation ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/labscript_devices/AD9959DDSSweeper/blacs_workers.py b/labscript_devices/AD9959DDSSweeper/blacs_workers.py index 3956197..8f43208 100644 --- a/labscript_devices/AD9959DDSSweeper/blacs_workers.py +++ b/labscript_devices/AD9959DDSSweeper/blacs_workers.py @@ -23,6 +23,21 @@ def __init__( ref_clock_frequency, pll_mult ): + '''Initializes serial communication and performs initial setup. + + Initial setup consists of checking the version, board, and status. + The DDS Sweeper is then reset, after which the clock, mode, and debug mode are configured. + + Args: + com_port (str): COM port assigned to the DDS Sweeper by the OS. + On Windows, takes the form of `COMd` where `d` is an integer. + sweep_mode (int): + The DDS Sweeper firmware can set the DDS outputs in either fixed steps or sweeps of the amplitude, frequency, or phase. + At this time, only steps are supported, so sweep_mode must be 0. + ref_clock_external (int): Set to 0 to have Pi Pico provide the reference clock to the AD9959 eval board. Set to 1 for another source of reference clock for the AD9959 eval board. + ref_clock_frequency (float): Frequency of the reference clock. If ref_clock_external is 0, the Pi Pico system clock will be set to this frequency. If the PLL is used, ref_clock_frequency * pll_mult must be between 100 MHz and 500 MHz. If the PLL is not used, ref_clock_frequency must be less than 500 MHz. + pll_mult: the AD9959 has a PLL to multiply the reference clock frequency. Allowed values are 1 or 4-20. + ''' global serial; import serial self.timeout = 0.1 @@ -47,12 +62,14 @@ def __init__( self.assert_OK() def assert_OK(self): + '''Read a response from the DDS Sweeper, assert that that response is "ok", the standard response to a successful command.''' resp = self.conn.readline().decode().strip() assert resp == "ok", 'Expected "ok", received "%s"' % resp def get_version(self): '''Sends 'version' command, which retrieves the Pico firmware version. - Returns response, throws serial exception on disconnect.''' + + Returns: (int, int, int): Tuple representing semantic version number.''' self.conn.write(b'version\n') version_str = self.conn.readline().decode() version = tuple(int(i) for i in version_str.split('.')) @@ -73,8 +90,16 @@ def start(self): self.assert_OK() def get_status(self): - '''Reads the status of the AD9959 DDS Sweeper - Returns int status code.`''' + '''Reads the status of the AD9959 DDS Sweeper. + + Returns: + (str): Status in string representation. Accepted values are + STOPPED: manual mode + TRANSITION_TO_RUNNING: transitioning to buffered execution + RUNNING: buffered execution + ABORTING: aborting buffered execution + ABORTED: last buffered execution was aborted + TRANSITION_TO_STOPPED: transitioning to manual mode''' self.conn.write(b'status\n') status_str = int(self.conn.readline().decode()) status_map = { @@ -93,14 +118,21 @@ def get_status(self): raise LabscriptError(f'Invalid status, returned {status_str}') def get_board(self): - '''Responds with pico board version.''' + '''Responds with pico board version. + + Returns: + (str): Either "pico1" for a Pi Pico 1 board or "pico2" for a Pi Pico 2 board.''' self.conn.write(b'board\n') resp = self.conn.readline().decode() return(resp) def get_freqs(self): '''Responds with a dictionary containing - the current operating frequencies (in kHz) of various clocks.''' + the current operating frequencies (in kHz) of various clocks. + + Returns: + (str): Multi-line string containing clock frequencies in kHz. + Intended to be human readable, potentially difficult to parse automatically.''' self.conn.write(b'getfreqs\n') freqs = {} while True: @@ -125,9 +157,22 @@ def set_channels(self, channels): self.conn.write(b'setchannels %d\n' % channels) self.assert_OK() - def set(self, channel, addr, frequency, amplitude, phase): + def seti(self, channel, addr, frequency, amplitude, phase): '''Set frequency, phase, and amplitude of a channel - for address addr in buffered sequence from integer values.''' + for address addr in buffered sequence from integer values. + + Args: + channel (int): channel to set the instruction for. Zero indexed. + addr (int): address of the instruction to set. Zero indexed. + frequency (unsigned 32 bit int): + frequency to jump to when this instruction runs. + In DDS units: ref_clock_frequency * pll_mult / 2^32 * frequency. + amplitude (unsigned 10 bit int): + amplitude to jump to when this instruction runs. + In DDS units: amplitude / 1023 fraction of maximum output amplitude. + phase (unsigned 14 bit int): + phase to jump to when this instruction runs. + In DDS units: 360 * phase / 2^14 degrees.''' self.conn.write(b'seti %d %d %f %f %f\n' % (channel, addr, frequency, amplitude, phase)) self.assert_OK() @@ -148,10 +193,15 @@ def set_batch(self, table): self.assert_OK() def stop(self, count): + '''Set the stop instruction for a buffered sequence. + + Args: + count (int): number of instructions to run in the buffered sequence.''' self.conn.write(b'set 4 %d\n' % count) self.assert_OK() def close(self): + '''Closes the serial connection.''' self.conn.close() class AD9959DDSSweeperWorker(Worker): From fb5acdaee1004428f87b8905f4d51fcefe49e104 Mon Sep 17 00:00:00 2001 From: Jason Pruitt <55477390+Json-To-String@users.noreply.github.com> Date: Tue, 29 Apr 2025 09:42:26 -0400 Subject: [PATCH 16/22] min version and status map update --- .../AD9959DDSSweeper/blacs_workers.py | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/labscript_devices/AD9959DDSSweeper/blacs_workers.py b/labscript_devices/AD9959DDSSweeper/blacs_workers.py index 8f43208..eca0c93 100644 --- a/labscript_devices/AD9959DDSSweeper/blacs_workers.py +++ b/labscript_devices/AD9959DDSSweeper/blacs_workers.py @@ -43,6 +43,16 @@ def __init__( self.timeout = 0.1 self.conn = serial.Serial(com_port, 10000000, timeout=self.timeout) + self.min_ver = (0, 4, 0) + self.status_map = { + 0: 'STOPPED', + 1: 'TRANSITION_TO_RUNNING', + 2: 'RUNNING', + 3: 'ABORTING', + 4: 'ABORTED', + 5: 'TRANSITION_TO_STOPPED' + } + version = self.get_version() print(f'Connected to version: {version}') @@ -73,10 +83,8 @@ def get_version(self): self.conn.write(b'version\n') version_str = self.conn.readline().decode() version = tuple(int(i) for i in version_str.split('.')) - assert len(version) == 3 - # may be better logic for semantic versioning w/o version pkg - assert version[1] >= 4, f'Version {version} too low' + assert version >= self.min_ver, f'Version {version} too low' return version def abort(self): @@ -100,20 +108,12 @@ def get_status(self): ABORTING: aborting buffered execution ABORTED: last buffered execution was aborted TRANSITION_TO_STOPPED: transitioning to manual mode''' + self.conn.write(b'status\n') - status_str = int(self.conn.readline().decode()) - status_map = { - 0: 'STOPPED', - 1: 'TRANSITION_TO_RUNNING', - 2: 'RUNNING', - 3: 'ABORTING', - 4: 'ABORTED', - 5: 'TRANSITION_TO_STOPPED' - } - self.conn.write(b'status\n') - status_str = int(self.conn.readline().decode()) - if status_str in status_map: - return status_map[status_str] + status_str = self.conn.readlines().decode() + status_int = int(status_str) + if status_int in self.status_map: + return self.status_map[status_int] else: raise LabscriptError(f'Invalid status, returned {status_str}') From a8adf76da5201211482a4410f4a9007d8573420d Mon Sep 17 00:00:00 2001 From: Jason Pruitt <55477390+Json-To-String@users.noreply.github.com> Date: Tue, 29 Apr 2025 09:54:01 -0400 Subject: [PATCH 17/22] no s in readline --- labscript_devices/AD9959DDSSweeper/blacs_workers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/labscript_devices/AD9959DDSSweeper/blacs_workers.py b/labscript_devices/AD9959DDSSweeper/blacs_workers.py index eca0c93..a13465f 100644 --- a/labscript_devices/AD9959DDSSweeper/blacs_workers.py +++ b/labscript_devices/AD9959DDSSweeper/blacs_workers.py @@ -110,7 +110,7 @@ def get_status(self): TRANSITION_TO_STOPPED: transitioning to manual mode''' self.conn.write(b'status\n') - status_str = self.conn.readlines().decode() + status_str = self.conn.readline().decode() status_int = int(status_str) if status_int in self.status_map: return self.status_map[status_int] From 43b482113082a86158209596f277e18f81bd5e7f Mon Sep 17 00:00:00 2001 From: Carter Turn Date: Tue, 29 Apr 2025 16:59:43 -0400 Subject: [PATCH 18/22] Add Pico2 instruction counts to docs. --- docs/source/devices/AD9959DDSSweeper.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/devices/AD9959DDSSweeper.rst b/docs/source/devices/AD9959DDSSweeper.rst index ddb70ac..0e65b61 100644 --- a/docs/source/devices/AD9959DDSSweeper.rst +++ b/docs/source/devices/AD9959DDSSweeper.rst @@ -21,7 +21,8 @@ At this time, stepping of frequency, amplitude, and phase parameters is supporte Parameter ramping is possible, but not currently supported by the labscript device (if support for this is of interest, please `open an issue `). The Pico interface provides the following: -* 16,656 instructions distributed evenly among the configured channels; 16,656, 8,615, 5,810, and 4,383 for 1, 2, 3, 4 channels respectively. +* For the Pico 1: 16,656 instructions distributed evenly among the configured channels; 16,656, 8,615, 5,810, and 4,383 for 1, 2, 3, 4 channels respectively. +* For the Pico 2: 34,132 instructions distributed evenly among the configured channels; 34,132, 17,654, 11,905, and 8,981 for 1, 2, 3, 4 channels respectively. * External timing via a pseudoclock clockline. * By default, the AD9959 system reference clock is taken from the Pi Pico. If a higher quality clock is needed, the user can provide an external system reference clock to the AD9959. For more details on clocking, see the Usage section. From aed9873e1da268245f079bd86b6699b6164d88f4 Mon Sep 17 00:00:00 2001 From: Jason Pruitt <55477390+Json-To-String@users.noreply.github.com> Date: Tue, 29 Apr 2025 19:12:45 -0400 Subject: [PATCH 19/22] saving progress, all static ins works, final value update works, still need to implement PrawnDO style smart cache and graceful aborts --- .../AD9959DDSSweeper/blacs_workers.py | 102 +++++++++++---- .../AD9959DDSSweeper/labscript_devices.py | 120 +++++++++--------- 2 files changed, 140 insertions(+), 82 deletions(-) diff --git a/labscript_devices/AD9959DDSSweeper/blacs_workers.py b/labscript_devices/AD9959DDSSweeper/blacs_workers.py index a13465f..40a8ad0 100644 --- a/labscript_devices/AD9959DDSSweeper/blacs_workers.py +++ b/labscript_devices/AD9959DDSSweeper/blacs_workers.py @@ -18,6 +18,7 @@ class AD9959DDSSweeperInterface(object): def __init__( self, com_port, + pico_board, sweep_mode, ref_clock_external, ref_clock_frequency, @@ -31,6 +32,7 @@ def __init__( Args: com_port (str): COM port assigned to the DDS Sweeper by the OS. On Windows, takes the form of `COMd` where `d` is an integer. + pico_board (str): The version of pico board used, pico1 or pico2. sweep_mode (int): The DDS Sweeper firmware can set the DDS outputs in either fixed steps or sweeps of the amplitude, frequency, or phase. At this time, only steps are supported, so sweep_mode must be 0. @@ -53,11 +55,28 @@ def __init__( 5: 'TRANSITION_TO_STOPPED' } + self.sys_clk_freq = ref_clock_frequency * pll_mult + + # self.SI_to_tuning_words = { + # 'freq' : (2**32 - 1) / self.sys_clk_freq, + # 'amp' : 1023.0, + # 'phase' : 360.0 / 16384.0 + # } + + self.tuning_words_to_SI = { + 'freq' : self.sys_clk_freq / (2**32 - 1) * 10.0, + 'amp' : 1/1023.0, + 'phase' : 360 / 16384.0 * 10.0 + } + + self.subchnls = ['freq', 'amp', 'phase'] + version = self.get_version() print(f'Connected to version: {version}') board = self.get_board() print(f'Connected to board: {board}') + assert board.strip() == pico_board.strip(), f'firmware thinks {board} attached, labscript thinks {pico_board}' current_status = self.get_status() print(f'Current status is {current_status}') @@ -208,61 +227,96 @@ class AD9959DDSSweeperWorker(Worker): def init(self): self.intf = AD9959DDSSweeperInterface( self.com_port, + self.pico_board, self.sweep_mode, self.ref_clock_external, self.ref_clock_frequency, self.pll_mult ) + + self.smart_cache = {'static_data' : None, 'dds_data' : None} def program_manual(self, values): - self.intf.abort() + # self.intf.abort() for chan in values: chan_int = int(chan[8:]) self.intf.set_output(chan_int, values[chan]['freq'], values[chan]['amp'], values[chan]['phase']) def transition_to_buffered(self, device_name, h5file, initial_values, fresh): + + # Store the initial values in case we have to abort and restore them: + self.initial_values = initial_values + # Store the final values for use during transition_to_manual: self.final_values = initial_values + dds_data = None + stat_data = None + with h5py.File(h5file, 'r') as hdf5_file: group = hdf5_file['devices'][device_name] - dds_data = group['dds_data'] - + if 'dds_data' in group: + dds_data = group['dds_data'][()] + dyn_chans = set([int(n[4:]) for n in dds_data.dtype.names if n.startswith('freq')]) if 'static_data' in group: - stat_data = group['static_data'] + stat_data = group['static_data'][()] stat_chans = set([int(n[4:]) for n in stat_data.dtype.names if n.startswith('freq')]) - if len(dds_data) == 0: - # Don't bother transitioning to buffered if no data - return {} - - if len(stat_data) > 0: - stat_array = stat_data[()] - for chan in sorted(stat_chans): - freq = stat_array[f'freq{chan}'] - amp = stat_array[f'amp{chan}'] - phase = stat_array[f'phase{chan}'] - self.intf.set_output(chan, freq, amp, phase) - - dyn_chans = set([int(n[4:]) for n in dds_data.dtype.names if n.startswith('freq')]) + if stat_data is not None: + stat_array = stat_data[:][0] + for chan in sorted(stat_chans): + freq = stat_array[f'freq{chan}'] + amp = stat_array[f'amp{chan}'] + phase = stat_array[f'phase{chan}'] + self.intf.set_output(chan, freq, amp, phase) + self.final_values[f'channel {chan}'] = { + 'freq' : freq * self.intf.tuning_words_to_SI['freq'], + 'amp' : amp * self.intf.tuning_words_to_SI['amp'], + 'phase' : phase * self.intf.tuning_words_to_SI['phase'] + } + + if dds_data is not None: self.intf.set_channels(len(dyn_chans)) self.intf.set_batch(dds_data[()]) self.intf.stop(len(dds_data[()])) - self.intf.start() - - return {} + last_entries = dds_data[-1] + for chan in sorted(dyn_chans): + freq = last_entries[f'freq{chan}'] + amp = last_entries[f'amp{chan}'] + phase = last_entries[f'phase{chan}'] + self.final_values[f'channel {chan}'] = { + 'freq' : freq * self.intf.tuning_words_to_SI['freq'], + 'amp' : amp * self.intf.tuning_words_to_SI['amp'], + 'phase' : phase * self.intf.tuning_words_to_SI['phase'] + } + self.intf.start() + + if dds_data is None and stat_data is None: + self.logger.debug('No instructions to set') + return {} + + # self.logger.info(self.final_values) + return self.final_values def transition_to_manual(self): - if self.final_values: - self.program_manual(self.final_values) + self.logger.debug(f'Transitioning to manual, values are: {self.final_values} ') + status = self.intf.get_status() + self.logger.debug(f'Transitioning to manual, got status: {status}') + + if status.strip().upper() == 'RUNNING': + self.logger.debug('Attempting to abort running') + self.intf.abort() + return True def abort_buffered(self): - return self.transition_to_manual() + self.intf.abort() + values = self.initial_values # fix, and update smart cache + return True def abort_transition_to_buffered(self): - return self.transition_to_manual() + return self.abort_buffered() def shutdown(self): self.intf.close() diff --git a/labscript_devices/AD9959DDSSweeper/labscript_devices.py b/labscript_devices/AD9959DDSSweeper/labscript_devices.py index 93e5ac9..900d307 100644 --- a/labscript_devices/AD9959DDSSweeper/labscript_devices.py +++ b/labscript_devices/AD9959DDSSweeper/labscript_devices.py @@ -20,12 +20,27 @@ class AD9959DDSSweeper(IntermediateDevice): allowed_children = [DDS, StaticDDS] + allowed_boards = ['pico1', 'pico2'] + # external timing + max_instructions_map = { + 'pico1' : + { + 'steps' : [16656, 8615, 5810, 4383], + 'sweeps' : [8614, 4382, 2938, 2210] + }, + 'pico2' : + { + 'steps' : [34132, 17654, 11905, 8981], + 'sweeps' : [17654, 8981, 6022, 4529] + } + } @set_passed_properties( property_names={ 'connection_table_properties': [ 'name', 'com_port', + 'pico_board', 'sweep_mode', 'ref_clock_external', 'ref_clock_frequency', @@ -35,7 +50,7 @@ class AD9959DDSSweeper(IntermediateDevice): ) def __init__(self, name, parent_device, com_port, - sweep_mode=0, + pico_board='pico1', sweep_mode=0, ref_clock_external=0, ref_clock_frequency=125e6, pll_mult=4, **kwargs): '''Labscript device class for AD9959 eval board controlled by a Raspberry Pi Pico running the DDS Sweeper firmware (https://github.com/QTC-UMD/dds-sweeper). @@ -47,6 +62,7 @@ def __init__(self, name, parent_device, com_port, Pseudoclock clockline used to clock DDS parameter changes. com_port (str): COM port assigned to the AD9959DDSSweeper by the OS. On Windows, takes the form of `COMd` where `d` is an integer. + pico_board (str): The version of pico board used, pico1 or pico2. sweep_mode (int): The DDS Sweeper firmware can set the DDS outputs in either fixed steps or sweeps of the amplitude, frequency, or phase. At this time, only steps are supported, so sweep_mode must be 0. @@ -57,6 +73,11 @@ def __init__(self, name, parent_device, com_port, IntermediateDevice.__init__(self, name, parent_device, **kwargs) self.BLACS_connection = '%s' % com_port + if pico_board in self.allowed_boards: + self.pico_board = pico_board + else: + raise LabscriptError(f'Pico board specified not in {self.allowed_boards}') + # store mode data self.sweep_mode = sweep_mode @@ -127,21 +148,8 @@ def quantise_amp(self, data, device): def generate_code(self, hdf5_file): - # external timing - max_instructions_map = { - 'pico1' : - { - 'steps' : [16656, 8615, 5810, 4383], - 'sweeps' : [8614, 4382, 2938, 2210] - }, - 'pico2' : - { - 'steps' : [34132, 17654, 11905, 8981], - 'sweeps' : [17654, 8981, 6022, 4529] - } - } - DDSs = {} + dyn_DDSs = {} stat_DDSs = {} num_channels = len(self.child_devices) @@ -153,7 +161,7 @@ def generate_code(self, hdf5_file): for output in self.child_devices: # Check that the instructions will fit into RAM: - max_instructions = max_instructions_map['pico1'][mode][num_channels-1] + max_instructions = self.max_instructions_map[self.pico_board][mode][num_channels-1] max_instructions -= 2 # -2 to include space for dummy instructions if isinstance(output, DDS) and len(output.frequency.raw_output) > max_instructions: raise LabscriptError( @@ -161,75 +169,71 @@ def generate_code(self, hdf5_file): Please decrease the sample rates of devices on the same clock, \ or connect {self.name} to a different pseudoclock.') try: + # if output.connection in range(4): prefix, channel = output.connection.split() channel = int(channel) + assert channel in range(4), 'requested channel out of range' except: raise LabscriptError('%s %s has invalid connection string: \'%s\'. ' % (output.description,output.name,str(output.connection)) + 'Format must be \'channel n\' with n from 0 to 4.') # separate dynamic from static if isinstance(output, DDS): - DDSs[channel] = output + dyn_DDSs[channel] = output elif isinstance(output, StaticDDS): stat_DDSs[channel] = output - if not DDSs: - # if no channels are being used, no need to continue - return - - for connection in DDSs: - if connection in range(4): - dds = DDSs[connection] + if dyn_DDSs: + for connection in dyn_DDSs: + dds = dyn_DDSs[connection] dds.frequency.raw_output, dds.frequency.scale_factor = self.quantise_freq(dds.frequency.raw_output, dds) dds.phase.raw_output, dds.phase.scale_factor = self.quantise_phase(dds.phase.raw_output, dds) dds.amplitude.raw_output, dds.amplitude.scale_factor = self.quantise_amp(dds.amplitude.raw_output, dds) - else: - raise LabscriptError('%s %s has invalid connection string: \'%s\'. ' % (dds.description,dds.name,str(dds.connection)) + - 'Format must be \'channel n\' with n from 0 to 4.') - dtypes = {'names':['%s%d' % (k, i) for i in DDSs for k in ['freq', 'amp', 'phase'] ], - 'formats':[f for i in DDSs for f in (' Date: Wed, 30 Apr 2025 10:20:53 -0400 Subject: [PATCH 20/22] Graceful aborts and docstring updates --- .../AD9959DDSSweeper/blacs_workers.py | 65 ++++++++++++++----- 1 file changed, 49 insertions(+), 16 deletions(-) diff --git a/labscript_devices/AD9959DDSSweeper/blacs_workers.py b/labscript_devices/AD9959DDSSweeper/blacs_workers.py index 40a8ad0..5bcb1e3 100644 --- a/labscript_devices/AD9959DDSSweeper/blacs_workers.py +++ b/labscript_devices/AD9959DDSSweeper/blacs_workers.py @@ -13,6 +13,7 @@ from blacs.tab_base_classes import Worker import labscript_utils.h5_lock, h5py +import time class AD9959DDSSweeperInterface(object): def __init__( @@ -32,7 +33,7 @@ def __init__( Args: com_port (str): COM port assigned to the DDS Sweeper by the OS. On Windows, takes the form of `COMd` where `d` is an integer. - pico_board (str): The version of pico board used, pico1 or pico2. + pico_board (str): The version of pico board used, 'pico1' or 'pico2'. sweep_mode (int): The DDS Sweeper firmware can set the DDS outputs in either fixed steps or sweeps of the amplitude, frequency, or phase. At this time, only steps are supported, so sweep_mode must be 0. @@ -120,12 +121,13 @@ def get_status(self): '''Reads the status of the AD9959 DDS Sweeper. Returns: - (str): Status in string representation. Accepted values are - STOPPED: manual mode - TRANSITION_TO_RUNNING: transitioning to buffered execution - RUNNING: buffered execution - ABORTING: aborting buffered execution - ABORTED: last buffered execution was aborted + (str): Status in string representation. Accepted values are: + + STOPPED: manual mode\n + TRANSITION_TO_RUNNING: transitioning to buffered execution\n + RUNNING: buffered execution\n + ABORTING: aborting buffered execution\n + ABORTED: last buffered execution was aborted\n TRANSITION_TO_STOPPED: transitioning to manual mode''' self.conn.write(b'status\n') @@ -237,7 +239,9 @@ def init(self): self.smart_cache = {'static_data' : None, 'dds_data' : None} def program_manual(self, values): - # self.intf.abort() + '''Called when user makes changes to the front panel. Performs updates + to freq, amp, phase by calling + :meth:`AD9959DDSSweeperInterface.set_output`''' for chan in values: chan_int = int(chan[8:]) @@ -245,6 +249,9 @@ def program_manual(self, values): def transition_to_buffered(self, device_name, h5file, initial_values, fresh): + if fresh: + self.smart_cache = {'static_data' : None, 'dds_data' : None} + # Store the initial values in case we have to abort and restore them: self.initial_values = initial_values # Store the final values for use during transition_to_manual: @@ -263,6 +270,8 @@ def transition_to_buffered(self, device_name, h5file, initial_values, fresh): stat_chans = set([int(n[4:]) for n in stat_data.dtype.names if n.startswith('freq')]) if stat_data is not None: + + # update static (final) values stat_array = stat_data[:][0] for chan in sorted(stat_chans): freq = stat_array[f'freq{chan}'] @@ -280,6 +289,7 @@ def transition_to_buffered(self, device_name, h5file, initial_values, fresh): self.intf.set_batch(dds_data[()]) self.intf.stop(len(dds_data[()])) + # update dynamic final values last_entries = dds_data[-1] for chan in sorted(dyn_chans): freq = last_entries[f'freq{chan}'] @@ -300,23 +310,46 @@ def transition_to_buffered(self, device_name, h5file, initial_values, fresh): return self.final_values def transition_to_manual(self): - self.logger.debug(f'Transitioning to manual, values are: {self.final_values} ') - status = self.intf.get_status() - self.logger.debug(f'Transitioning to manual, got status: {status}') - - if status.strip().upper() == 'RUNNING': - self.logger.debug('Attempting to abort running') - self.intf.abort() + '''Handles period between programmed instructions and return to front + panel control. Device will move from RUNNING -> TRANSITION_TO_STOPPED + -> STOPPED ideally. If buffered execution takes too long, calls + :meth:`abort_buffered`''' - return True + i = 0 + while True: + status = self.intf.get_status() + i += 1 + if status == 'STOPPED': + # self.logger.info('Transition to manual successful') + return True + + elif i == 1000: + # program hasn't ended, probably bad triggering + # abort and raise an error + self.abort_buffered() + raise LabscriptError(f'Buffered operation did not end with status {status:d}. Is triggering working?') + elif status in ['ABORTING', 'ABORTED']: + raise LabscriptError(f'AD9959 returned status {status} in transition to manual') def abort_buffered(self): + '''Aborts currently running program, ensuring ABORTED status. + Additionally updates front panels with values before run start and + updates smart cache before return.''' self.intf.abort() + while self.intf.get_status() != 'ABORTED': + self.logger.info('Tried to abort buffer, waiting another half second for ABORTED STATUS') + time.sleep(0.5) + self.logger.info('Successfully aborted buffered execution') + values = self.initial_values # fix, and update smart cache return True def abort_transition_to_buffered(self): + '''Aborts transition to buffered. + + Calls :meth:`abort_buffered`''' return self.abort_buffered() def shutdown(self): + '''Calls :meth:`AD9959DDSSweeperInterface.close` to end serial connection to AD9959''' self.intf.close() From 995a0f4eae829969232d041224a8686fff2ffd23 Mon Sep 17 00:00:00 2001 From: Jason Pruitt <55477390+Json-To-String@users.noreply.github.com> Date: Wed, 30 Apr 2025 11:38:33 -0400 Subject: [PATCH 21/22] status string format specifier fix --- labscript_devices/AD9959DDSSweeper/blacs_workers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/labscript_devices/AD9959DDSSweeper/blacs_workers.py b/labscript_devices/AD9959DDSSweeper/blacs_workers.py index 5bcb1e3..c74d4d6 100644 --- a/labscript_devices/AD9959DDSSweeper/blacs_workers.py +++ b/labscript_devices/AD9959DDSSweeper/blacs_workers.py @@ -327,7 +327,7 @@ def transition_to_manual(self): # program hasn't ended, probably bad triggering # abort and raise an error self.abort_buffered() - raise LabscriptError(f'Buffered operation did not end with status {status:d}. Is triggering working?') + raise LabscriptError(f'Buffered operation did not end with status {status}. Is triggering working?') elif status in ['ABORTING', 'ABORTED']: raise LabscriptError(f'AD9959 returned status {status} in transition to manual') From b821feaefb102629b54cf5cc0fff803ad50ecf3e Mon Sep 17 00:00:00 2001 From: Jason Pruitt <55477390+Json-To-String@users.noreply.github.com> Date: Thu, 1 May 2025 17:16:54 -0400 Subject: [PATCH 22/22] transition_to_buffered supports smart cache, borrowing logic from PrawnDO, NovatechDDS, and numpy magic --- .../AD9959DDSSweeper/blacs_workers.py | 201 +++++++++++++++--- 1 file changed, 170 insertions(+), 31 deletions(-) diff --git a/labscript_devices/AD9959DDSSweeper/blacs_workers.py b/labscript_devices/AD9959DDSSweeper/blacs_workers.py index c74d4d6..2586a4a 100644 --- a/labscript_devices/AD9959DDSSweeper/blacs_workers.py +++ b/labscript_devices/AD9959DDSSweeper/blacs_workers.py @@ -14,6 +14,8 @@ from blacs.tab_base_classes import Worker import labscript_utils.h5_lock, h5py import time +import numpy as np +from labscript import LabscriptError class AD9959DDSSweeperInterface(object): def __init__( @@ -67,7 +69,8 @@ def __init__( self.tuning_words_to_SI = { 'freq' : self.sys_clk_freq / (2**32 - 1) * 10.0, 'amp' : 1/1023.0, - 'phase' : 360 / 16384.0 * 10.0 + # 'phase' : 360 / 16384.0 * 10.0 + 'phase' : 360 / 16384.0 } self.subchnls = ['freq', 'amp', 'phase'] @@ -194,7 +197,9 @@ def seti(self, channel, addr, frequency, amplitude, phase): phase (unsigned 14 bit int): phase to jump to when this instruction runs. In DDS units: 360 * phase / 2^14 degrees.''' - self.conn.write(b'seti %d %d %f %f %f\n' % (channel, addr, frequency, amplitude, phase)) + cmd = f'seti {channel} {addr} {int(frequency)} {int(amplitude)} {int(phase)}\n' + self.conn.write(cmd.encode()) + # self.conn.write(b'seti %d %d %f %f %f\n' % (channel, addr, frequency, amplitude, phase)) self.assert_OK() def set_batch(self, table): @@ -238,10 +243,30 @@ def init(self): self.smart_cache = {'static_data' : None, 'dds_data' : None} + def _update_final_values(self, dds_data, dyn_chans): + '''Updates the final values in place using the last entry of dynamic + data. Only for internal use.''' + last_entries = dds_data[-1] + for chan in sorted(dyn_chans): + freq = last_entries[f'freq{chan}'] + amp = last_entries[f'amp{chan}'] + phase = last_entries[f'phase{chan}'] + self.final_values[f'channel {chan}'] = { + 'freq' : freq * self.intf.tuning_words_to_SI['freq'], + 'amp' : amp * self.intf.tuning_words_to_SI['amp'], + 'phase' : phase * self.intf.tuning_words_to_SI['phase'] + } + def program_manual(self, values): '''Called when user makes changes to the front panel. Performs updates to freq, amp, phase by calling - :meth:`AD9959DDSSweeperInterface.set_output`''' + :meth:`AD9959DDSSweeperInterface.set_output` + + Args: + values (dict): dictionary of dictionaries with keys of active DDS + channels, subkeys of ['freq', 'amp', 'phase'] + ''' + # invalidate static cache for chan in values: chan_int = int(chan[8:]) @@ -250,6 +275,7 @@ def program_manual(self, values): def transition_to_buffered(self, device_name, h5file, initial_values, fresh): if fresh: + self.logger.debug('\n------------Clearing smart cache for fresh start-----------') self.smart_cache = {'static_data' : None, 'dds_data' : None} # Store the initial values in case we have to abort and restore them: @@ -260,6 +286,7 @@ def transition_to_buffered(self, device_name, h5file, initial_values, fresh): dds_data = None stat_data = None + # get data to program from shot, if defined with h5py.File(h5file, 'r') as hdf5_file: group = hdf5_file['devices'][device_name] if 'dds_data' in group: @@ -269,44 +296,151 @@ def transition_to_buffered(self, device_name, h5file, initial_values, fresh): stat_data = group['static_data'][()] stat_chans = set([int(n[4:]) for n in stat_data.dtype.names if n.startswith('freq')]) + # handle static channels if stat_data is not None: + self.logger.debug(f'Static Data found') + stat_array = stat_data[0] - # update static (final) values - stat_array = stat_data[:][0] + if self.smart_cache['static_data'] is None: + self.smart_cache['static_data'] = np.zeros_like(stat_array) + for chan in sorted(stat_chans): freq = stat_array[f'freq{chan}'] amp = stat_array[f'amp{chan}'] phase = stat_array[f'phase{chan}'] - self.intf.set_output(chan, freq, amp, phase) - self.final_values[f'channel {chan}'] = { - 'freq' : freq * self.intf.tuning_words_to_SI['freq'], - 'amp' : amp * self.intf.tuning_words_to_SI['amp'], - 'phase' : phase * self.intf.tuning_words_to_SI['phase'] - } + + cache_freq = self.smart_cache['static_data'][f'freq{chan}'] + cache_amp = self.smart_cache['static_data'][f'amp{chan}'] + cache_phase = self.smart_cache['static_data'][f'phase{chan}'] + + if fresh or freq != cache_freq or amp != cache_amp or phase != cache_phase: + self.logger.debug(f'Setting fresh outputs on chan: {chan}') + self.intf.set_output(chan, freq, amp, phase) + self.final_values[f'channel {chan}'] = { + 'freq' : freq * self.intf.tuning_words_to_SI['freq'], + 'amp' : amp * self.intf.tuning_words_to_SI['amp'], + 'phase' : phase * self.intf.tuning_words_to_SI['phase'] + } + + else: + self.logger.debug(f'Setting outputs using cache on chan: {chan}') + self.intf.set_output(chan, cache_freq, cache_amp, cache_phase) + self.final_values[f'channel {chan}'] = { + 'freq' : cache_freq * self.intf.tuning_words_to_SI['freq'], + 'amp' : cache_amp * self.intf.tuning_words_to_SI['amp'], + 'phase' : cache_phase * self.intf.tuning_words_to_SI['phase'] + } if dds_data is not None: - self.intf.set_channels(len(dyn_chans)) - self.intf.set_batch(dds_data[()]) - self.intf.stop(len(dds_data[()])) - - # update dynamic final values - last_entries = dds_data[-1] - for chan in sorted(dyn_chans): - freq = last_entries[f'freq{chan}'] - amp = last_entries[f'amp{chan}'] - phase = last_entries[f'phase{chan}'] - self.final_values[f'channel {chan}'] = { - 'freq' : freq * self.intf.tuning_words_to_SI['freq'], - 'amp' : amp * self.intf.tuning_words_to_SI['amp'], - 'phase' : phase * self.intf.tuning_words_to_SI['phase'] - } - self.intf.start() + self.logger.debug(f'Dynamic Data found') + + if self.smart_cache['dds_data'] is None: + # self.logger.debug('Initializing dds_data smart cache') + self.logger.debug('First time run ') + self.intf.set_channels(len(dyn_chans)) + self.intf.set_batch(dds_data) + self.intf.stop(len(dds_data)) + self.smart_cache['dds_data'] = dds_data.copy() + self._update_final_values(dds_data, dyn_chans) + self.intf.start() + return self.final_values + + # check if it is more efficient to fully refresh + ## define boolean mask of lines that differ here for later line-by-line programming + # if not fresh and self.smart_cache['dds_data'] is not None: + if not fresh: + self.logger.debug('Checking to see if more efficient to fully refresh') + + cache = self.smart_cache['dds_data'] + + # check where the cache and table are equal + min_len = min(len(cache), len(dds_data)) + equal_mask = cache[:min_len] == dds_data[:min_len] + n_diff = np.count_nonzero(~equal_mask) + + # check where they differ + n_total = max(len(cache), len(dds_data)) + n_extra = abs(len(cache) - len(dds_data)) + changed_ratio = (n_diff + n_extra) / n_total + + # past a 10% change, force a refresh + if changed_ratio > 0.1: + self.logger.debug(f'Changed ratio: {changed_ratio:.2%}, refreshing fully') + fresh = True + + # Fresh starts use the faster binary batch mode + if fresh: + self.logger.debug('Programming a fresh set of dynamic instructions') + self.intf.set_channels(len(dyn_chans)) + self.intf.set_batch(dds_data) + self.intf.stop(len(dds_data)) + self.smart_cache['dds_data'] = dds_data.copy() + self.logger.debug('Updating dynamic final values via batch') + self._update_final_values(dds_data, dyn_chans) + self.intf.start() + + # If only a few changes, it should be fast to go through and change + # just the new instructions + else: + self.intf.set_channels(len(dyn_chans)) + self.logger.debug('Comparing changed instructions') + cache = self.smart_cache['dds_data'] + n_cache = len(cache) + + # Extend cache if necessary + if len(dds_data) > n_cache: + new_cache = np.empty(len(dds_data), dtype=dds_data.dtype) + new_cache[:n_cache] = cache + self.smart_cache['dds_data'] = new_cache + cache = new_cache + + # Boolean mask of each rows + changed_mask = np.zeros(len(dds_data), dtype=bool) + for name in dds_data.dtype.names: + + # need to check field-by-field, both vals and dtypes + diffs = np.where(cache[:len(dds_data)][name] != dds_data[name])[0] + if diffs.size > 0: + self.logger.debug(f"Field {name} differs at rows: {diffs}") + field_dtype = dds_data[name].dtype + if np.issubdtype(field_dtype, np.floating): + changed_mask |= ~np.isclose(cache[:len(dds_data)][name], dds_data[name]) + else: + changed_mask |= cache[:len(dds_data)][name] != dds_data[name] + + changed_indices = np.where(changed_mask)[0] + # Handle potential row count difference + if len(cache) != len(dds_data): + self.logger.debug(f"Length mismatch: cache has {len(cache)}, dds_data has {len(dds_data)}") + changed_indices = np.union1d(changed_indices, np.arange(len(dds_data), len(cache))) + self.logger.debug(f"Changed rows: {changed_indices}") + + # Iterate only over changed rows + for i in changed_indices: + self.logger.debug(f'Smart cache differs at index {i}') + if i >= len(dds_data): + self.logger.warning(f"Skipping seti at index {i} — beyond dds_data length") + continue + for chan in sorted(dyn_chans): + freq = dds_data[i][f'freq{chan}'] + amp = dds_data[i][f'amp{chan}'] + phase = dds_data[i][f'phase{chan}'] + self.logger.debug(f'seti {chan} {i} {freq} {amp} {phase}') + self.intf.seti(int(chan), int(i), int(freq), int(amp), int(phase)) + for name in dds_data.dtype.names: + cache[i][name] = dds_data[i][name] + + self.smart_cache['dds_data'] = cache[:len(dds_data)] + self.intf.stop(len(dds_data)) + self.logger.debug('Updating dynamic final values with smart cache') + self._update_final_values(self.smart_cache['dds_data'], dyn_chans) + self.intf.start() if dds_data is None and stat_data is None: self.logger.debug('No instructions to set') return {} - # self.logger.info(self.final_values) + self.logger.debug('Ending buffered execution\n') return self.final_values def transition_to_manual(self): @@ -320,7 +454,7 @@ def transition_to_manual(self): status = self.intf.get_status() i += 1 if status == 'STOPPED': - # self.logger.info('Transition to manual successful') + self.logger.debug('Transition to manual successful') return True elif i == 1000: @@ -337,11 +471,16 @@ def abort_buffered(self): updates smart cache before return.''' self.intf.abort() while self.intf.get_status() != 'ABORTED': - self.logger.info('Tried to abort buffer, waiting another half second for ABORTED STATUS') + self.logger.debug('Tried to abort buffer, waiting another half second for ABORTED STATUS') time.sleep(0.5) - self.logger.info('Successfully aborted buffered execution') + self.logger.debug('Successfully aborted buffered execution') + # return state to initial values values = self.initial_values # fix, and update smart cache + self.logger.debug(f'Returning to values: {values}') + self.smart_cache['static_data'] = None + self.smart_cache['dds_data'] = None + self.program_manual(values) return True def abort_transition_to_buffered(self):