From 640a0192dfab4270b049fe22070918a9c68e956b Mon Sep 17 00:00:00 2001 From: David Meyer Date: Fri, 21 Jan 2022 12:06:42 -0500 Subject: [PATCH 01/16] Initial boiler plate for controlling Windfreak Synthesizers. Only tested for SynthHDPro HWv2.06 --- labscript_devices/Windfreak/__init__.py | 12 ++ labscript_devices/Windfreak/blacs_tabs.py | 76 ++++++++++++ labscript_devices/Windfreak/blacs_workers.py | 114 ++++++++++++++++++ .../Windfreak/labscript_devices.py | 101 ++++++++++++++++ .../Windfreak/register_classes.py | 20 +++ 5 files changed, 323 insertions(+) create mode 100644 labscript_devices/Windfreak/__init__.py create mode 100644 labscript_devices/Windfreak/blacs_tabs.py create mode 100644 labscript_devices/Windfreak/blacs_workers.py create mode 100644 labscript_devices/Windfreak/labscript_devices.py create mode 100644 labscript_devices/Windfreak/register_classes.py diff --git a/labscript_devices/Windfreak/__init__.py b/labscript_devices/Windfreak/__init__.py new file mode 100644 index 00000000..69d0cf4f --- /dev/null +++ b/labscript_devices/Windfreak/__init__.py @@ -0,0 +1,12 @@ +##################################################################### +# # +# /labscript_devices/Windfreak/__init__.py # +# # +# Copyright 2022, Monash University and contributors # +# # +# This file is part of 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/Windfreak/blacs_tabs.py b/labscript_devices/Windfreak/blacs_tabs.py new file mode 100644 index 00000000..c4916929 --- /dev/null +++ b/labscript_devices/Windfreak/blacs_tabs.py @@ -0,0 +1,76 @@ +##################################################################### +# # +# /labscript_devices/Windfreak/blacs_tabs.py # +# # +# Copyright 2022, Monash University and contributors # +# # +# This file is part of 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 + +import labscript_utils.properties + +class WindfreakSynthTab(DeviceTab): + + def __init__(self, *args, **kwargs): + if not hasattr(self,'device_worker_class'): + self.device_worker_class = 'labscript_devices.Windfreak.blacs_workers.WindfreakSynthWorker' + DeviceTab.__init__(self, *args, **kwargs) + + def initialise_GUI(self): + + print(self.settings) + conn_obj = self.settings['connection_table'].find_by_name(self.device_name).properties + + self.allowed_chans = conn_obj.get('allowed_chans',None) + + # finish populating these from device properties + chan_prop = {'freq':{},'amp':{},'phase':{}} + freq_limits = conn_obj.get('freq_limits',None) + chan_prop['freq']['min'] = freq_limits[0] + chan_prop['freq']['max'] = freq_limits[1] + chan_prop['freq']['decimals'] = conn_obj.get('freq_res',None) + chan_prop['freq']['base_unit'] = 'Hz' + chan_prop['freq']['step'] = 100 + amp_limits = conn_obj.get('amp_limits',None) + chan_prop['amp']['min'] = amp_limits[0] + chan_prop['amp']['max'] = amp_limits[1] + chan_prop['amp']['decimals'] = conn_obj.get('amp_res',None) + chan_prop['amp']['base_unit'] = 'dBm' + chan_prop['amp']['step'] = 1 + phase_limits = conn_obj.get('phase_limits',None) + chan_prop['phase']['min'] = phase_limits[0] + chan_prop['phase']['max'] = phase_limits[1] + chan_prop['phase']['decimals'] = conn_obj.get('phase_res',None) + chan_prop['phase']['base_unit'] = 'deg' + chan_prop['phase']['step'] = 1 + + dds_prop = {} + for chan in self.allowed_chans: + dds_prop[f'channel {chan:d}'] = chan_prop + + self.create_dds_outputs(dds_prop) + dds_widgets,ao_widgets,do_widgets = self.auto_create_widgets() + self.auto_place_widgets(('Synth Outputs',dds_widgets)) + + DeviceTab.initialise_GUI(self) + + # set capabilities + self.supports_remote_value_check(True) + self.supports_smart_programming(True) + #self.statemachine_timeout_add(5000,self.status_monitor) + + def initialise_workers(self): + + conn_obj = self.settings['connection_table'].find_by_name(self.device_name).properties + self.com_port = conn_obj.get('com_port',None) + + self.create_worker('main_worker',self.device_worker_class,{'com_port':self.com_port, + 'allowed_chans':self.allowed_chans}) + + self.primary_worker = 'main_worker' diff --git a/labscript_devices/Windfreak/blacs_workers.py b/labscript_devices/Windfreak/blacs_workers.py new file mode 100644 index 00000000..63c0bb57 --- /dev/null +++ b/labscript_devices/Windfreak/blacs_workers.py @@ -0,0 +1,114 @@ +##################################################################### +# # +# /labscript_devices/Windfreak/blacs_workers.py # +# # +# Copyright 2022, Monash University and contributors # +# # +# This file is part of 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 WindfreakSynthWorker(Worker): + + def init(self): + # hide import of 3rd-party library in here so docs don't need it + global windfreak; import windfreak + + # init smart cache to a known point + self.smart_cache = {'STATIC_DATA':None} + self.subchnls = ['freq','amp','phase'] + + # connect to synth + self.synth = windfreak.SynthHD(self.com_port) + + Worker.init(self) + + # populate smart chache + self.smart_cache['STATIC_DATA'] = self.check_remote_values() + + def check_remote_values(self): + + results = {} + for i in self.allowed_chans: + chan = f'channel {i:d}' + results[chan] = {} + for sub in self.subchnls: + results[chan][sub] = self.check_remote_value(i,sub) + + return results + + def program_manual(self, front_panel_values): + + for i in self.allowed_chans: + chan = f'channel {i:d}' + for sub in self.subchnls: + if self.smart_cache['STATIC_DATA'][chan][sub] == front_panel_values[chan][sub]: + # don't program if desired setting already present + continue + self.program_static_value(i,sub,front_panel_values[chan][sub]) + # invalidate smart cache upon manual programming + self.smart_cache['STATIC_DATA'][chan][sub] = None + + return self.check_remote_values() + + def check_remote_value(self,channel,type): + + if type == 'freq': + return self.synth[channel].frequency + elif type == 'amp': + return self.synth[channel].power + elif type == 'phase': + return self.synth[channel].phase + else: + raise ValueError(type) + + def program_static_value(self,channel,type,value): + + if type == 'freq': + self.synth[channel].frequency = value + elif type == 'amp': + self.synth[channel].power = value + elif type == 'phase': + self.synth[channel].phase = value + else: + raise ValueError(type) + + def transition_to_buffered(self, device_name, h5file, initial_values, fresh): + + self.initial_values = initial_values + self.final_values = initial_values + + static_data = None + with h5py.File(h5file,'r') as hdf5_file: + group = hdf5_file['/devices/'+device_name] + if 'STATIC_DATA' in group: + static_data = group['STATIC_DATA'][:][0] + + if static_data is not None: + data = static_data + if fresh or data != self.smart_cache['STATIC_DATA']: + + # need to infer which channels are programming + num_chan = len(data)//len(self.subchnls) + channels = [int(name[-1]) for name in data.dtype.names[0:num_chan]] + + for i in channels: + for sub in self.subchnls: + self.program_static_value(i,sub,data[sub+str(i)]) + + # update smart cache to reflect programmed values + self.smart_cache['STATIC_DATA'] = data + + + return self.final_values + + def shutdown(self): + # save current state the memory + self.synth.save() + self.synth.close() \ No newline at end of file diff --git a/labscript_devices/Windfreak/labscript_devices.py b/labscript_devices/Windfreak/labscript_devices.py new file mode 100644 index 00000000..59b75e99 --- /dev/null +++ b/labscript_devices/Windfreak/labscript_devices.py @@ -0,0 +1,101 @@ +##################################################################### +# # +# /labscript_devices/Windfreak/labscript_devices.py # +# # +# Copyright 2022, Monash University and contributors # +# # +# This file is part of 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 LabscriptError, set_passed_properties, config, StaticDDS, IntermediateDevice, Device +from labscript_utils import dedent + +import numpy as np + +class WindfreakSynth(Device): + description = 'Windfreak HDPro Synthesizer' + allowed_children = [StaticDDS] + # note, box labels 'A', 'B' map to programming channels 0, 1 + allowed_chans = [0,1] + + # define output limitations for the SynthHDPro + freq_limits = (10e6,24e9) # set in Hz + freq_res = 1 # number of sig digits after decimal + amp_limits = (-40.0,20.0) # set in dBm + amp_res = 2 + phase_limits = (0.0,360.0) # in deg + phase_res = 2 + + @set_passed_properties(property_names={ + 'connection_table_properties': [ + 'com_port', + 'allowed_chans', + 'freq_limits', + 'freq_res', + 'amp_limits', + 'amp_res', + 'phase_limits', + 'phase_res', + ] + }) + def __init__(self, name, com_port="", **kwargs): + + Device.__init__(self, name, None, com_port, **kwargs) + self.BLACS_connection = com_port + + def add_device(self, device): + Device.add_device(self, device) + # ensure a valid default value + device.frequency.default_value = 10e6 + + def validate_data(self, data, limits, device): + if not isinstance(data, np.ndarray): + data = np.array(data,dtype=np.float64) + if np.any(data < limits[0]) or np.any(data > limits[1]): + msg = f'''{device.description} {device.name} can only have frequencies between + {limits[0]:E}Hz and {limits[1]:E}Hz, {data} given + ''' + raise LabscriptError(dedent(msg)) + return data + + def generate_code(self, hdf5_file): + DDSs = {} + + + for output in self.child_devices: + + try: + prefix, channel = output.connection.split() + if channel not in self.allowed_chans: + LabscriptError(f"Channel {channel} must be 0 or 0") + except: + msg = f"""{output.description}:{output.name} has invalid connection string. + Only 'channel 0' or 'channel 1' is allowed. + """ + raise LabscriptError(dedent(msg)) + + DDSs[channel] = output + + for connection in DDSs: + dds = DDSs[connection] + dds.frequency.raw_output = self.validate_data(dds.frequency.static_value,self.freq_limits,dds) + dds.amplitude.raw_output = self.validate_data(dds.amplitude.static_value,self.amp_limits,dds) + dds.phase.raw_output = self.validate_data(dds.phase.static_value,self.phase_limits,dds) + + static_dtypes = [(f'freq{i:d}',np.float64) for i in self.allowed_chans] +\ + [(f'amp{i:d}',np.float64) for i in self.allowed_chans] +\ + [(f'phase{i:d}',np.float64) for i in self.allowed_chans] + static_table = np.zeros(1,dtype=static_dtypes) + + for connection in DDSs: + static_table[f'freq{connection}'] = dds.frequency.raw_output + static_table[f'amp{connection}'] = dds.amplitude.raw_output + static_table[f'phase{connection}'] = dds.phase.raw_output + + grp = self.init_device_group(hdf5_file) + grp.create_dataset('STATIC_DATA',compression=config.compression,data=static_table) + diff --git a/labscript_devices/Windfreak/register_classes.py b/labscript_devices/Windfreak/register_classes.py new file mode 100644 index 00000000..1d93a66c --- /dev/null +++ b/labscript_devices/Windfreak/register_classes.py @@ -0,0 +1,20 @@ +##################################################################### +# # +# /labscript_devices/Windfreak/register_classes.py # +# # +# Copyright 2022, Monash University and contributors # +# # +# This file is part of 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. # +# # +##################################################################### + +import labscript_devices + +labscript_devices.register_classes( + 'WindfreakSynth', + BLACS_tab='labscript_devices.Windfreak.blacs_tabs.WindfreakSynthTab', + runviewer_parser=None +) \ No newline at end of file From 0cd145300949e4e67f0123bbff851e4dadf05160 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Wed, 9 Feb 2022 12:08:19 -0500 Subject: [PATCH 02/16] Update to have a default Frequency conversion class --- .../Windfreak/labscript_devices.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/labscript_devices/Windfreak/labscript_devices.py b/labscript_devices/Windfreak/labscript_devices.py index 59b75e99..b13efe0c 100644 --- a/labscript_devices/Windfreak/labscript_devices.py +++ b/labscript_devices/Windfreak/labscript_devices.py @@ -13,6 +13,46 @@ from labscript import LabscriptError, set_passed_properties, config, StaticDDS, IntermediateDevice, Device from labscript_utils import dedent +from labscript_utils.unitconversions.UnitConversionBase import UnitConversion + +class FreqConversion(UnitConversion): + """ + A Generic frequency conversion class that covers standard SI prefixes from a base Hz. + """ + + base_unit = 'Hz' # must be defined here and match default hardware unit in BLACS tab + + def __init__(self, calibration_parameters = None): + self.parameters = calibration_parameters + if hasattr(self, 'derived_units'): + self.derived_units += ['kHz', 'MHz', 'GHz'] + else: + self.derived_units = ['kHz', 'MHz', 'GHz'] + UnitConversion.__init__(self,self.parameters) + + def kHz_to_base(self,kHz): + Hz = kHz*1e3 + return Hz + + def kHz_from_base(self,Hz): + kHz = Hz*1e-3 + return kHz + + def MHz_to_base(self,MHz): + Hz = MHz*1e6 + return Hz + + def MHz_from_base(self,Hz): + MHz = Hz*1e-6 + return MHz + + def GHz_to_base(self,GHz): + Hz = GHz*1e9 + return Hz + + def GHz_from_base(self,Hz): + GHz = Hz*1e-9 + return GHz import numpy as np @@ -52,6 +92,14 @@ def add_device(self, device): # ensure a valid default value device.frequency.default_value = 10e6 + def get_default_unit_conversion_classes(self, device): + """Child devices call this during their `__init__` to get default unit conversions. + + If user has not overridden, will use generic FreqConversion class. + """ + + return FreqConversion, None, None + def validate_data(self, data, limits, device): if not isinstance(data, np.ndarray): data = np.array(data,dtype=np.float64) From c0996963c49f9b72af16d5d19266a2ea08868ebd Mon Sep 17 00:00:00 2001 From: David Meyer Date: Wed, 9 Feb 2022 12:09:43 -0500 Subject: [PATCH 03/16] Allow for setting the synth trigger mode. Right now, only the 'rf enable' mode is correctly programmed by labscript directly (via DDS.gates). --- labscript_devices/Windfreak/blacs_tabs.py | 4 +++- labscript_devices/Windfreak/blacs_workers.py | 24 +++++++++++++++++-- .../Windfreak/labscript_devices.py | 4 +++- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/labscript_devices/Windfreak/blacs_tabs.py b/labscript_devices/Windfreak/blacs_tabs.py index c4916929..4e627ad1 100644 --- a/labscript_devices/Windfreak/blacs_tabs.py +++ b/labscript_devices/Windfreak/blacs_tabs.py @@ -69,8 +69,10 @@ def initialise_workers(self): conn_obj = self.settings['connection_table'].find_by_name(self.device_name).properties self.com_port = conn_obj.get('com_port',None) + self.trigger_mode = conn_obj.get('trigger_mode','disabled') self.create_worker('main_worker',self.device_worker_class,{'com_port':self.com_port, - 'allowed_chans':self.allowed_chans}) + 'allowed_chans':self.allowed_chans, + 'trigger_mode':self.trigger_mode}) self.primary_worker = 'main_worker' diff --git a/labscript_devices/Windfreak/blacs_workers.py b/labscript_devices/Windfreak/blacs_workers.py index 63c0bb57..b285259f 100644 --- a/labscript_devices/Windfreak/blacs_workers.py +++ b/labscript_devices/Windfreak/blacs_workers.py @@ -24,14 +24,34 @@ def init(self): self.smart_cache = {'STATIC_DATA':None} self.subchnls = ['freq','amp','phase'] + Worker.init(self) + # connect to synth self.synth = windfreak.SynthHD(self.com_port) - - Worker.init(self) + self.valid_modes = self.synth.trigger_modes + # set trigger mode from connection_table_properties + self.set_trigger_mode(self.trigger_mode) # populate smart chache self.smart_cache['STATIC_DATA'] = self.check_remote_values() + def set_trigger_mode(self,mode): + """Sets the synth trigger mode. + + Provides basic error checking to confirm setting is valid. + + Args: + mode (str): Trigger mode to set. + + Raises: + ValueError: If `mode` is not a valid setting for the device. + """ + + if mode in self.valid_modes: + self.synth.trigger_mode = mode + else: + raise ValueError(f'{mode} not in {self.valid_modes}') + def check_remote_values(self): results = {} diff --git a/labscript_devices/Windfreak/labscript_devices.py b/labscript_devices/Windfreak/labscript_devices.py index b13efe0c..3efa9a0e 100644 --- a/labscript_devices/Windfreak/labscript_devices.py +++ b/labscript_devices/Windfreak/labscript_devices.py @@ -80,12 +80,14 @@ class WindfreakSynth(Device): 'amp_res', 'phase_limits', 'phase_res', + 'trigger_mode', ] }) - def __init__(self, name, com_port="", **kwargs): + def __init__(self, name, com_port="", trigger_mode='disabled', **kwargs): Device.__init__(self, name, None, com_port, **kwargs) self.BLACS_connection = com_port + self.trigger_mode = trigger_mode def add_device(self, device): Device.add_device(self, device) From ea96b97ffc57108e3c25cd494137d55a07d90e28 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Wed, 9 Feb 2022 12:20:09 -0500 Subject: [PATCH 04/16] Update some docstrings. --- .../Windfreak/labscript_devices.py | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/labscript_devices/Windfreak/labscript_devices.py b/labscript_devices/Windfreak/labscript_devices.py index 3efa9a0e..c9d2de41 100644 --- a/labscript_devices/Windfreak/labscript_devices.py +++ b/labscript_devices/Windfreak/labscript_devices.py @@ -17,7 +17,7 @@ class FreqConversion(UnitConversion): """ - A Generic frequency conversion class that covers standard SI prefixes from a base Hz. + A Generic frequency conversion class that covers standard SI prefixes from a base of Hz. """ base_unit = 'Hz' # must be defined here and match default hardware unit in BLACS tab @@ -84,6 +84,20 @@ class WindfreakSynth(Device): ] }) def __init__(self, name, com_port="", trigger_mode='disabled', **kwargs): + """Creates a Windfreak HDPro Synthesizer + + Args: + name (str): python variable name to assign the device to. + com_port (str): COM port connection string. + Must take the form of 'COM d', where d is an integer. + trigger_mode (str): Trigger mode for the device to use. + Currently, labscript only directly programs 'rf enable', + via setting DDS gates. + labscript could correctly program other modes with some effort. + Other modes can be correctly programmed externally, + with the settings saved to EEPROM. + **kwargs: Keyword arguments passed to :obj:`labscript:labscript.Device.__init__`. + """ Device.__init__(self, name, None, com_port, **kwargs) self.BLACS_connection = com_port @@ -103,6 +117,17 @@ def get_default_unit_conversion_classes(self, device): return FreqConversion, None, None def validate_data(self, data, limits, device): + """Tests that requested data is within limits. + + Args: + data (iterable or numeric): Data to be checked. + Input is cast to a numpy array of type float64. + limits (tuple): 2-element tuple of (min, max) range + device (:obj:`labscript:labscript.Device`): labscript device we are performing check on. + + Returns: + numpy.ndarray: Input data, cast to a numpy array. + """ if not isinstance(data, np.ndarray): data = np.array(data,dtype=np.float64) if np.any(data < limits[0]) or np.any(data > limits[1]): From 87468a1220a7724618e09be3a3d7d7850c2734a8 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Thu, 3 Mar 2022 09:32:36 -0500 Subject: [PATCH 05/16] Update to use `FreqConversion` class in mainline labscript_utils. Also cleans up a few linter warnings. --- .../Windfreak/labscript_devices.py | 61 ++++--------------- 1 file changed, 11 insertions(+), 50 deletions(-) diff --git a/labscript_devices/Windfreak/labscript_devices.py b/labscript_devices/Windfreak/labscript_devices.py index c9d2de41..2fb92269 100644 --- a/labscript_devices/Windfreak/labscript_devices.py +++ b/labscript_devices/Windfreak/labscript_devices.py @@ -13,61 +13,23 @@ from labscript import LabscriptError, set_passed_properties, config, StaticDDS, IntermediateDevice, Device from labscript_utils import dedent -from labscript_utils.unitconversions.UnitConversionBase import UnitConversion - -class FreqConversion(UnitConversion): - """ - A Generic frequency conversion class that covers standard SI prefixes from a base of Hz. - """ - - base_unit = 'Hz' # must be defined here and match default hardware unit in BLACS tab - - def __init__(self, calibration_parameters = None): - self.parameters = calibration_parameters - if hasattr(self, 'derived_units'): - self.derived_units += ['kHz', 'MHz', 'GHz'] - else: - self.derived_units = ['kHz', 'MHz', 'GHz'] - UnitConversion.__init__(self,self.parameters) - - def kHz_to_base(self,kHz): - Hz = kHz*1e3 - return Hz - - def kHz_from_base(self,Hz): - kHz = Hz*1e-3 - return kHz - - def MHz_to_base(self,MHz): - Hz = MHz*1e6 - return Hz - - def MHz_from_base(self,Hz): - MHz = Hz*1e-6 - return MHz - - def GHz_to_base(self,GHz): - Hz = GHz*1e9 - return Hz - - def GHz_from_base(self,Hz): - GHz = Hz*1e-9 - return GHz +from labscript_utils.unitconversions.generic_frequency import FreqConversion import numpy as np + class WindfreakSynth(Device): description = 'Windfreak HDPro Synthesizer' allowed_children = [StaticDDS] # note, box labels 'A', 'B' map to programming channels 0, 1 - allowed_chans = [0,1] + allowed_chans = [0, 1] # define output limitations for the SynthHDPro - freq_limits = (10e6,24e9) # set in Hz - freq_res = 1 # number of sig digits after decimal - amp_limits = (-40.0,20.0) # set in dBm + freq_limits = (10e6, 24e9) # set in Hz + freq_res = 1 # number of sig digits after decimal + amp_limits = (-40.0, 20.0) # set in dBm amp_res = 2 - phase_limits = (0.0,360.0) # in deg + phase_limits = (0.0, 360.0) # in deg phase_res = 2 @set_passed_properties(property_names={ @@ -85,7 +47,7 @@ class WindfreakSynth(Device): }) def __init__(self, name, com_port="", trigger_mode='disabled', **kwargs): """Creates a Windfreak HDPro Synthesizer - + Args: name (str): python variable name to assign the device to. com_port (str): COM port connection string. @@ -110,7 +72,7 @@ def add_device(self, device): def get_default_unit_conversion_classes(self, device): """Child devices call this during their `__init__` to get default unit conversions. - + If user has not overridden, will use generic FreqConversion class. """ @@ -118,7 +80,7 @@ def get_default_unit_conversion_classes(self, device): def validate_data(self, data, limits, device): """Tests that requested data is within limits. - + Args: data (iterable or numeric): Data to be checked. Input is cast to a numpy array of type float64. @@ -135,11 +97,10 @@ def validate_data(self, data, limits, device): {limits[0]:E}Hz and {limits[1]:E}Hz, {data} given ''' raise LabscriptError(dedent(msg)) - return data + return data def generate_code(self, hdf5_file): DDSs = {} - for output in self.child_devices: From de8509553633976604ca3a82a6c5761284547f09 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Fri, 4 Mar 2022 10:47:48 -0500 Subject: [PATCH 06/16] Enable software gating of the outputs. Adds enable flat to synth output that is independent of the optional hardware-timed gate provided by another digital output on the DDS. Note, you will need to use WinfreakSynth.enable_output() in scripts to enable the output at the device level. --- labscript_devices/Windfreak/blacs_tabs.py | 5 ++--- labscript_devices/Windfreak/blacs_workers.py | 13 ++++++++++--- .../Windfreak/labscript_devices.py | 19 ++++++++++++++++++- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/labscript_devices/Windfreak/blacs_tabs.py b/labscript_devices/Windfreak/blacs_tabs.py index 4e627ad1..fbb89550 100644 --- a/labscript_devices/Windfreak/blacs_tabs.py +++ b/labscript_devices/Windfreak/blacs_tabs.py @@ -13,7 +13,6 @@ from blacs.device_base_class import DeviceTab -import labscript_utils.properties class WindfreakSynthTab(DeviceTab): @@ -28,9 +27,9 @@ def initialise_GUI(self): conn_obj = self.settings['connection_table'].find_by_name(self.device_name).properties self.allowed_chans = conn_obj.get('allowed_chans',None) - + # finish populating these from device properties - chan_prop = {'freq':{},'amp':{},'phase':{}} + chan_prop = {'freq':{},'amp':{},'phase':{},'gate':{}} freq_limits = conn_obj.get('freq_limits',None) chan_prop['freq']['min'] = freq_limits[0] chan_prop['freq']['max'] = freq_limits[1] diff --git a/labscript_devices/Windfreak/blacs_workers.py b/labscript_devices/Windfreak/blacs_workers.py index b285259f..8777efb1 100644 --- a/labscript_devices/Windfreak/blacs_workers.py +++ b/labscript_devices/Windfreak/blacs_workers.py @@ -14,6 +14,7 @@ from blacs.tab_base_classes import Worker import labscript_utils.h5_lock, h5py + class WindfreakSynthWorker(Worker): def init(self): @@ -22,7 +23,8 @@ def init(self): # init smart cache to a known point self.smart_cache = {'STATIC_DATA':None} - self.subchnls = ['freq','amp','phase'] + self.subchnls = ['freq','amp','phase','gate'] + # this will be the order in which each channel is programmed Worker.init(self) @@ -37,7 +39,7 @@ def init(self): def set_trigger_mode(self,mode): """Sets the synth trigger mode. - + Provides basic error checking to confirm setting is valid. Args: @@ -85,6 +87,8 @@ def check_remote_value(self,channel,type): return self.synth[channel].power elif type == 'phase': return self.synth[channel].phase + elif type == 'gate': + return self.synth[channel].rf_enable and self.synth[channel].pll_enable else: raise ValueError(type) @@ -96,6 +100,9 @@ def program_static_value(self,channel,type,value): self.synth[channel].power = value elif type == 'phase': self.synth[channel].phase = value + elif type == 'gate': + self.synth[channel].rf_enable = value + self.synth[channel].pll_enable = value else: raise ValueError(type) @@ -131,4 +138,4 @@ def transition_to_buffered(self, device_name, h5file, initial_values, fresh): def shutdown(self): # save current state the memory self.synth.save() - self.synth.close() \ No newline at end of file + self.synth.close() diff --git a/labscript_devices/Windfreak/labscript_devices.py b/labscript_devices/Windfreak/labscript_devices.py index 2fb92269..86ffe6b5 100644 --- a/labscript_devices/Windfreak/labscript_devices.py +++ b/labscript_devices/Windfreak/labscript_devices.py @@ -23,6 +23,7 @@ class WindfreakSynth(Device): allowed_children = [StaticDDS] # note, box labels 'A', 'B' map to programming channels 0, 1 allowed_chans = [0, 1] + enabled_chans = [] # define output limitations for the SynthHDPro freq_limits = (10e6, 24e9) # set in Hz @@ -124,14 +125,30 @@ def generate_code(self, hdf5_file): static_dtypes = [(f'freq{i:d}',np.float64) for i in self.allowed_chans] +\ [(f'amp{i:d}',np.float64) for i in self.allowed_chans] +\ - [(f'phase{i:d}',np.float64) for i in self.allowed_chans] + [(f'phase{i:d}',np.float64) for i in self.allowed_chans] +\ + [(f'gate{i:d}',bool) for i in self.allowed_chans] static_table = np.zeros(1,dtype=static_dtypes) for connection in DDSs: static_table[f'freq{connection}'] = dds.frequency.raw_output static_table[f'amp{connection}'] = dds.amplitude.raw_output static_table[f'phase{connection}'] = dds.phase.raw_output + static_table[f'gate{connection}'] = self.enabled_chans[connection] grp = self.init_device_group(hdf5_file) grp.create_dataset('STATIC_DATA',compression=config.compression,data=static_table) + def enable_output(self, channel): + """Enable an output channel at the device level. + + This is a software enable only, it cannot be hardware timed. + + Args: + channel (int): Channel to enable. + """ + + if channel in self.allowed_chans: + if channel not in self.enabled_chans: + self.enabled_chans.append(channel) + else: + raise LabscriptError(f'Channel {channel} is not a valid option for {self.device.name}.') From 036ac3ff324f49a5909648504ad3e1eae5c22cff Mon Sep 17 00:00:00 2001 From: David Meyer Date: Fri, 4 Mar 2022 14:14:46 -0500 Subject: [PATCH 07/16] Fix error in software gating. --- labscript_devices/Windfreak/labscript_devices.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/labscript_devices/Windfreak/labscript_devices.py b/labscript_devices/Windfreak/labscript_devices.py index 86ffe6b5..a14b0b52 100644 --- a/labscript_devices/Windfreak/labscript_devices.py +++ b/labscript_devices/Windfreak/labscript_devices.py @@ -133,7 +133,7 @@ def generate_code(self, hdf5_file): static_table[f'freq{connection}'] = dds.frequency.raw_output static_table[f'amp{connection}'] = dds.amplitude.raw_output static_table[f'phase{connection}'] = dds.phase.raw_output - static_table[f'gate{connection}'] = self.enabled_chans[connection] + static_table[f'gate{connection}'] = connection in self.enabled_chans grp = self.init_device_group(hdf5_file) grp.create_dataset('STATIC_DATA',compression=config.compression,data=static_table) From 271e33fee70d5182fa1fa07f5ded5aa3c0a4ba84 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Wed, 16 Mar 2022 07:45:34 -0400 Subject: [PATCH 08/16] Fix errors in code generation such that only programmed channels generate data when compiling a shot. --- .../Windfreak/labscript_devices.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/labscript_devices/Windfreak/labscript_devices.py b/labscript_devices/Windfreak/labscript_devices.py index a14b0b52..3cefc45e 100644 --- a/labscript_devices/Windfreak/labscript_devices.py +++ b/labscript_devices/Windfreak/labscript_devices.py @@ -107,8 +107,9 @@ def generate_code(self, hdf5_file): try: prefix, channel = output.connection.split() + channel = int(channel) if channel not in self.allowed_chans: - LabscriptError(f"Channel {channel} must be 0 or 0") + LabscriptError(f"Channel {channel} must be 0 or 1") except: msg = f"""{output.description}:{output.name} has invalid connection string. Only 'channel 0' or 'channel 1' is allowed. @@ -117,26 +118,30 @@ def generate_code(self, hdf5_file): DDSs[channel] = output + # get which channels to program + stat_DDSs = set(DDSs)&set(range(2)) for connection in DDSs: dds = DDSs[connection] dds.frequency.raw_output = self.validate_data(dds.frequency.static_value,self.freq_limits,dds) dds.amplitude.raw_output = self.validate_data(dds.amplitude.static_value,self.amp_limits,dds) dds.phase.raw_output = self.validate_data(dds.phase.static_value,self.phase_limits,dds) - static_dtypes = [(f'freq{i:d}',np.float64) for i in self.allowed_chans] +\ - [(f'amp{i:d}',np.float64) for i in self.allowed_chans] +\ - [(f'phase{i:d}',np.float64) for i in self.allowed_chans] +\ - [(f'gate{i:d}',bool) for i in self.allowed_chans] + static_dtypes = [(f'freq{i:d}',np.float64) for i in stat_DDSs] +\ + [(f'amp{i:d}',np.float64) for i in stat_DDSs] +\ + [(f'phase{i:d}',np.float64) for i in stat_DDSs] +\ + [(f'gate{i:d}',bool) for i in stat_DDSs] static_table = np.zeros(1,dtype=static_dtypes) for connection in DDSs: + dds = DDSs[connection] static_table[f'freq{connection}'] = dds.frequency.raw_output static_table[f'amp{connection}'] = dds.amplitude.raw_output static_table[f'phase{connection}'] = dds.phase.raw_output static_table[f'gate{connection}'] = connection in self.enabled_chans grp = self.init_device_group(hdf5_file) - grp.create_dataset('STATIC_DATA',compression=config.compression,data=static_table) + if stat_DDSs: + grp.create_dataset('STATIC_DATA',compression=config.compression,data=static_table) def enable_output(self, channel): """Enable an output channel at the device level. From 7a63e16eaa5d1327bcd608903885b1434c5fd0d0 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Wed, 16 Mar 2022 07:46:52 -0400 Subject: [PATCH 09/16] Fix issue with windfreak API that doesn't handle the `np.bool_` type correctly. Also fix accidental override of internal `type` command with function local variables. --- labscript_devices/Windfreak/blacs_workers.py | 24 ++++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/labscript_devices/Windfreak/blacs_workers.py b/labscript_devices/Windfreak/blacs_workers.py index 8777efb1..5e1fd307 100644 --- a/labscript_devices/Windfreak/blacs_workers.py +++ b/labscript_devices/Windfreak/blacs_workers.py @@ -13,6 +13,7 @@ from blacs.tab_base_classes import Worker import labscript_utils.h5_lock, h5py +import numpy as np class WindfreakSynthWorker(Worker): @@ -79,28 +80,31 @@ def program_manual(self, front_panel_values): return self.check_remote_values() - def check_remote_value(self,channel,type): + def check_remote_value(self,channel,typ): - if type == 'freq': + if typ == 'freq': return self.synth[channel].frequency - elif type == 'amp': + elif typ == 'amp': return self.synth[channel].power - elif type == 'phase': + elif typ == 'phase': return self.synth[channel].phase - elif type == 'gate': + elif typ == 'gate': return self.synth[channel].rf_enable and self.synth[channel].pll_enable else: raise ValueError(type) - def program_static_value(self,channel,type,value): + def program_static_value(self,channel,typ,value): - if type == 'freq': + if typ == 'freq': self.synth[channel].frequency = value - elif type == 'amp': + elif typ == 'amp': self.synth[channel].power = value - elif type == 'phase': + elif typ == 'phase': self.synth[channel].phase = value - elif type == 'gate': + elif typ == 'gate': + # windfreak API does not like np.bool_ + # convert to native python bool + if isinstance(value, np.bool_): value = value.item() self.synth[channel].rf_enable = value self.synth[channel].pll_enable = value else: From ab37dfecf4bf89c6e983907af8b32a4bb0bb71ee Mon Sep 17 00:00:00 2001 From: David Meyer Date: Wed, 16 Mar 2022 07:48:00 -0400 Subject: [PATCH 10/16] Fix issues with transition_to_buffered. Fixes smart cache not updating correctly and adds necessary abort function boilerplate. --- labscript_devices/Windfreak/blacs_workers.py | 25 ++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/labscript_devices/Windfreak/blacs_workers.py b/labscript_devices/Windfreak/blacs_workers.py index 5e1fd307..a0fe5cb4 100644 --- a/labscript_devices/Windfreak/blacs_workers.py +++ b/labscript_devices/Windfreak/blacs_workers.py @@ -132,10 +132,8 @@ def transition_to_buffered(self, device_name, h5file, initial_values, fresh): for i in channels: for sub in self.subchnls: self.program_static_value(i,sub,data[sub+str(i)]) - - # update smart cache to reflect programmed values - self.smart_cache['STATIC_DATA'] = data - + # update smart cache to reflect programmed values + self.smart_cache['STATIC_DATA'][f'channel {i:d}'][sub] = data[sub+str(i)] return self.final_values @@ -143,3 +141,22 @@ def shutdown(self): # save current state the memory self.synth.save() self.synth.close() + + def abort_transition_to_buffered(self): + """Special abort shot configuration code belongs here. + """ + return self.transition_to_manual(True) + + def abort_buffered(self): + """Special abort shot code belongs here. + """ + return self.transition_to_manual(True) + + def transition_to_manual(self,abort = False): + """Simple transition_to_manual method where no data is saved.""" + if abort: + # If we're aborting the run, reset to original value + self.program_manual(self.initial_values) + # If we're not aborting the run, stick with buffered value. Nothing to do really! + # return the current values in the device + return True From 69fc39c285a2bfbf474961c673529938ece6441a Mon Sep 17 00:00:00 2001 From: David Meyer Date: Wed, 16 Mar 2022 07:59:09 -0400 Subject: [PATCH 11/16] Fix issue where updated final values did not reflect most recent shot. --- labscript_devices/Windfreak/blacs_workers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/labscript_devices/Windfreak/blacs_workers.py b/labscript_devices/Windfreak/blacs_workers.py index a0fe5cb4..67402b77 100644 --- a/labscript_devices/Windfreak/blacs_workers.py +++ b/labscript_devices/Windfreak/blacs_workers.py @@ -134,6 +134,8 @@ def transition_to_buffered(self, device_name, h5file, initial_values, fresh): self.program_static_value(i,sub,data[sub+str(i)]) # update smart cache to reflect programmed values self.smart_cache['STATIC_DATA'][f'channel {i:d}'][sub] = data[sub+str(i)] + # update final values to reflect programmed values + self.final_values[f'channel {i:d}'][sub] = data[sub+str(i)] return self.final_values From 190942c04b414ac517f812eae40cca88ad803ebb Mon Sep 17 00:00:00 2001 From: David Meyer Date: Fri, 22 Sep 2023 11:38:38 -0400 Subject: [PATCH 12/16] Prevent windfreak from programming a channel if it doesn't need to. --- labscript_devices/Windfreak/blacs_workers.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/labscript_devices/Windfreak/blacs_workers.py b/labscript_devices/Windfreak/blacs_workers.py index 67402b77..d35ea800 100644 --- a/labscript_devices/Windfreak/blacs_workers.py +++ b/labscript_devices/Windfreak/blacs_workers.py @@ -131,11 +131,12 @@ def transition_to_buffered(self, device_name, h5file, initial_values, fresh): for i in channels: for sub in self.subchnls: - self.program_static_value(i,sub,data[sub+str(i)]) - # update smart cache to reflect programmed values - self.smart_cache['STATIC_DATA'][f'channel {i:d}'][sub] = data[sub+str(i)] - # update final values to reflect programmed values - self.final_values[f'channel {i:d}'][sub] = data[sub+str(i)] + if initial_values[f'channel {i:d}'][sub] != data[sub+str(i)]: + self.program_static_value(i,sub,data[sub+str(i)]) + # update smart cache to reflect programmed values + self.smart_cache['STATIC_DATA'][f'channel {i:d}'][sub] = data[sub+str(i)] + # update final values to reflect programmed values + self.final_values[f'channel {i:d}'][sub] = data[sub+str(i)] return self.final_values From 7b9fcfadd67e5aef5f431af7efd3a49c0a4920e5 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Mon, 11 Dec 2023 11:41:41 -0500 Subject: [PATCH 13/16] Add frequency reference control and general cleanup and docs. --- docs/source/devices.rst | 1 + docs/source/devices/windfreak.rst | 120 ++++++++++++++++++ labscript_devices/Windfreak/blacs_tabs.py | 10 +- labscript_devices/Windfreak/blacs_workers.py | 91 ++++++++++--- .../Windfreak/labscript_devices.py | 38 +++++- .../Windfreak/register_classes.py | 10 +- 6 files changed, 238 insertions(+), 32 deletions(-) create mode 100644 docs/source/devices/windfreak.rst diff --git a/docs/source/devices.rst b/docs/source/devices.rst index 6237a18e..82f6c0e4 100644 --- a/docs/source/devices.rst +++ b/docs/source/devices.rst @@ -59,6 +59,7 @@ These devices cover various frequency sources that provide either hardware-timed devices/novatechDDS9m devices/phasematrixquicksyn + devices/windfreak Miscellaneous diff --git a/docs/source/devices/windfreak.rst b/docs/source/devices/windfreak.rst new file mode 100644 index 00000000..1f48421c --- /dev/null +++ b/docs/source/devices/windfreak.rst @@ -0,0 +1,120 @@ +Windfreak Synth +=============== + +This labscript device controls the Windfreak SynthHD and SynthHD Pro signal generators. + +At present only static frequencies and DDS gating is supported. +This driver also supports external referencing. + + +Installation +~~~~~~~~~~~~ + +This driver requires the `windfreak` package available on pip. +If using a version of Windows older than 10, +you will need to install the usb driver available from windfreak. + +Usage +~~~~~ + +Below is a basic script using the driver. + +.. code-block:: python + + from labscript import * + + from labscript_devices.PrawnBlaster.labscript_devices import PrawnBlaster + from labscript_devices.Windfreak.labscript_devices import WindfreakSynthHDPro + + PrawnBlaster(name='prawn', com_port='COM6', num_pseudoclocks=1) + + WindfreakSynthHDPro(name='WF', com_port="COM7") + + StaticDDS('WF_A', WF, 'channel 0') + + if __name__ == '__main__': + + WF.enable_output(0) # enables channel A (0) + WF_A.setfreq(10, units = 'GHz') + WF_A.setamp(-24) # in dBm + WF_A.setphase(45) # in deg + + start(0) + stop(1) + +This driver supports the DDS Gate feature which can provide dynamic TTL control of the outputs. +This is done by enabling the `rf_enable` triggering mode on the synth, +as well as setting the correct `digital_gate` on the output. +Note that both outputs will be toggled on/off when using `rf_enable` modulation. + +It also supports external referencing of the device. +The below script uses external clocking and gating features. + +.. code-block:: python + + from labscript import * + + from labscript_devices.PrawnBlaster.labscript_devices import PrawnBlaster + from labscript_devices.Windfreak.labscript_devices import WindfreakSynthHDPro + from labscript_devices.NI_DAQmx.Models.NI_USB_6343 import NI_USB_6343 + + PrawnBlaster(name='prawn', com_port='COM6', num_pseudoclocks=1) + + NI_USB_6343(name='ni_6343', parent_device=prawn.clocklines[0], + clock_terminal='/ni_usb_6343/PFI0', + MAX_name='ni_usb_6343', + ) + + WindfreakSynthHDPro(name='WF', com_port="COM7", + trigger_mode='rf enable', + reference_mode='external', + reference_frequency=10e6) + + StaticDDS('WF_A', WF, 'channel 0', + digital_gate={'device':ni_6343, 'connection':'port0/line0'}) + + if __name__ == '__main__': + + WF.enable_output(0) # enables channel A (0) + WF_A.setfreq(10, units = 'GHz') + WF_A.setamp(-24) # in dBm + WF_A.setphase(45) # in deg + + t = 0 + start(t) + + # enable rf via digital gate for 1 ms at 10 ms + t = 10e-3 + WF_A.enable(t) + t += 1e-3 + WF_A.disable(t) + + stop(t+1e-3) + + +Detailed Documentation +~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: labscript_devices.Windfreak + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.Windfreak.labscript_devices + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.Windfreak.blacs_tabs + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.Windfreak.blacs_workers + :members: + :undoc-members: + :show-inheritance: + :private-members: diff --git a/labscript_devices/Windfreak/blacs_tabs.py b/labscript_devices/Windfreak/blacs_tabs.py index fbb89550..5265f427 100644 --- a/labscript_devices/Windfreak/blacs_tabs.py +++ b/labscript_devices/Windfreak/blacs_tabs.py @@ -14,11 +14,11 @@ from blacs.device_base_class import DeviceTab -class WindfreakSynthTab(DeviceTab): +class WindfreakSynthHDTab(DeviceTab): def __init__(self, *args, **kwargs): if not hasattr(self,'device_worker_class'): - self.device_worker_class = 'labscript_devices.Windfreak.blacs_workers.WindfreakSynthWorker' + self.device_worker_class = 'labscript_devices.Windfreak.blacs_workers.WindfreakSynthHDWorker' DeviceTab.__init__(self, *args, **kwargs) def initialise_GUI(self): @@ -69,9 +69,13 @@ def initialise_workers(self): conn_obj = self.settings['connection_table'].find_by_name(self.device_name).properties self.com_port = conn_obj.get('com_port',None) self.trigger_mode = conn_obj.get('trigger_mode','disabled') + self.reference_mode = conn_obj['reference_mode'] + self.reference_frequency = conn_obj['reference_frequency'] self.create_worker('main_worker',self.device_worker_class,{'com_port':self.com_port, 'allowed_chans':self.allowed_chans, - 'trigger_mode':self.trigger_mode}) + 'trigger_mode':self.trigger_mode, + 'reference_mode':self.reference_mode, + 'reference_frequency':self.reference_frequency}) self.primary_worker = 'main_worker' diff --git a/labscript_devices/Windfreak/blacs_workers.py b/labscript_devices/Windfreak/blacs_workers.py index d35ea800..418d1c30 100644 --- a/labscript_devices/Windfreak/blacs_workers.py +++ b/labscript_devices/Windfreak/blacs_workers.py @@ -16,7 +16,7 @@ import numpy as np -class WindfreakSynthWorker(Worker): +class WindfreakSynthHDWorker(Worker): def init(self): # hide import of 3rd-party library in here so docs don't need it @@ -32,12 +32,41 @@ def init(self): # connect to synth self.synth = windfreak.SynthHD(self.com_port) self.valid_modes = self.synth.trigger_modes + self.valid_ref_modes = self.synth.reference_modes + # set reference mode + self.set_reference_mode(self.reference_mode, self.reference_frequency) # set trigger mode from connection_table_properties self.set_trigger_mode(self.trigger_mode) # populate smart chache self.smart_cache['STATIC_DATA'] = self.check_remote_values() + def set_reference_mode(self, mode, ext_freq): + """Sets the synth reference mode. + + Provides basic error checking that setting is valid. + + Args: + mode (str): Valid reference modes are `external`, `internal 27mhz` + and `internal 10mhz`. If mode is external, ext_freq must be provided. + ext_freq (float): Frequency of external reference. + If using internal reference, pass `None`. + + Raises: + ValueError: if `mode` is not a valid setting or `ext_ref` not provided + when using an external reference. + """ + + if mode == 'external' and ext_freq is None: + raise ValueError('Must specify external reference frequency') + + if mode in self.valid_ref_modes: + self.synth.reference_mode = mode + if mode == 'external': + self.synth.reference_frequency = ext_freq + else: + raise ValueError(f'{mode} not in {self.valid_ref_modes}') + def set_trigger_mode(self,mode): """Sets the synth trigger mode. @@ -81,6 +110,16 @@ def program_manual(self, front_panel_values): return self.check_remote_values() def check_remote_value(self,channel,typ): + """Checks the remote value of a parameter for a channel. + + Args: + channel (int): Which channel to check. Must be 0 or 1. + typ (str): Which parameter to get. Must be `freq`, `amp`, `phase` + or `gate`. + + Raises: + ValueError: If `typ` is not a valid parameter type for the channel. + """ if typ == 'freq': return self.synth[channel].frequency @@ -91,9 +130,21 @@ def check_remote_value(self,channel,typ): elif typ == 'gate': return self.synth[channel].rf_enable and self.synth[channel].pll_enable else: - raise ValueError(type) + raise ValueError(typ) def program_static_value(self,channel,typ,value): + """Program a value for the specified parameter of the channel. + + Args: + channel (int): Channel to program. Must be 0 or 1. + typ (str): Parameter to program. Must be `freq`, `amp`, `phase`, + or `gate`. + value (float or bool): Value to program. `gate` takes a boolean type, + all others take a float. + + Raises: + ValueError: If requested parameter type is not valid. + """ if typ == 'freq': self.synth[channel].frequency = value @@ -104,11 +155,12 @@ def program_static_value(self,channel,typ,value): elif typ == 'gate': # windfreak API does not like np.bool_ # convert to native python bool - if isinstance(value, np.bool_): value = value.item() + if isinstance(value, np.bool_): + value = value.item() self.synth[channel].rf_enable = value self.synth[channel].pll_enable = value else: - raise ValueError(type) + raise ValueError(typ) def transition_to_buffered(self, device_name, h5file, initial_values, fresh): @@ -122,21 +174,20 @@ def transition_to_buffered(self, device_name, h5file, initial_values, fresh): static_data = group['STATIC_DATA'][:][0] if static_data is not None: - data = static_data - if fresh or data != self.smart_cache['STATIC_DATA']: - - # need to infer which channels are programming - num_chan = len(data)//len(self.subchnls) - channels = [int(name[-1]) for name in data.dtype.names[0:num_chan]] - - for i in channels: - for sub in self.subchnls: - if initial_values[f'channel {i:d}'][sub] != data[sub+str(i)]: - self.program_static_value(i,sub,data[sub+str(i)]) - # update smart cache to reflect programmed values - self.smart_cache['STATIC_DATA'][f'channel {i:d}'][sub] = data[sub+str(i)] - # update final values to reflect programmed values - self.final_values[f'channel {i:d}'][sub] = data[sub+str(i)] + + # need to infer which channels are programming + num_chan = len(static_data)//len(self.subchnls) + channels = [int(name[-1]) for name in static_data.dtype.names[0:num_chan]] + + for i in channels: + for sub in self.subchnls: + desired_value = static_data[sub+str(i)] + if self.smart_cache['STATIC_DATA'][f'channel {i:d}'][sub] != desired_value or fresh: + self.program_static_value(i,sub,desired_value) + # update smart cache to reflect programmed values + self.smart_cache['STATIC_DATA'][f'channel {i:d}'][sub] = desired_value + # update final values to reflect programmed values + self.final_values[f'channel {i:d}'][sub] = desired_value return self.final_values @@ -161,5 +212,5 @@ def transition_to_manual(self,abort = False): # If we're aborting the run, reset to original value self.program_manual(self.initial_values) # If we're not aborting the run, stick with buffered value. Nothing to do really! - # return the current values in the device + return True diff --git a/labscript_devices/Windfreak/labscript_devices.py b/labscript_devices/Windfreak/labscript_devices.py index 3cefc45e..6b99266e 100644 --- a/labscript_devices/Windfreak/labscript_devices.py +++ b/labscript_devices/Windfreak/labscript_devices.py @@ -11,22 +11,22 @@ # # ##################################################################### -from labscript import LabscriptError, set_passed_properties, config, StaticDDS, IntermediateDevice, Device +from labscript import LabscriptError, set_passed_properties, config, StaticDDS, Device from labscript_utils import dedent from labscript_utils.unitconversions.generic_frequency import FreqConversion import numpy as np -class WindfreakSynth(Device): - description = 'Windfreak HDPro Synthesizer' +class WindfreakSynthHD(Device): + description = 'Windfreak HD Synthesizer' allowed_children = [StaticDDS] # note, box labels 'A', 'B' map to programming channels 0, 1 allowed_chans = [0, 1] enabled_chans = [] # define output limitations for the SynthHDPro - freq_limits = (10e6, 24e9) # set in Hz + freq_limits = (10e6, 15e9) # set in Hz freq_res = 1 # number of sig digits after decimal amp_limits = (-40.0, 20.0) # set in dBm amp_res = 2 @@ -44,9 +44,13 @@ class WindfreakSynth(Device): 'phase_limits', 'phase_res', 'trigger_mode', + 'reference_mode', + 'reference_frequency', ] }) - def __init__(self, name, com_port="", trigger_mode='disabled', **kwargs): + def __init__(self, name, com_port="", trigger_mode='disabled', + reference_mode='internal 27mhz', + reference_frequency=None, **kwargs): """Creates a Windfreak HDPro Synthesizer Args: @@ -54,17 +58,25 @@ def __init__(self, name, com_port="", trigger_mode='disabled', **kwargs): com_port (str): COM port connection string. Must take the form of 'COM d', where d is an integer. trigger_mode (str): Trigger mode for the device to use. - Currently, labscript only directly programs 'rf enable', + Currently, labscript only directly supports `'rf enable'`, via setting DDS gates. labscript could correctly program other modes with some effort. Other modes can be correctly programmed externally, with the settings saved to EEPROM. - **kwargs: Keyword arguments passed to :obj:`labscript:labscript.Device.__init__`. + reference_mode (str): Frequency reference mode to use. + Valid options are 'external', 'internal 27mhz', and 'internal 10mhz'. + Default is 'internal 27mhz'. + reference_frequency (float): Reference frequency (in Hz) + when using an external frequency. + Valid values are between 10 and 100 MHz. + **kwargs: Keyword arguments passed to :obj:`labscript:labscript.Device.__init__`. """ Device.__init__(self, name, None, com_port, **kwargs) self.BLACS_connection = com_port self.trigger_mode = trigger_mode + self.reference_mode = reference_mode + self.reference_frequency = reference_frequency def add_device(self, device): Device.add_device(self, device) @@ -157,3 +169,15 @@ def enable_output(self, channel): self.enabled_chans.append(channel) else: raise LabscriptError(f'Channel {channel} is not a valid option for {self.device.name}.') + + +class WindfreakSynthHDPro(WindfreakSynthHD): + description = 'Windfreak HDPro Synthesizer' + + # define output limitations for the SynthHDPro + freq_limits = (10e6, 24e9) # set in Hz + freq_res = 1 # number of sig digits after decimal + amp_limits = (-40.0, 20.0) # set in dBm + amp_res = 2 + phase_limits = (0.0, 360.0) # in deg + phase_res = 2 \ No newline at end of file diff --git a/labscript_devices/Windfreak/register_classes.py b/labscript_devices/Windfreak/register_classes.py index 1d93a66c..f31776fe 100644 --- a/labscript_devices/Windfreak/register_classes.py +++ b/labscript_devices/Windfreak/register_classes.py @@ -14,7 +14,13 @@ import labscript_devices labscript_devices.register_classes( - 'WindfreakSynth', - BLACS_tab='labscript_devices.Windfreak.blacs_tabs.WindfreakSynthTab', + 'WindfreakSynthHD', + BLACS_tab='labscript_devices.Windfreak.blacs_tabs.WindfreakSynthHDTab', + runviewer_parser=None +) + +labscript_devices.register_classes( + 'WindfreakSynthHDPro', + BLACS_tab='labscript_devices.Windfreak.blacs_tabs.WindfreakSynthHDTab', runviewer_parser=None ) \ No newline at end of file From 0daf81f6719734aa17311ba15581986d2f9e10d3 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Mon, 18 Mar 2024 10:17:34 -0400 Subject: [PATCH 14/16] Fix bug where windfreak enabled channels persist between compilations. --- labscript_devices/Windfreak/labscript_devices.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/labscript_devices/Windfreak/labscript_devices.py b/labscript_devices/Windfreak/labscript_devices.py index 6b99266e..6a50f549 100644 --- a/labscript_devices/Windfreak/labscript_devices.py +++ b/labscript_devices/Windfreak/labscript_devices.py @@ -23,7 +23,6 @@ class WindfreakSynthHD(Device): allowed_children = [StaticDDS] # note, box labels 'A', 'B' map to programming channels 0, 1 allowed_chans = [0, 1] - enabled_chans = [] # define output limitations for the SynthHDPro freq_limits = (10e6, 15e9) # set in Hz @@ -77,6 +76,7 @@ def __init__(self, name, com_port="", trigger_mode='disabled', self.trigger_mode = trigger_mode self.reference_mode = reference_mode self.reference_frequency = reference_frequency + self.enabled_chans = [] def add_device(self, device): Device.add_device(self, device) From 78706d1ad3a3ace09521751333a32e77017447fb Mon Sep 17 00:00:00 2001 From: David Meyer Date: Thu, 23 May 2024 12:19:29 -0400 Subject: [PATCH 15/16] Have the worker print out device information once connected. --- labscript_devices/Windfreak/blacs_workers.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/labscript_devices/Windfreak/blacs_workers.py b/labscript_devices/Windfreak/blacs_workers.py index 418d1c30..8f2f6fda 100644 --- a/labscript_devices/Windfreak/blacs_workers.py +++ b/labscript_devices/Windfreak/blacs_workers.py @@ -31,6 +31,7 @@ def init(self): # connect to synth self.synth = windfreak.SynthHD(self.com_port) + self.device_info() self.valid_modes = self.synth.trigger_modes self.valid_ref_modes = self.synth.reference_modes # set reference mode @@ -41,6 +42,20 @@ def init(self): # populate smart chache self.smart_cache['STATIC_DATA'] = self.check_remote_values() + def device_info(self): + """Print device info for connected device""" + + fw_ver = self.synth.firmware_version + hw_ver = self.synth.hardware_version + model = self.synth.model + sn = self.synth.serial_number + + info = f"""Connected to: + {model}: SN {sn} + {fw_ver}, {hw_ver} + """ + print(info) + def set_reference_mode(self, mode, ext_freq): """Sets the synth reference mode. From 9f2de82cdfbb65ad8720dac31cdc52c03ca7a3d0 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Thu, 23 May 2024 13:27:01 -0400 Subject: [PATCH 16/16] Add note to docs about observed issues with windfreak outputs not updating correctly. --- docs/source/devices/windfreak.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/source/devices/windfreak.rst b/docs/source/devices/windfreak.rst index 1f48421c..65ff8695 100644 --- a/docs/source/devices/windfreak.rst +++ b/docs/source/devices/windfreak.rst @@ -6,6 +6,12 @@ This labscript device controls the Windfreak SynthHD and SynthHD Pro signal gene At present only static frequencies and DDS gating is supported. This driver also supports external referencing. +.. note:: + + There have been observed, infrequent instances where the device does not update to newly programmed values. + This does not appear to be an issue with this code, but rather the device or the `windfreak` package. + As with any new hardware; trust, but verify, the output. + If you can reliably reproduce the problem, please create an issue so it can be addressed. Installation ~~~~~~~~~~~~