#####################################################################
# #
# /hardware_interfaces/output_classes.py #
# #
# Copyright 2013, Monash University #
# #
# This file is part of the program BLACS, 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 logging
import math
import sys
from qtutils.qt.QtCore import *
from qtutils.qt.QtGui import *
from qtutils.qt.QtWidgets import *
from labscript_utils.qtwidgets.analogoutput import AnalogOutput
from labscript_utils.qtwidgets.digitaloutput import DigitalOutput, InvertedDigitalOutput
from labscript_utils.qtwidgets.ddsoutput import DDSOutput
from labscript_utils.qtwidgets.imageoutput import ImageOutput
from labscript_utils.unitconversions import get_unit_conversion_class
[docs]class AO(object):
[docs] def __init__(self, hardware_name, connection_name, device_name, program_function, settings, calib_class, calib_params, default_units, min, max, step, decimals):
self._connection_name = connection_name
self._hardware_name = hardware_name
self._device_name = device_name
self._locked = False
self._comboboxmodel = QStandardItemModel()
self._widgets = []
self._current_units = default_units
self._base_unit = default_units
self._program_device = program_function
# All of these are in base units ALWAYS
self._current_value = 0 # value in base units
self._current_step_size = step # step size in current units
self._step_size = step # step size in base units
self._limits = [min,max]
self._decimals = decimals
self._logger = logging.getLogger('BLACS.%s.%s'%(self._device_name,hardware_name))
# Initialise Calibrations
self._comboboxmodel.appendRow(QStandardItem(self._base_unit))
if calib_class is not None:
try:
cls = get_unit_conversion_class(calib_class)
# ImportError if module doesn't exist. Attribute error if it does but the
# class name does not exist within it. KeyError if the unit conversion class
# is an old-style unqualified class name expected to be in the globals()
# dict of the unitconversions module, but does not exist.
except (ImportError, AttributeError, KeyError):
cls = None
if cls is None or not isinstance(calib_params, dict) or cls.base_unit != default_units:
# log an error:
reason = ''
if cls is None:
reason = f'The unit conversion class {calib_class} could not be imported. Ensure it is available on the computer running BLACS.'
elif not isinstance(calib_params, dict):
reason = 'The parameters for the unit conversion class are not a dictionary. Check your connection table code for errors and recompile it'
elif cls.base_unit != default_units:
reason = f'The base unit of your unit conversion class does not match this hardware channel. The hardware channel has base units {default_units} while your unit conversion class uses {cls.base_unit}'
self._logger.error('The unit conversion class (%s) could not be loaded. Reason: %s'%(calib_class,reason))
# Use default units
self._calibration = None
else:
try:
# initialise calibration class
self._calibration = cls(calib_params)
self._logger.debug('unit conversion class instantiated')
for unit in self._calibration.derived_units:
try:
self._comboboxmodel.appendRow(QStandardItem(unit))
except Exception:
self._logger.exception('Error while trying to add unit "%s"'%unit)
except Exception:
self._logger.exception('Error while trying to instantiate unit conversion class')
self._calibration = None
else:
# use default units
self._calibration = None
self._logger.debug('No unit conversion class specified')
self._update_from_settings(settings,program=False)
def _update_from_settings(self,settings,program=True):
# Build up the settings dictionary if it isn't already
if not isinstance(settings,dict):
settings = {}
if 'front_panel_settings' not in settings or not isinstance(settings['front_panel_settings'],dict):
settings['front_panel_settings'] = {}
if self._hardware_name not in settings['front_panel_settings'] or not isinstance(settings['front_panel_settings'][self._hardware_name],dict):
settings['front_panel_settings'][self._hardware_name] = {}
# Set default values if they are not already saved in the settings dictionary
if 'base_value' not in settings['front_panel_settings'][self._hardware_name]:
settings['front_panel_settings'][self._hardware_name]['base_value'] = False
if 'locked' not in settings['front_panel_settings'][self._hardware_name]:
settings['front_panel_settings'][self._hardware_name]['locked'] = False
if 'base_step_size' not in settings['front_panel_settings'][self._hardware_name]:
settings['front_panel_settings'][self._hardware_name]['base_step_size'] = self._step_size
if 'current_units' not in settings['front_panel_settings'][self._hardware_name]:
settings['front_panel_settings'][self._hardware_name]['current_units'] = self._base_unit
if 'name' not in settings['front_panel_settings'][self._hardware_name]:
settings['front_panel_settings'][self._hardware_name]['name'] = self._connection_name
# only keep a reference to the part of the settings dictionary relevant to this DO
self._settings = settings['front_panel_settings'][self._hardware_name]
# Update the state of the button
self.set_value(self._settings['base_value'],program=program)
# Update the lock state
self._update_lock(self._settings['locked'])
# Update the step size
self.set_step_size(self._settings['base_step_size'],self._base_unit)
# Update the unit selection
if self._calibration and self._settings['current_units'] in self._calibration.derived_units:
self.change_unit(self._settings['current_units'],program=program)
else:
self.change_unit(self._base_unit,program=program)
[docs] def convert_value_to_base(self, value, unit):
if unit != self._base_unit:
if self._calibration and unit in self._calibration.derived_units:
return getattr(self._calibration,unit+"_to_base")(value)
# TODO: include device name somehow, and also the calibration class name
raise RuntimeError('The value %s (%s) could not be converted to base units because the hardware channel %s, named %s, either does not have a unit conversion class or the unit specified was invalid'%(str(value),unit,self._hardware_name,self._connection_name))
else:
return value
[docs] def convert_value_from_base(self, value, unit):
if unit != self._base_unit:
if self._calibration and unit in self._calibration.derived_units:
return getattr(self._calibration,unit+"_from_base")(value)
# TODO: include device name somehow, and also the calibration class name
raise RuntimeError('The value %s (%s) could not be converted to base units because the hardware channel %s, named %s, either does not have a unit conversion class or the unit specified was invalid'%(str(value),unit,self._hardware_name,self._connection_name))
else:
return value
# handles the conversion of a range centered on value in units to base units
# In other words, how big is "range" in base units assuming that you care about what range is
# between value-range/2 and value+range/2
#
# If value+range/2 or value-range/2 is outside of the limits, then we will shift the fraction of range
# used on the offending side of value
# If range is greater than the difference of the limits, we will return the difference between the limits
[docs] def convert_range_to_base(self,value,range,unit):
self._logger.debug('convert_range_to_base called. value: %f, range: %f, unit: %s'%(value,range,unit))
# Do we need to convert the limits?
if unit != self._base_unit:
limits = [self.convert_value_from_base(self._limits[0],unit),self.convert_value_from_base(self._limits[1],unit)]
if limits[0] > limits[1]:
limits[0],limits[1] = limits[1],limits[0]
else:
limits = self._limits
self._logger.debug('limits in unit: %s), limits=[%f,%f]'%(unit,limits[0],limits[1]))
# limits are now in the units given to the function!
# (As are range and value)
# If range is bigger than the difference of the limits, return the difference of the limits
# in base units
if range >= abs(limits[0]-limits[1]):
limits = [self.convert_value_to_base(limits[0],unit),self.convert_value_to_base(limits[1],unit)]
self._logger.debug('range bigger than range of limits, returning difference of limits')
return abs(limits[0]-limits[1])
# At this point, the range must fit inside the limits, so if we find we are out of bounds on one side,
# we can be certain shifting the fractions will not cause us to go out of bounds on the other side
positive_fraction = range/2.0
negative_fraction = range/2.0
self._logger.debug('fractions are.... positive_fraction: %f, negative_fraction: %f'%(positive_fraction, negative_fraction))
# If the value+range/2 is greater than the upper limit, shift the fraction
if value+positive_fraction > limits[1]:
positive_fraction = abs(limits[1]-value)
negative_fraction = abs(range-positive_fraction)
self._logger.debug('outside upper limit. positive_fraction: %f, negative_fraction: %f'%(positive_fraction, negative_fraction))
# Similarly if value-range/2 is less than the lower limit, shift the fraction
elif value-negative_fraction < limits[0]:
negative_fraction = abs(limits[0]-value)
positive_fraction = abs(range-negative_fraction)
self._logger.debug('outside lower limit. positive_fraction: %f, negative_fraction: %f'%(positive_fraction, negative_fraction))
self._logger.debug('converting values to base units')
# Now do the conversion!
bound1 = self.convert_value_to_base(value+positive_fraction,unit)
bound2 = self.convert_value_to_base(value-negative_fraction,unit)
self._logger.debug('range in base units is: %f'%(abs(bound1-bound2)))
return abs(bound1-bound2)
# This does the reverse of teh above function, with the same rules
[docs] def convert_range_from_base(self,value,range,unit):
# limits are always in base units
limits = self._limits
# limits are now in base units!
# (As are range and value)
# If range is bigger than the difference of the limits, return the difference of the limits
# in the specified units units
if range >= abs(limits[0]-limits[1]):
limits = [self.convert_value_from_base(limits[0],unit),self.convert_value_from_base(limits[1],unit)]
return abs(limits[0]-limits[1])
# At this point, the range must fit inside the limits, so if we find we are out of bounds on one side,
# we can be certain shifting the fractions will not cause us to go out of bounds on the other side
positive_fraction = range/2.0
negative_fraction = range/2.0
# If the value+range/2 is greater than the upper limit, shift the fraction
if value+positive_fraction > limits[1]:
positive_fraction = abs(limits[1]-value)
negative_fraction = abs(range-positive_fraction)
# Similarly if value-range/2 is less than the lower limit, shift the fraction
elif value-negative_fraction < limits[0]:
negative_fraction = abs(limits[0]-value)
positive_fraction = abs(range-negative_fraction)
# Now do the conversion!
bound1 = self.convert_value_from_base(value+positive_fraction,unit)
bound2 = self.convert_value_from_base(value-negative_fraction,unit)
return abs(bound1-bound2)
# If calling this method directly from outside the set_AO function in the analog widget
# you should NOT specify a value for new_AO.
[docs] def change_unit(self,unit,program=True):
# These values are always stored in base units!
property_value_list = [self._current_value,self._limits[0],self._limits[1]]
property_range_list = [self._step_size]
self._logger.debug('changing unit to %s'%unit)
self._logger.debug('Values in base units are: value: %f, lower_limit: %f, upper_limit: %f'%(property_value_list[0],property_value_list[1],property_value_list[2]))
self._logger.debug('ranges in base units are: step_size: %f'%(property_range_list[0]))
# Now convert to the new unit
if unit != self._base_unit:
for index,param in enumerate(property_value_list):
#convert each to base units
property_value_list[index] = self.convert_value_from_base(param,unit)
for index,param in enumerate(property_range_list):
#convert each to base units
property_range_list[index] = self.convert_range_from_base(self._current_value,param,unit)
self._logger.debug('Values in new unit are: value: %f, lower_limit: %f, upper_limit: %f'%(property_value_list[0],property_value_list[1],property_value_list[2]))
self._logger.debug('ranges in new unit are: step_size: %f'%(property_range_list[0]))
# figure out how many decimal points we need in the new unit
smallest_step = 10**(-self._decimals)
self._logger.debug('Smallest step size in base units: %f'%smallest_step)
smallest_step_in_new_unit = self.convert_range_from_base(self._current_value+smallest_step,smallest_step,unit)
self._logger.debug('Smallest step size in new_unit: %f'%smallest_step_in_new_unit)
try:
if smallest_step_in_new_unit > 1:
if smallest_step_in_new_unit > 10:
num_decimals = 0
else:
num_decimals = 1
else:
num_decimals = abs(math.floor(math.log10(smallest_step_in_new_unit))-2)
except Exception:
self._logger.warning('Failed to convert number of significant figures to new unit. Loss of precision likely (in manual mode) for this unit. Probably cause is a unit conversion class that imposes limits on the converted values.')
num_decimals = self._decimals
else:
num_decimals = self._decimals
# Store the current units
self._current_units = unit
self._settings['current_units'] = unit
# Check to see if the upper/lower bound has switched
if property_value_list[1] > property_value_list[2]:
property_value_list[1], property_value_list[2] = property_value_list[2], property_value_list[1]
# Now update all the widgets
for widget in self._widgets:
# Update the combo box
widget.block_combobox_signals()
widget.set_selected_unit(unit)
widget.unblock_combobox_signals()
# block the spinbox from emitting a signal
widget.block_spinbox_signals()
# Update the limits
widget.set_limits(property_value_list[1],property_value_list[2])
# Update the step size
widget.set_step_size(property_range_list[0])
# Update the decimals
widget.set_num_decimals(num_decimals)
# Update the value - This should be the last thing you do,
# otherwise it might get truncated or
# limited in a bad way
widget.set_spinbox_value(property_value_list[0],unit)
# unblock the spinbox signals
widget.unblock_spinbox_signals()
@property
def value(self):
return self._current_value
[docs] def set_value(self, value, unit=None, program=True):
# conversion to float means a string can be passed in too:
value = float(value)
if unit is not None and unit != self._base_unit:
self._current_value = self.convert_value_to_base(value,unit)
else:
self._current_value = value
# Update the saved value in the settings dictionary
self._settings['base_value'] = self._current_value
if program:
self._logger.debug('program device called')
self._program_device()
for widget in self._widgets:
# block signals
widget.block_spinbox_signals()
# update widget
widget.set_spinbox_value(value,unit if unit is not None else self._base_unit)
# unblock signals
widget.unblock_spinbox_signals()
[docs] def set_step_size(self,step_size,unit):
self._logger.debug('set_step_size called. step_size: %f, unit: %s'%(step_size,unit))
if unit != self._base_unit:
# convert and store!
value = self.convert_value_from_base(self._current_value,unit)
self._step_size = self.convert_range_to_base(value,step_size,unit)
else:
# This check is usually performed when converting the range to base units
# But since we are already in base units we should do it here
if abs(self._limits[0]-self._limits[1]) <= step_size:
step_size = abs(self._limits[0]-self._limits[1])
self._step_size = step_size
self._logger.debug('step_size in base units: %f'%self._step_size)
#self._current_step_size = self._step_size
self._settings['base_step_size'] = self._step_size
# now convert to current units
self._current_step_size = self.get_step_size(self._current_units)
self._logger.debug('step_size in current units (%s): %f'%(self._current_units,self._current_step_size))
# Update the step size for all widgets
for widget in self._widgets:
widget.set_step_size(self._current_step_size)
[docs] def get_step_size(self,unit):
if unit != self._base_unit:
# we should convert it
return self.convert_range_from_base(self._current_value,self._step_size,unit)
else:
return self._step_size
[docs] def lock(self):
self._update_lock(True)
[docs] def unlock(self):
self._update_lock(False)
def _update_lock(self, locked):
self._locked = locked
self._settings['locked'] = locked
# Lock all widgets if they are not already locked
for widget in self._widgets:
if locked:
widget.lock(False)
else:
widget.unlock(False)
@property
def name(self):
return self._hardware_name + ' - ' + self._connection_name
[docs]class DO(object):
[docs] def __init__(self, hardware_name, connection_name, device_name, program_function, settings):
self._hardware_name = hardware_name
self._connection_name = connection_name
self._widget_list = []
self._device_name = device_name
self._logger = logging.getLogger('BLACS.%s.%s'%(self._device_name,hardware_name))
# Note that while we could store self._current_state and self._locked in the
# settings dictionary, this dictionary is available to other parts of BLACS
# and using separate variables avoids those parts from being able to directly
# influence behaviour (the worst they can do is change the value used on initialisation)
self._locked = False
self._current_state = False
self._program_device = program_function
self._update_from_settings(settings)
def _update_from_settings(self,settings):
# Build up the settings dictionary if it isn't already
if not isinstance(settings,dict):
settings = {}
if 'front_panel_settings' not in settings or not isinstance(settings['front_panel_settings'],dict):
settings['front_panel_settings'] = {}
if self._hardware_name not in settings['front_panel_settings'] or not isinstance(settings['front_panel_settings'][self._hardware_name],dict):
settings['front_panel_settings'][self._hardware_name] = {}
# Set default values if they are not already saved in the settings dictionary
if 'base_value' not in settings['front_panel_settings'][self._hardware_name]:
settings['front_panel_settings'][self._hardware_name]['base_value'] = False
if 'locked' not in settings['front_panel_settings'][self._hardware_name]:
settings['front_panel_settings'][self._hardware_name]['locked'] = False
if 'name' not in settings['front_panel_settings'][self._hardware_name]:
settings['front_panel_settings'][self._hardware_name]['name'] = self._connection_name
# only keep a reference to the part of the settings dictionary relevant to this DO
self._settings = settings['front_panel_settings'][self._hardware_name]
# Update the state of the button
self.set_value(self._settings['base_value'],program=False)
# Update the lock state
self._update_lock(self._settings['locked'])
@property
def value(self):
return bool(self._current_state)
[docs] def lock(self):
self._update_lock(True)
[docs] def unlock(self):
self._update_lock(False)
def _update_lock(self,locked):
self._locked = locked
for widget in self._widget_list:
if locked:
widget.lock(False)
else:
widget.unlock(False)
# update the settings dictionary if it exists, to maintain continuity on tab restarts
self._settings['locked'] = locked
[docs] def set_value(self,state,program=True):
# conversion to integer, then bool means we can safely pass in
# either a string '1' or '0', True or False or 1 or 0
state = bool(int(state))
# We are programatically setting the state, so break the check lock function logic
self._current_state = state
# update the settings dictionary if it exists, to maintain continuity on tab restarts
self._settings['base_value'] = state
if program:
self._logger.debug('program device called')
self._program_device()
for widget in self._widget_list:
if state != widget.state:
widget.blockSignals(True)
widget.state = state
widget.blockSignals(False)
@property
def name(self):
return self._hardware_name + ' - ' + self._connection_name
[docs]class Image(object):
[docs] def __init__(self, hardware_name, connection_name, device_name, program_function, settings, width, height, x = None, y = None):
self._hardware_name = hardware_name
self._connection_name = connection_name
self._widget_list = []
if x is not None and y is not None:
self.location = 'Location: (%d, %d) '%(x,y)
else:
self.location = ""
self.width = width
self.height = height
self._device_name = device_name
self._logger = logging.getLogger('BLACS.%s.%s'%(self._device_name,hardware_name))
# Note that while we could store self._current_state and self._locked in the
# settings dictionary, this dictionary is available to other parts of BLACS
# and using separate variables avoids those parts from being able to directly
# influence behaviour (the worst they can do is change the value used on initialisation)
self._locked = False
self._current_value = ""
self._program_device = program_function
self._update_from_settings(settings)
def _update_from_settings(self, settings):
# Build up the settings dictionary if it isn't already
if not isinstance(settings,dict):
settings = {}
if 'front_panel_settings' not in settings or not isinstance(settings['front_panel_settings'],dict):
settings['front_panel_settings'] = {}
if self._hardware_name not in settings['front_panel_settings'] or not isinstance(settings['front_panel_settings'][self._hardware_name],dict):
settings['front_panel_settings'][self._hardware_name] = {}
# Set default values if they are not already saved in the settings dictionary
if 'base_value' not in settings['front_panel_settings'][self._hardware_name]:
settings['front_panel_settings'][self._hardware_name]['base_value'] = ""
if 'locked' not in settings['front_panel_settings'][self._hardware_name]:
settings['front_panel_settings'][self._hardware_name]['locked'] = False
if 'name' not in settings['front_panel_settings'][self._hardware_name]:
settings['front_panel_settings'][self._hardware_name]['name'] = self._connection_name
# only keep a reference to the part of the settings dictionary relevant to this DO
self._settings = settings['front_panel_settings'][self._hardware_name]
# Update the state of the button
self.set_value(self._settings['base_value'],program=False)
# Update the lock state
self._update_lock(self._settings['locked'])
@property
def value(self):
return str(self._current_value)
[docs] def lock(self):
self._update_lock(True)
[docs] def unlock(self):
self._update_lock(False)
def _update_lock(self, locked):
self._locked = locked
for widget in self._widget_list:
if locked:
widget.lock(False)
else:
widget.unlock(False)
# update the settings dictionary if it exists, to maintain continuity on tab restarts
self._settings['locked'] = locked
[docs] def set_value(self, value, program = True):
value = str(value)
# We are programatically setting the value, so break the check lock function logic
self._current_value = value
# update the settings dictionary if it exists, to maintain continuity on tab restarts
self._settings['base_value'] = value
if program:
self._logger.debug('program device called')
self._program_device()
for widget in self._widget_list:
if value != widget.value:
widget.blockSignals(True)
widget.value = value
widget.blockSignals(False)
@property
def name(self):
return self._hardware_name + ' - ' + self._connection_name
[docs]class DDS(object):
[docs] def __init__(self, hardware_name, connection_name, output_list):
self._hardware_name = hardware_name
self._connection_name = connection_name
self._sub_channel_list = ['freq','amp','phase','gate']
self._widget_list = []
for subchnl in self._sub_channel_list:
value = None
if subchnl in output_list:
value = output_list[subchnl]
setattr(self,subchnl,value)
[docs] def get_subchnl_list(self):
subchnls = []
for subchnl in self._sub_channel_list:
if hasattr(self,subchnl):
subchnls.append(subchnl)
return subchnls
[docs] def get_unused_subchnl_list(self):
return list(set(self._sub_channel_list).difference(set(self.get_subchnl_list())))
@property
def value(self):
value = {}
for subchnl in self._sub_channel_list:
if hasattr(self,subchnl):
value[subchnl] = getattr(self,subchnl).value
return value
[docs] def set_value(self,value,program=True):
for subchnl in self._sub_channel_list:
if subchnl in value:
if hasattr(self,subchnl):
getattr(self,subchnl).set_value(value[subchnl],program=program)
@property
def name(self):
return self._hardware_name + ' - ' + self._connection_name
if __name__ == '__main__':
from labscript_utils.qtwidgets.toolpalette import ToolPaletteGroup
import sys
qapplication = QApplication(sys.argv)
window = QWidget()
layout = QVBoxLayout(window)
widget = QWidget()
layout.addWidget(widget)
tpg = ToolPaletteGroup(widget)
toolpalette = tpg.append_new_palette('Digital Outputs')
toolpalette2 = tpg.append_new_palette('Analog Outputs')
layout.addItem(QSpacerItem(0,0,QSizePolicy.Minimum,QSizePolicy.MinimumExpanding))
# create settings dictionary
settings = {'front_panel_settings':{
'do0':{
'base_value':False,
'locked':False,
},
'ao0':{
'base_value':3.0,
'locked':False,
'base_step_size':0.1,
'current_units':'V',
}
}
}
def print_something():
print('program_function called')
# Create a DO object
my_DO = DO(hardware_name='do0', connection_name='my first digital output', program_function=print_something, settings=settings)
# Link in two DO widgets
button1 = DigitalOutput('do0\nmy first digital output')
button2 = DigitalOutput('a linked do0')
toolpalette.addWidget(button1)
toolpalette.addWidget(button2)
my_DO.add_widget(button1)
my_DO.add_widget(button2)
# Create an AO object
my_AO = AO(hardware_name = 'ao0', connection_name='my ao', device_name='ni_blah',
program_function=print_something, settings=settings,
calib_class=None, calib_params=None, default_units='V',
min=-10.0, max=10.0, step=0.01, decimals=3)
# link in two AO widgets
analog1 = AnalogOutput('AO1')
analog2 = AnalogOutput('AO1 copy')
my_AO.add_widget(analog1)
my_AO.add_widget(analog2)
toolpalette2.addWidget(analog1)
toolpalette2.addWidget(analog2)
# TODO: Add in test case for DDS
window.show()
sys.exit(qapplication.exec_())