From a6585823a0d88065c16b0c05aff11d12493bccc2 Mon Sep 17 00:00:00 2001 From: Carter Turn Date: Mon, 31 Mar 2025 08:03:37 -0400 Subject: [PATCH 01/45] 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 00000000..1810c078 --- /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 00000000..70f262ba --- /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 00000000..f16a0c8d --- /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/45] 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 00000000..39fd0993 --- /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 1810c078..e6b11e62 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 70f262ba..e6e8048f 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 f16a0c8d..66b519b0 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/45] 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 e6b11e62..ee600348 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 e6e8048f..28f9ad2c 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 66b519b0..96bba447 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/45] 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 28f9ad2c..495ff682 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/45] 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 1b42ec02..1e4fb666 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 00000000..817a076c --- /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/45] 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 817a076c..0a4c0033 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/45] 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 0a4c0033..8c922b7b 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 98d54dd3..78e06aa7 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/45] 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 8c922b7b..33ce044e 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 495ff682..9b495438 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 96bba447..bd954983 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/45] 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 9b495438..ed082482 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/45] 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 33ce044e..f1708231 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/45] 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 ed082482..7efd6322 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 bd954983..4e08d5e4 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/45] 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 4e08d5e4..9d2c31ef 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/45] 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 7efd6322..3956197e 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 9d2c31ef..93e5ac96 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/45] 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 fc068248..6a530524 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/45] 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 f1708231..ddb70ac5 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 3956197e..8f43208b 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/45] 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 8f43208b..eca0c93d 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/45] 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 eca0c93d..a13465f0 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/45] 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 ddb70ac5..0e65b613 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/45] 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 a13465f0..40a8ad0b 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 93e5ac96..900d3072 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/45] 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 40a8ad0b..5bcb1e39 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/45] 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 5bcb1e39..c74d4d69 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/45] 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 c74d4d69..2586a4a8 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): From 0c02e78e662365734bb3badf7dd184f07d3927e1 Mon Sep 17 00:00:00 2001 From: Jason Pruitt <55477390+Json-To-String@users.noreply.github.com> Date: Fri, 2 May 2025 14:34:34 -0400 Subject: [PATCH 23/45] adding clock_limit class attribute --- .../AD9959DDSSweeper/labscript_devices.py | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/labscript_devices/AD9959DDSSweeper/labscript_devices.py b/labscript_devices/AD9959DDSSweeper/labscript_devices.py index 900d3072..1164014a 100644 --- a/labscript_devices/AD9959DDSSweeper/labscript_devices.py +++ b/labscript_devices/AD9959DDSSweeper/labscript_devices.py @@ -21,7 +21,7 @@ class AD9959DDSSweeper(IntermediateDevice): allowed_children = [DDS, StaticDDS] allowed_boards = ['pico1', 'pico2'] - # external timing + # external timing max_instructions_map = { 'pico1' : { @@ -34,6 +34,11 @@ class AD9959DDSSweeper(IntermediateDevice): 'sweeps' : [17654, 8981, 6022, 4529] } } + + cycles_per_instruction_map = { + 'steps' : [500, 750, 1000, 1250], + 'sweeps' : [1000, 1500, 2000, 2500] + } @set_passed_properties( property_names={ @@ -91,7 +96,27 @@ def __init__(self, name, parent_device, com_port, self.dds_clock = ref_clock_frequency * pll_mult self.clk_scale = 2**32 / self.dds_clock - + + @property + def clock_limit(self): + '''Dynamically computs clock limit based off of number of channels and ref clock.''' + num_channels = len(self.child_devices) + if num_channels == 0: + # Set to worst case + # 4 channels, step mode, default 125 MHz pico ref clk + return 100000 + + if self.sweep_mode > 0: + mode = 'sweeps' + else: + mode = 'steps' + try: + cycles_per_instruction = self.cycles_per_instruction_map[mode][num_channels - 1] + except (KeyError, IndexError): + raise LabscriptError(f'Unsupported mode or number of channels: {mode}, {num_channels}') + + return self.dds_clock / cycles_per_instruction + 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 @@ -148,7 +173,6 @@ def quantise_amp(self, data, device): def generate_code(self, hdf5_file): - dyn_DDSs = {} stat_DDSs = {} num_channels = len(self.child_devices) @@ -169,7 +193,6 @@ 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' From c2ec2b99934726ef8f32e45cdee8c5b2dd8f177c Mon Sep 17 00:00:00 2001 From: Jason Pruitt <55477390+Json-To-String@users.noreply.github.com> Date: Fri, 2 May 2025 14:47:01 -0400 Subject: [PATCH 24/45] limit calculation uses dynamic channels, not all, and ref clk freq before pll mult --- .../AD9959DDSSweeper/labscript_devices.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/labscript_devices/AD9959DDSSweeper/labscript_devices.py b/labscript_devices/AD9959DDSSweeper/labscript_devices.py index 1164014a..6db6eb7e 100644 --- a/labscript_devices/AD9959DDSSweeper/labscript_devices.py +++ b/labscript_devices/AD9959DDSSweeper/labscript_devices.py @@ -85,7 +85,7 @@ def __init__(self, name, parent_device, com_port, # store mode data self.sweep_mode = sweep_mode - + self.ref_clock_frequency = ref_clock_frequency # Check clocking if ref_clock_frequency * pll_mult > 500e6: raise ValueError('DDS system clock frequency must be less than 500 MHz') @@ -99,9 +99,10 @@ def __init__(self, name, parent_device, com_port, @property def clock_limit(self): - '''Dynamically computs clock limit based off of number of channels and ref clock.''' - num_channels = len(self.child_devices) - if num_channels == 0: + '''Dynamically computs clock limit based off of number of dynamic + channels and reference clock frequency.''' + num_dyn_chans = sum(isinstance(child, DDS) for child in self.child_devices) + if num_dyn_chans == 0: # Set to worst case # 4 channels, step mode, default 125 MHz pico ref clk return 100000 @@ -111,11 +112,11 @@ def clock_limit(self): else: mode = 'steps' try: - cycles_per_instruction = self.cycles_per_instruction_map[mode][num_channels - 1] + cycles_per_instruction = self.cycles_per_instruction_map[mode][num_dyn_chans - 1] except (KeyError, IndexError): - raise LabscriptError(f'Unsupported mode or number of channels: {mode}, {num_channels}') + raise LabscriptError(f'Unsupported mode or number of channels: {mode}, {num_dyn_chans}') - return self.dds_clock / cycles_per_instruction + return self.ref_clock_frequency / cycles_per_instruction def get_default_unit_conversion_classes(self, device): """Child devices call this during their __init__ (with themselves @@ -182,7 +183,7 @@ def generate_code(self, hdf5_file): mode = 'sweeps' else: mode = 'steps' - + for output in self.child_devices: # Check that the instructions will fit into RAM: max_instructions = self.max_instructions_map[self.pico_board][mode][num_channels-1] From 146157a686370f95de0a9e54294fa34b91a2eabd Mon Sep 17 00:00:00 2001 From: Jason Pruitt <55477390+Json-To-String@users.noreply.github.com> Date: Tue, 6 May 2025 10:48:16 -0400 Subject: [PATCH 25/45] removed unnecessary if statement checks for both dyn and stat DDSs --- .../AD9959DDSSweeper/labscript_devices.py | 67 +++++++++---------- 1 file changed, 32 insertions(+), 35 deletions(-) diff --git a/labscript_devices/AD9959DDSSweeper/labscript_devices.py b/labscript_devices/AD9959DDSSweeper/labscript_devices.py index 6db6eb7e..4abf9efa 100644 --- a/labscript_devices/AD9959DDSSweeper/labscript_devices.py +++ b/labscript_devices/AD9959DDSSweeper/labscript_devices.py @@ -207,48 +207,45 @@ def generate_code(self, hdf5_file): elif isinstance(output, StaticDDS): stat_DDSs[channel] = output - 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) + 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) - dyn_dtypes = {'names':['%s%d' % (k, i) for i in dyn_DDSs for k in ['freq', 'amp', 'phase'] ], - 'formats':[f for i in dyn_DDSs for f in (' Date: Tue, 6 May 2025 11:45:33 -0400 Subject: [PATCH 26/45] More docstrings --- .../AD9959DDSSweeper/blacs_workers.py | 62 +++++++++++++++++-- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/labscript_devices/AD9959DDSSweeper/blacs_workers.py b/labscript_devices/AD9959DDSSweeper/blacs_workers.py index 2586a4a8..9fb9c6cd 100644 --- a/labscript_devices/AD9959DDSSweeper/blacs_workers.py +++ b/labscript_devices/AD9959DDSSweeper/blacs_workers.py @@ -168,7 +168,20 @@ def get_freqs(self): return freqs def set_output(self, channel, frequency, amplitude, phase): - '''Set frequency, amplitude, and phase of a channel.''' + '''Set frequency, phase, and amplitude of a channel + outside of the buffered sequence from floating point values. + + Args: + channel (int): channel to set the instruction for. Zero indexed. + frequency (float): + frequency of output. Floating point number in Hz (0-DDS clock/2). + Will be rounded during quantization to DDS units. + amplitude (float): + amplitude of output. Fraction of maximum output amplitude (0-1). + Will be rounded during quantization to DDS units. + phase (float): + phase of output. Floating point number in degrees (0-360). + Will be rounded during quantization to DDS units.''' self.conn.write(b'setfreq %d %f\n' % (channel, frequency)) self.assert_OK() self.conn.write(b'setamp %d %f\n' % (channel, amplitude)) @@ -177,7 +190,13 @@ def set_output(self, channel, frequency, amplitude, phase): self.assert_OK() def set_channels(self, channels): - '''Set number of channels to use in buffered sequence.''' + '''Set number of channels to use in buffered sequence. + + Args: + channels (int): + If 1-4, sets the number of channels activated for buffered mode. + Lowest channels are always used first. + If 0, simultaneously updates all channels during buffered mode.''' self.conn.write(b'setchannels %d\n' % channels) self.assert_OK() @@ -203,8 +222,26 @@ def seti(self, 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.''' + '''Set frequency, phase, and amplitude of all channels + for many addresses in buffered sequence from integer values in a table. + + Uses binary instruction encoding in transit to improve write speeds. + :meth:`set_batch` does not send a stop instruction, so call :meth:`stop` separately. + + Args: + table (numpy array): + Table should be an array of instructions in a mode-dependent format. + The dtypes should be repeated for each channel, with channel 0s parameters + first, followed by channel1s parameters, etc. depending on the number of channels. + The formats for each channel are as follows: + Single-step mode: ('frequency', ' Date: Thu, 8 May 2025 09:37:11 -0400 Subject: [PATCH 27/45] Default connection table example --- docs/source/devices/AD9959DDSSweeper.rst | 37 ++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/docs/source/devices/AD9959DDSSweeper.rst b/docs/source/devices/AD9959DDSSweeper.rst index 0e65b613..649da4d6 100644 --- a/docs/source/devices/AD9959DDSSweeper.rst +++ b/docs/source/devices/AD9959DDSSweeper.rst @@ -47,8 +47,40 @@ The number is assigned by the controlling computer and will need to be determine Usage ~~~~~ +An example connection table with the default settings of the sweeper: -An example connection table that uses the PrawnBlaster and sweeper with an external, 100 MHz clock: +.. code-block:: python + + 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 + + # 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', + ) + + + chann0 = DDS( 'chann0', AD9959, 'channel 0') + chann1 = DDS( 'chann1', AD9959, 'channel 1') + chann2 = StaticDDS( 'chann2', AD9959, 'channel 2') + chann3 = StaticDDS( 'chann3', AD9959, 'channel 3') + + + start() + + stop(1) + +An example connection table that uses the PrawnBlaster and sweeper with an +external, 100 MHz clock and pll multiplier of 5: .. code-block:: python @@ -67,7 +99,8 @@ An example connection table that uses the PrawnBlaster and sweeper with an exter name='AD9959', parent_device=prawn.clocklines[0], com_port='COM11', - ref_clock_external=1, + pico_board='pico2', + ref_clock_external=1, ref_clock_frequency=100e6, pll_mult=5 ) From 4580b0636fa910c3baefff5c9917d3c77a146244 Mon Sep 17 00:00:00 2001 From: Jason Pruitt <55477390+Json-To-String@users.noreply.github.com> Date: Thu, 8 May 2025 09:39:52 -0400 Subject: [PATCH 28/45] Cleaned up extra imports in the examples --- docs/source/devices/AD9959DDSSweeper.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/devices/AD9959DDSSweeper.rst b/docs/source/devices/AD9959DDSSweeper.rst index 649da4d6..d5b179a5 100644 --- a/docs/source/devices/AD9959DDSSweeper.rst +++ b/docs/source/devices/AD9959DDSSweeper.rst @@ -51,7 +51,7 @@ An example connection table with the default settings of the sweeper: .. code-block:: python - from labscript import start, stop, add_time_marker, AnalogOut, DigitalOut, DDS, StaticDDS + from labscript import start, stop, DDS, StaticDDS from labscript_devices.PrawnBlaster.labscript_devices import PrawnBlaster from labscript_devices.AD9959DDSSweeper.labscript_devices import AD9959DDSSweeper @@ -84,7 +84,7 @@ external, 100 MHz clock and pll multiplier of 5: .. code-block:: python - from labscript import start, stop, add_time_marker, AnalogOut, DigitalOut, DDS, StaticDDS + from labscript import start, stop, DDS, StaticDDS from labscript_devices.PrawnBlaster.labscript_devices import PrawnBlaster from labscript_devices.AD9959DDSSweeper.labscript_devices import AD9959DDSSweeper From f1985c537b45c6dae540cbea285c8c00d71fe040 Mon Sep 17 00:00:00 2001 From: Jason Pruitt <55477390+Json-To-String@users.noreply.github.com> Date: Thu, 8 May 2025 16:19:08 -0400 Subject: [PATCH 29/45] smart cache logic cleanup, removed inaccurate debug msg, moved explicit break --- .../AD9959DDSSweeper/blacs_workers.py | 35 +++++++------------ 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/labscript_devices/AD9959DDSSweeper/blacs_workers.py b/labscript_devices/AD9959DDSSweeper/blacs_workers.py index 9fb9c6cd..ded06d75 100644 --- a/labscript_devices/AD9959DDSSweeper/blacs_workers.py +++ b/labscript_devices/AD9959DDSSweeper/blacs_workers.py @@ -148,7 +148,7 @@ def get_board(self): (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) + return resp def get_freqs(self): '''Responds with a dictionary containing @@ -385,24 +385,17 @@ def transition_to_buffered(self, device_name, h5file, initial_values, fresh): 'phase' : cache_phase * self.intf.tuning_words_to_SI['phase'] } + if dds_data is None and stat_data is None: + self.logger.debug('No instructions to set') + return self.initial_values + if dds_data is not None: 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: + # using 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: self.logger.debug('Checking to see if more efficient to fully refresh') cache = self.smart_cache['dds_data'] @@ -421,6 +414,9 @@ def transition_to_buffered(self, device_name, h5file, initial_values, fresh): if changed_ratio > 0.1: self.logger.debug(f'Changed ratio: {changed_ratio:.2%}, refreshing fully') fresh = True + + elif self.smart_cache['dds_data'] is None: + fresh = True # Fresh starts use the faster binary batch mode if fresh: @@ -433,8 +429,8 @@ def transition_to_buffered(self, device_name, h5file, initial_values, fresh): 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 + # If only a few changes, incrementally program only the differing + # instructions else: self.intf.set_channels(len(dyn_chans)) self.logger.debug('Comparing changed instructions') @@ -490,11 +486,6 @@ def transition_to_buffered(self, device_name, h5file, initial_values, fresh): 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.debug('Ending buffered execution\n') return self.final_values def transition_to_manual(self): From d7b0dfc50b348ab196c64bc7baf8dd179f8c8e5a Mon Sep 17 00:00:00 2001 From: Carter Turn Date: Wed, 14 May 2025 09:57:03 -0400 Subject: [PATCH 30/45] Conversion to DDS units is done on Pi Pico for set_output. --- .../AD9959DDSSweeper/labscript_devices.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/labscript_devices/AD9959DDSSweeper/labscript_devices.py b/labscript_devices/AD9959DDSSweeper/labscript_devices.py index 4abf9efa..1b6773ce 100644 --- a/labscript_devices/AD9959DDSSweeper/labscript_devices.py +++ b/labscript_devices/AD9959DDSSweeper/labscript_devices.py @@ -227,16 +227,10 @@ def generate_code(self, hdf5_file): dyn_table['amp%d' % i][:] = dds.amplitude.raw_output dyn_table['phase%d' % i][:] = dds.phase.raw_output - # conversion to AD9959 units - for connection in stat_DDSs: - 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) - + # conversion to AD9959 units is done on the Pi Pico 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: Wed, 14 May 2025 10:00:46 -0400 Subject: [PATCH 31/45] Placeholder values for scale factor when only static channels are used. --- labscript_devices/AD9959DDSSweeper/labscript_devices.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/labscript_devices/AD9959DDSSweeper/labscript_devices.py b/labscript_devices/AD9959DDSSweeper/labscript_devices.py index 1b6773ce..cb77b7e5 100644 --- a/labscript_devices/AD9959DDSSweeper/labscript_devices.py +++ b/labscript_devices/AD9959DDSSweeper/labscript_devices.py @@ -228,6 +228,12 @@ def generate_code(self, hdf5_file): dyn_table['phase%d' % i][:] = dds.phase.raw_output # conversion to AD9959 units is done on the Pi Pico + for connection in stat_DDSs: + dds = stat_DDSs[connection] + dds.frequency.scale_factor = 1.0 + dds.phase.scale_factor = 1.0 + dds.amplitude.scale_factor = 1.0 + 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 ('float', 'float', 'float')] From b0797feeea5240ecd1711eebfff60ab199cb8073 Mon Sep 17 00:00:00 2001 From: Carter Turn Date: Wed, 14 May 2025 10:05:20 -0400 Subject: [PATCH 32/45] Don't need to do unit conversions for set output --- labscript_devices/AD9959DDSSweeper/blacs_workers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/labscript_devices/AD9959DDSSweeper/blacs_workers.py b/labscript_devices/AD9959DDSSweeper/blacs_workers.py index ded06d75..ccc76bfd 100644 --- a/labscript_devices/AD9959DDSSweeper/blacs_workers.py +++ b/labscript_devices/AD9959DDSSweeper/blacs_workers.py @@ -371,9 +371,9 @@ def transition_to_buffered(self, device_name, h5file, initial_values, fresh): 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'] + 'freq' : freq, + 'amp' : amp, + 'phase' : phase, } else: From adfa697a171825de06446f5c0489243c04f216ac Mon Sep 17 00:00:00 2001 From: Carter Turn Date: Wed, 14 May 2025 10:14:00 -0400 Subject: [PATCH 33/45] Remove stray? factor of 10 from tuning_words_to_SI dictionary. --- labscript_devices/AD9959DDSSweeper/blacs_workers.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/labscript_devices/AD9959DDSSweeper/blacs_workers.py b/labscript_devices/AD9959DDSSweeper/blacs_workers.py index ccc76bfd..a396f809 100644 --- a/labscript_devices/AD9959DDSSweeper/blacs_workers.py +++ b/labscript_devices/AD9959DDSSweeper/blacs_workers.py @@ -59,17 +59,10 @@ def __init__( } 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, + 'freq' : self.sys_clk_freq / (2**32 - 1), 'amp' : 1/1023.0, - # 'phase' : 360 / 16384.0 * 10.0 'phase' : 360 / 16384.0 } From 3f55bde705844d8cc7110257e642b4ecc26af63a Mon Sep 17 00:00:00 2001 From: Jason Pruitt <55477390+Json-To-String@users.noreply.github.com> Date: Mon, 19 May 2025 10:12:19 -0400 Subject: [PATCH 34/45] removing explicit break, is the same as returning self.final_values --- labscript_devices/AD9959DDSSweeper/blacs_workers.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/labscript_devices/AD9959DDSSweeper/blacs_workers.py b/labscript_devices/AD9959DDSSweeper/blacs_workers.py index a396f809..2611a014 100644 --- a/labscript_devices/AD9959DDSSweeper/blacs_workers.py +++ b/labscript_devices/AD9959DDSSweeper/blacs_workers.py @@ -377,10 +377,6 @@ def transition_to_buffered(self, device_name, h5file, initial_values, fresh): 'amp' : cache_amp * self.intf.tuning_words_to_SI['amp'], 'phase' : cache_phase * self.intf.tuning_words_to_SI['phase'] } - - if dds_data is None and stat_data is None: - self.logger.debug('No instructions to set') - return self.initial_values if dds_data is not None: self.logger.debug(f'Dynamic Data found') From fe811ef78bfba85fbe80c26c15388bf9b33b3ff4 Mon Sep 17 00:00:00 2001 From: Jason Pruitt <55477390+Json-To-String@users.noreply.github.com> Date: Mon, 19 May 2025 10:30:11 -0400 Subject: [PATCH 35/45] program manual will invalidate static cache --- labscript_devices/AD9959DDSSweeper/blacs_workers.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/labscript_devices/AD9959DDSSweeper/blacs_workers.py b/labscript_devices/AD9959DDSSweeper/blacs_workers.py index 2611a014..fe73c0d1 100644 --- a/labscript_devices/AD9959DDSSweeper/blacs_workers.py +++ b/labscript_devices/AD9959DDSSweeper/blacs_workers.py @@ -296,7 +296,7 @@ def program_manual(self, values): values (dict): dictionary of dictionaries with keys of active DDS channels, subkeys of ['freq', 'amp', 'phase'] ''' - # invalidate static cache + self.smart_cache = {'static_data' : None, 'dds_data' : None} for chan in values: chan_int = int(chan[8:]) @@ -368,15 +368,6 @@ def transition_to_buffered(self, device_name, h5file, initial_values, fresh): 'amp' : amp, 'phase' : 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.logger.debug(f'Dynamic Data found') From e0fe6eec967a7c44b4681a69114e0f453ce11445 Mon Sep 17 00:00:00 2001 From: Carter Turn Date: Tue, 20 May 2025 09:15:09 -0400 Subject: [PATCH 36/45] Add dynamic channels to constructor, and add checks in add_device to enforce it. This will allow the maximum clock rate to be determined. --- .../AD9959DDSSweeper/labscript_devices.py | 49 +++++++++++++++---- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/labscript_devices/AD9959DDSSweeper/labscript_devices.py b/labscript_devices/AD9959DDSSweeper/labscript_devices.py index cb77b7e5..94adceea 100644 --- a/labscript_devices/AD9959DDSSweeper/labscript_devices.py +++ b/labscript_devices/AD9959DDSSweeper/labscript_devices.py @@ -54,7 +54,7 @@ class AD9959DDSSweeper(IntermediateDevice): } ) - def __init__(self, name, parent_device, com_port, + def __init__(self, name, parent_device, com_port, dynamic_channels, 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). @@ -67,6 +67,8 @@ 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. + dynamic_channels (int): number of dynamic DDS channels that will be added. + This must be specified in the constructor so that update rates can be calculated correctly. 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. @@ -75,7 +77,6 @@ def __init__(self, name, parent_device, com_port, 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 if pico_board in self.allowed_boards: @@ -97,27 +98,55 @@ def __init__(self, name, parent_device, com_port, self.dds_clock = ref_clock_frequency * pll_mult self.clk_scale = 2**32 / self.dds_clock + # Store number of dynamic channels + if dynamic_channels > 4: + raise ValueError('AD9959DDSSweeper only supports up to 4 total channels, dynamic channels must be 4 or less.') + self.dynamic_channels = dynamic_channels + + IntermediateDevice.__init__(self, name, parent_device, **kwargs) + @property def clock_limit(self): '''Dynamically computs clock limit based off of number of dynamic channels and reference clock frequency.''' - num_dyn_chans = sum(isinstance(child, DDS) for child in self.child_devices) - if num_dyn_chans == 0: - # Set to worst case - # 4 channels, step mode, default 125 MHz pico ref clk - return 100000 + if self.dynamic_channels == 0: + # No clock limit + return None if self.sweep_mode > 0: mode = 'sweeps' else: mode = 'steps' try: - cycles_per_instruction = self.cycles_per_instruction_map[mode][num_dyn_chans - 1] + cycles_per_instruction = self.cycles_per_instruction_map[mode][self.dynamic_channels - 1] except (KeyError, IndexError): - raise LabscriptError(f'Unsupported mode or number of channels: {mode}, {num_dyn_chans}') + raise LabscriptError(f'Unsupported mode or number of channels: {mode}, {self.dynamic_channels}') return self.ref_clock_frequency / cycles_per_instruction - + + def add_device(self, device): + """Confirms channel specified is valid before adding + + Validity checks include channel name and static/dynamic status. + Dynamic channels must be specified before static channels. + Args: + device(): Device to attach. Must be a DDS or a StaticDDS. + Allowed connections are a string of the form `channel X`. + """ + conn = device.connection + chan = int(conn.split('channel ')[-1]) + + if isinstance(device, StaticDDS): + if chan < self.dynamic_channels: + raise LabscriptError(f'Channel {chan} configured as dynamic channel, can not create StaticDDS.') + elif chan >= 4: + raise LabscriptError('AD9959DDSSweeper only supports 4 channels') + elif isinstance(device, DDS): + if chan >= self.dynamic_channels: + raise LabscriptError(f'Channel {chan} not configured as dynamic channel, can not create DDS.') + + super().add_device(device) + 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 From 273c42dc103534a8fc0005e2b9016e36c04409ab Mon Sep 17 00:00:00 2001 From: Jason Pruitt <55477390+Json-To-String@users.noreply.github.com> Date: Tue, 20 May 2025 10:27:22 -0400 Subject: [PATCH 37/45] calculate max freq --- labscript_devices/AD9959DDSSweeper/blacs_tabs.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/labscript_devices/AD9959DDSSweeper/blacs_tabs.py b/labscript_devices/AD9959DDSSweeper/blacs_tabs.py index ee600348..b792aa76 100644 --- a/labscript_devices/AD9959DDSSweeper/blacs_tabs.py +++ b/labscript_devices/AD9959DDSSweeper/blacs_tabs.py @@ -13,12 +13,20 @@ from blacs.device_base_class import DeviceTab + class AD9959DDSSweeperTab(DeviceTab): def initialise_GUI(self): + device = self.settings['connection_table'].find_by_name(self.device_name) + + ref_clk_freq = device.properties['ref_clock_frequency'] + pll_mult = device.properties['pll_mult'] + self.com_port = device.properties['com_port'] + + max_freq = 0.5 * (ref_clk_freq * pll_mult) # 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_max = {'freq':max_freq, '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 @@ -38,10 +46,6 @@ def initialise_GUI(self): 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) From 10911d65ff843b44faef8d839fa0416868181785 Mon Sep 17 00:00:00 2001 From: Jason Pruitt <55477390+Json-To-String@users.noreply.github.com> Date: Thu, 22 May 2025 15:48:48 -0400 Subject: [PATCH 38/45] program manual should only invalidate static cache --- 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 fe73c0d1..15d0b3d1 100644 --- a/labscript_devices/AD9959DDSSweeper/blacs_workers.py +++ b/labscript_devices/AD9959DDSSweeper/blacs_workers.py @@ -296,7 +296,7 @@ def program_manual(self, values): values (dict): dictionary of dictionaries with keys of active DDS channels, subkeys of ['freq', 'amp', 'phase'] ''' - self.smart_cache = {'static_data' : None, 'dds_data' : None} + self.smart_cache = {'static_data' : None} for chan in values: chan_int = int(chan[8:]) From 0fe0504196f7bce534ed28d25e7b852db1f47172 Mon Sep 17 00:00:00 2001 From: Jason Pruitt <55477390+Json-To-String@users.noreply.github.com> Date: Thu, 22 May 2025 15:52:13 -0400 Subject: [PATCH 39/45] update examples with dynamic_channels parameter --- docs/source/devices/AD9959DDSSweeper.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/source/devices/AD9959DDSSweeper.rst b/docs/source/devices/AD9959DDSSweeper.rst index d5b179a5..315ecd7c 100644 --- a/docs/source/devices/AD9959DDSSweeper.rst +++ b/docs/source/devices/AD9959DDSSweeper.rst @@ -66,6 +66,7 @@ An example connection table with the default settings of the sweeper: name='AD9959', parent_device=prawn.clocklines[0], com_port='COM11', + dynamic_channels=2 ) @@ -79,8 +80,8 @@ An example connection table with the default settings of the sweeper: stop(1) -An example connection table that uses the PrawnBlaster and sweeper with an -external, 100 MHz clock and pll multiplier of 5: +An example connection table that uses the PrawnBlaster and sweeper with three +dynamic channels, an external, 100 MHz clock and pll multiplier of 5: .. code-block:: python @@ -99,6 +100,7 @@ external, 100 MHz clock and pll multiplier of 5: name='AD9959', parent_device=prawn.clocklines[0], com_port='COM11', + dynamic_channels=3, pico_board='pico2', ref_clock_external=1, ref_clock_frequency=100e6, From 9f377519e745287fc16d42156433b0bc8f048ac4 Mon Sep 17 00:00:00 2001 From: Carter Turn Date: Fri, 23 May 2025 16:51:02 -0400 Subject: [PATCH 40/45] Compute scale factors separately from quantising data. This handles the case where static and dynamic channels are mixed a bit better. --- .../AD9959DDSSweeper/labscript_devices.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/labscript_devices/AD9959DDSSweeper/labscript_devices.py b/labscript_devices/AD9959DDSSweeper/labscript_devices.py index 94adceea..488fd042 100644 --- a/labscript_devices/AD9959DDSSweeper/labscript_devices.py +++ b/labscript_devices/AD9959DDSSweeper/labscript_devices.py @@ -256,13 +256,6 @@ def generate_code(self, hdf5_file): dyn_table['amp%d' % i][:] = dds.amplitude.raw_output dyn_table['phase%d' % i][:] = dds.phase.raw_output - # conversion to AD9959 units is done on the Pi Pico - for connection in stat_DDSs: - dds = stat_DDSs[connection] - dds.frequency.scale_factor = 1.0 - dds.phase.scale_factor = 1.0 - dds.amplitude.scale_factor = 1.0 - 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 ('float', 'float', 'float')] @@ -286,6 +279,10 @@ def generate_code(self, hdf5_file): grp.create_dataset('dds_data', compression=config.compression, data=dyn_table) if stat_DDSs: grp.create_dataset('static_data', compression=config.compression, data=static_table) - self.set_property('frequency_scale_factor', dds.frequency.scale_factor, location='device_properties') - self.set_property('amplitude_scale_factor', dds.amplitude.scale_factor, location='device_properties') - self.set_property('phase_scale_factor', dds.phase.scale_factor, location='device_properties') + # Store parameter scale factors + _, frequency_scale_factor = self.quantise_freq([], None) + _, amplitude_scale_factor = self.quantise_amp([], None) + _, phase_scale_factor = self.quantise_phase([], None) + self.set_property('frequency_scale_factor', frequency_scale_factor, location='device_properties') + self.set_property('amplitude_scale_factor', amplitude_scale_factor, location='device_properties') + self.set_property('phase_scale_factor', phase_scale_factor, location='device_properties') From 9854b3cfdb2082f92297de633e237d27b2a02ee6 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Thu, 29 May 2025 10:58:49 -0400 Subject: [PATCH 41/45] Cache scale factor calculations and fully separate for quantization functions --- .../AD9959DDSSweeper/labscript_devices.py | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/labscript_devices/AD9959DDSSweeper/labscript_devices.py b/labscript_devices/AD9959DDSSweeper/labscript_devices.py index 488fd042..fb231e9d 100644 --- a/labscript_devices/AD9959DDSSweeper/labscript_devices.py +++ b/labscript_devices/AD9959DDSSweeper/labscript_devices.py @@ -96,7 +96,11 @@ def __init__(self, name, parent_device, com_port, dynamic_channels, 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 + # define output scale factors for dynamic channels + # static channel scaling handled by firmware + self.freq_scale = 2**32 / self.dds_clock + self.amp_scale = 1023 + self.phase_scale = 16384/360.0 # Store number of dynamic channels if dynamic_channels > 4: @@ -168,11 +172,10 @@ def quantise_freq(self, data, device): 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=' Date: Thu, 29 May 2025 10:59:41 -0400 Subject: [PATCH 42/45] Move `generate_code` break if no channels are connected earlier in the function --- labscript_devices/AD9959DDSSweeper/labscript_devices.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/labscript_devices/AD9959DDSSweeper/labscript_devices.py b/labscript_devices/AD9959DDSSweeper/labscript_devices.py index fb231e9d..d7fb52d3 100644 --- a/labscript_devices/AD9959DDSSweeper/labscript_devices.py +++ b/labscript_devices/AD9959DDSSweeper/labscript_devices.py @@ -237,6 +237,10 @@ def generate_code(self, hdf5_file): elif isinstance(output, StaticDDS): stat_DDSs[channel] = output + # if no channels are being used, no need to continue + if not dyn_DDSs and not stat_DDSs: + return + for connection in dyn_DDSs: dds = dyn_DDSs[connection] dds.frequency.raw_output = self.quantise_freq(dds.frequency.raw_output, dds) @@ -269,10 +273,6 @@ def generate_code(self, hdf5_file): static_table['freq%d' % connection] = sdds.frequency.raw_output[0] static_table['amp%d' % connection] = sdds.amplitude.raw_output[0] static_table['phase%d' % connection] = sdds.phase.raw_output[0] - - # if no channels are being used, no need to continue - if not dyn_DDSs and not stat_DDSs: - return # write out data tables grp = self.init_device_group(hdf5_file) From b3aee0a28cde5ab196f9d33113737d1451554166 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Thu, 29 May 2025 11:00:03 -0400 Subject: [PATCH 43/45] Tidy up some lints --- .../AD9959DDSSweeper/labscript_devices.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/labscript_devices/AD9959DDSSweeper/labscript_devices.py b/labscript_devices/AD9959DDSSweeper/labscript_devices.py index d7fb52d3..a14b6e05 100644 --- a/labscript_devices/AD9959DDSSweeper/labscript_devices.py +++ b/labscript_devices/AD9959DDSSweeper/labscript_devices.py @@ -16,7 +16,6 @@ import numpy as np -import sys class AD9959DDSSweeper(IntermediateDevice): allowed_children = [DDS, StaticDDS] @@ -169,9 +168,9 @@ def quantise_freq(self, data, device): 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)) + raise LabscriptError(f'{device.description:s} {device.name:s} '+ + f'can only have frequencies between 0.0Hz and {self.dds_clock/2e6:.1f} MHz, ' + + f'the limit imposed by {self.name:s}.') # It's faster to add 0.5 then typecast than to round to integers first: data = np.array((self.freq_scale*data)+0.5,dtype=' Date: Tue, 10 Jun 2025 08:50:08 -0400 Subject: [PATCH 44/45] program_manual should not erase dds_data from smart cache. --- 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 15d0b3d1..a8787433 100644 --- a/labscript_devices/AD9959DDSSweeper/blacs_workers.py +++ b/labscript_devices/AD9959DDSSweeper/blacs_workers.py @@ -296,7 +296,7 @@ def program_manual(self, values): values (dict): dictionary of dictionaries with keys of active DDS channels, subkeys of ['freq', 'amp', 'phase'] ''' - self.smart_cache = {'static_data' : None} + self.smart_cache['static_data'] = None for chan in values: chan_int = int(chan[8:]) From 33e1b6125404db202f626ee10ea81b8a133fdd65 Mon Sep 17 00:00:00 2001 From: Carter Turn Date: Mon, 23 Jun 2025 20:45:54 -0400 Subject: [PATCH 45/45] Ensure data table is constructed in correct order. --- labscript_devices/AD9959DDSSweeper/labscript_devices.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/labscript_devices/AD9959DDSSweeper/labscript_devices.py b/labscript_devices/AD9959DDSSweeper/labscript_devices.py index a14b6e05..eefe2603 100644 --- a/labscript_devices/AD9959DDSSweeper/labscript_devices.py +++ b/labscript_devices/AD9959DDSSweeper/labscript_devices.py @@ -240,6 +240,12 @@ def generate_code(self, hdf5_file): if not dyn_DDSs and not stat_DDSs: return + # Ensure data table is constructed in correct order + if dyn_DDSs: + dyn_DDSs = dict(sorted(dyn_DDSs.items())) + if stat_DDSs: + stat_DDSs = dict(sorted(stat_DDSs.items())) + for connection in dyn_DDSs: dds = dyn_DDSs[connection] dds.frequency.raw_output = self.quantise_freq(dds.frequency.raw_output, dds)