diff --git a/docs/source/devices.rst b/docs/source/devices.rst index 6237a18..82f6c0e 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 0000000..65ff869 --- /dev/null +++ b/docs/source/devices/windfreak.rst @@ -0,0 +1,126 @@ +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. + +.. 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 +~~~~~~~~~~~~ + +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/__init__.py b/labscript_devices/Windfreak/__init__.py new file mode 100644 index 0000000..69d0cf4 --- /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 0000000..5265f42 --- /dev/null +++ b/labscript_devices/Windfreak/blacs_tabs.py @@ -0,0 +1,81 @@ +##################################################################### +# # +# /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 + + +class WindfreakSynthHDTab(DeviceTab): + + def __init__(self, *args, **kwargs): + if not hasattr(self,'device_worker_class'): + self.device_worker_class = 'labscript_devices.Windfreak.blacs_workers.WindfreakSynthHDWorker' + 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':{},'gate':{}} + 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.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, + '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 new file mode 100644 index 0000000..8f2f6fd --- /dev/null +++ b/labscript_devices/Windfreak/blacs_workers.py @@ -0,0 +1,231 @@ +##################################################################### +# # +# /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 +import numpy as np + + +class WindfreakSynthHDWorker(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','gate'] + # this will be the order in which each channel is programmed + + Worker.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 + 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 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. + + 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. + + 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 = {} + 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,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 + elif typ == 'amp': + return self.synth[channel].power + elif typ == 'phase': + return self.synth[channel].phase + elif typ == 'gate': + return self.synth[channel].rf_enable and self.synth[channel].pll_enable + else: + 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 + elif typ == 'amp': + self.synth[channel].power = value + elif typ == 'phase': + self.synth[channel].phase = value + 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: + raise ValueError(typ) + + 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: + + # 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 + + 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 True diff --git a/labscript_devices/Windfreak/labscript_devices.py b/labscript_devices/Windfreak/labscript_devices.py new file mode 100644 index 0000000..6a50f54 --- /dev/null +++ b/labscript_devices/Windfreak/labscript_devices.py @@ -0,0 +1,183 @@ +##################################################################### +# # +# /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, Device +from labscript_utils import dedent +from labscript_utils.unitconversions.generic_frequency import FreqConversion + +import numpy as np + + +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] + + # define output limitations for the SynthHDPro + 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 + 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', + 'trigger_mode', + 'reference_mode', + 'reference_frequency', + ] + }) + def __init__(self, name, com_port="", trigger_mode='disabled', + reference_mode='internal 27mhz', + reference_frequency=None, **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 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. + 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 + self.enabled_chans = [] + + def add_device(self, device): + Device.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): + """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]): + 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() + channel = int(channel) + if channel not in self.allowed_chans: + 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. + """ + raise LabscriptError(dedent(msg)) + + 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 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) + 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. + + 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}.') + + +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 new file mode 100644 index 0000000..f31776f --- /dev/null +++ b/labscript_devices/Windfreak/register_classes.py @@ -0,0 +1,26 @@ +##################################################################### +# # +# /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( + '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