import traceback
from functools import partial
import json
import pyperclip
import logging
from PyQt5 import QtGui
from PyQt5.QtGui import QPalette, QFont
from PyQt5.QtWidgets import *
from PyQt5.QtCore import Qt, pyqtSignal, QEvent
from silq.tools.general_tools import get_exponent, get_first_digit
from . import start_gui
logger = logging.getLogger(__name__)
states = ['up_down', 'left_right', 'none']
[docs]def sim_gui(mini=False):
# Must set sim_gui.voltage_parameters
start_gui(SIMControlDialog, 'sim_gui',
parameters=sim_gui.voltage_parameters,
mini=mini)
[docs]class SIM928Dialog(QFrame):
state_change = pyqtSignal()
def __init__(self, parameter, idx=1, mini=False):
super().__init__()
self.parameter = parameter
self.idx = idx
self.mini = mini
self._state = 'none'
self.min_step = 0.0001
self.max_step = 0.05
self.modified_val = False
self.setMaximumWidth(170)
self.initUI()
self.name_label.mousePressEvent = self.cycle_state
@property
def state(self):
return self._state
@state.setter
def state(self, state):
if state not in ['none', 'up_down', 'left_right']:
logger.debug(f'State {state} not recognized')
else:
# Create palette for name color customization
name_palette = QPalette()
if state == 'none':
name_palette.setColor(QPalette.Foreground, Qt.black)
elif state == 'up_down':
name_palette.setColor(QPalette.Foreground, Qt.blue)
elif state == 'left_right':
name_palette.setColor(QPalette.Foreground, Qt.darkGreen)
self._state = state
self.name_label.setPalette(name_palette)
# Emit signal that state has changed
self.state_change.emit()
[docs] def cycle_state(self, *args, **kwargs):
logger.debug(f'Cycling gate {self.parameter.name}')
if self.state == 'none':
self.state = 'up_down'
elif self.state == 'up_down':
self.state = 'left_right'
else:
self.state = 'none'
[docs] def initUI(self):
layout = QVBoxLayout()
if self.mini:
layout.setContentsMargins(0, 0, 0, 0)
else:
layout.setContentsMargins(5, 0, 5, 0)
self.setLayout(layout)
# Set parameter name
self.name_label = QLabel(f'{self.idx}: {self.parameter.name}')
self.name_label.setAlignment(Qt.AlignCenter)
if not self.mini:
self.name_label.setFont(QFont("Times", 16, QFont.Bold))
layout.addWidget(self.name_label)
# Add editable voltage
val_hbox = QHBoxLayout()
layout.addLayout(val_hbox)
if not self.mini:
val_descr_label = QLabel('Val:')
val_descr_label.setFont(QFont("Times", 12, QFont.Bold))
val_hbox.addWidget(val_descr_label)
else:
self.name_label.setFont(QFont("Times", 12, QFont.Bold))
val_hbox.addWidget(self.name_label)
self.val_textbox = QLineEdit(f'{self.parameter.get_latest():.5g}')
self.val_textbox.setAlignment(Qt.AlignCenter)
self.val_textbox.setFont(QFont("Times", 12, QFont.Bold))
self.val_textbox.returnPressed.connect(
lambda: self.set_voltage(self.val_textbox.text()))
val_hbox.addWidget(self.val_textbox)
val_units_label = QLabel('V')
val_units_label.setFont(QFont("Times", 12, QFont.Bold))
val_hbox.addWidget(val_units_label)
# Add current val
self.current_val_label = QLabel(f'')
self.current_val_label.setAlignment(Qt.AlignCenter)
self.current_val_label.setFont(QFont("Times", 12))
layout.addWidget(self.current_val_label)
self.val_textbox.textChanged.connect(self._val_textbox_changed)
# Add voltage buttons
if not self.mini:
self.val_grid = QGridLayout()
self.val_grid.setHorizontalSpacing(0)
self.val_grid.setVerticalSpacing(0)
layout.addLayout(self.val_grid)
self.val_buttons = {}
for column_idx, scale in enumerate([100, 10, 1]):
for row_idx, sign in enumerate([1, -1]):
val = sign * scale
button = QPushButton(f"{['-','+'][sign==1]}{scale} mV")
self.val_grid.addWidget(button, row_idx, column_idx + 1)
width = button.fontMetrics().boundingRect(
f"+100 mV").width() + 7
button.setMaximumWidth(width)
button.clicked.connect(
partial(self.increase_voltage, val / 1000))
self.val_buttons[val] = button
# Add scale
self.scale_label = QLabel(f'Scale: {self.parameter.scale}')
self.scale_label.setAlignment(Qt.AlignCenter)
self.scale_label.setFont(QFont("Times", 12))
layout.addWidget(self.scale_label)
# Add SIM voltage
self.raw_val_label = QLabel('SIM val: {:.3g} V'.format(
self.parameter.raw_value))
self.raw_val_label.setAlignment(Qt.AlignCenter)
self.raw_val_label.setFont(QFont("Times", 12))
layout.addWidget(self.raw_val_label)
def _val_textbox_changed(self):
try:
textbox_val = float(self.val_textbox.text())
self.modified_val = textbox_val != self.parameter.get_latest()
except:
self.modified_val = True
if self.modified_val:
self.val_textbox.setStyleSheet("color: rgb(255, 0, 0);")
if self.current_val_label.text() == '':
self.current_val_label.setText(
f'Current val: {self.parameter.get_latest()}')
else:
pass
# self._reset_val_textbox()
[docs] def set_voltage(self, val):
try:
val = round(float(val), 6)
self.parameter(val)
except:
pass
self._reset_val_textbox()
def _reset_val_textbox(self):
self.modified_val = False
self.val_textbox.setText('{:.5g}'.format(self.parameter.get_latest()))
self.current_val_label.setText('')
self.val_textbox.setStyleSheet("color: rgb(0, 0, 0);")
if not self.mini:
self.raw_val_label.setText('SIM val: {:.3g} V'.format(
self.parameter.get_latest() * self.parameter.scale))
[docs] def increase_voltage(self, increment):
self.set_voltage(self.parameter.get_latest() + increment)
[docs]class SIMConfigDialog(QFrame):
def __init__(self, mini=False):
super().__init__()
self.mini = mini
self.step = {'up_down': 0.001, 'left_right': 0.001}
self.min_step = 0.0001
self.max_step = 0.05
self.initUI()
[docs] def initUI(self):
self.layout = QVBoxLayout()
self.setLayout(self.layout)
# Add up, down, left, right color indication
if not self.mini:
key_grid = QGridLayout()
key_grid.setVerticalSpacing(0)
for k, (color, keys) in enumerate([
('blue', [('up', (0, 1)), ('down', (1, 1))]),
('darkGreen', [('left', (1, 0)), ('right', (1, 2))])]):
for (key, pos) in keys:
key_label = QLabel(key)
key_label.setAlignment(Qt.AlignCenter)
key_label.setFont(QFont("Times", 12, QFont.Bold))
key_palette = QPalette()
key_palette.setColor(QPalette.Foreground, getattr(Qt, color))
key_label.setPalette(key_palette)
key_grid.addWidget(key_label, *pos)
self.layout.addLayout(key_grid)
self.layout.addStretch(1)
# Add step sizes
step_label = QLabel('Step:')
step_label.setAlignment(Qt.AlignCenter)
step_label.setFont(QFont("Times", 12, QFont.Bold))
self.layout.addWidget(step_label)
step_hbox = QHBoxLayout()
self.layout.addLayout(step_hbox)
self.step_textbox = {}
for k, (state, color) in enumerate([('up_down', 'blue'),
('left_right', 'darkGreen')]):
step_hbox.addStretch(0.8)
self.step_textbox[state] = QLineEdit(str(self.step[state]))
self.step_textbox[state].setMaximumWidth(55)
step_hbox.addWidget(self.step_textbox[state])
self.step_textbox[state].returnPressed.connect(
lambda: self.set_step(state, self.step_textbox.text()))
step_unit_label = QLabel('V')
if k == 0:
step_unit_label.setContentsMargins(0, 0, 20, 0)
step_hbox.addWidget(step_unit_label)
for widget in [step_unit_label, self.step_textbox[state]]:
widget.setFont(QFont("Times", 10, QFont.Bold))
widget.setStyleSheet(f"color: {color};")
self.layout.addStretch(1)
# Add ramp buttons
ramp_hbox = QHBoxLayout()
self.ramp_button = QPushButton('Ramp')
self.ramp_zero_button = QPushButton('Ramp to zero')
ramp_hbox.addWidget(self.ramp_button)
ramp_hbox.addWidget(self.ramp_zero_button)
self.layout.addLayout(ramp_hbox)
self.layout.addStretch(1)
[docs] def set_step(self, state, val):
try:
val = round(float(val), 6)
if val > self.max_step:
val = self.max_step
elif val < self.min_step:
val = self.min_step
self.step[state] = val
self.step_textbox[state].setText(str(val))
self._clear_focus()
except:
traceback.print_exc()
[docs] def increase_step(self, state):
current_val = self.step[state]
exponent = get_exponent(current_val)
first_digit = get_first_digit(current_val)
if first_digit == 1:
self.set_step(state, 2 * 10 ** exponent)
elif first_digit < 4:
self.set_step(state, 5 * 10 ** exponent)
else:
self.set_step(state, 1 * 10 ** (exponent + 1))
[docs] def decrease_step(self, state):
current_val = self.step[state]
exponent = get_exponent(current_val)
first_digit = get_first_digit(current_val)
if first_digit == 1:
self.set_step(state, 5 * 10 ** (exponent - 1))
elif first_digit < 4:
self.set_step(state, 1 * 10 ** exponent)
else:
self.set_step(state, 2 * 10 ** exponent)
def _clear_focus(self):
focus_widget = QApplication.focusWidget()
if focus_widget is not None:
focus_widget.clearFocus()
[docs]class Separator(QFrame):
def __init__(self, mode='vertical'):
super().__init__()
if mode is 'vertical':
frame_shape = QFrame.VLine
else:
frame_shape = QFrame.HLine
self.setFrameShape(frame_shape)
self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding)
self.setLineWidth(2)
[docs]class SIMControlDialog(QDialog):
def __init__(self, parameters, mini=False):
if len(parameters) > 10:
raise SyntaxError('Can use at most 10 gates')
super().__init__(
flags=Qt.WindowMinimizeButtonHint|
Qt.WindowCloseButtonHint)
self.parameters = parameters
self.mini = mini
self.index_keys = {}
self.state_parameters = {state: [] for state in states}
# Dict of {parameter.name: SIM928Dialog}
self.SIM928_dialogs = {}
self.initUI()
self.move(1400, 350)
[docs] def initUI(self):
if not self.mini:
self.layout = QHBoxLayout()
else:
self.layout = QVBoxLayout()
self.layout.setSpacing(0)
self.setLayout(self.layout)
self.config_widget = SIMConfigDialog(mini=self.mini)
self.config_widget.ramp_button.clicked.connect(
lambda clicked: self.ramp_voltages())
self.config_widget.ramp_button.clicked.connect(self._clear_focus)
self.config_widget.ramp_zero_button.clicked.connect(
lambda clicked: self.ramp_voltages(0))
self.config_widget.ramp_zero_button.clicked.connect(self._clear_focus)
self.config_widget.copy_button = QPushButton('Copy from clipboard')
self.config_widget.copy_button.clicked.connect(
self._copy_from_clipboard)
self.config_widget.copy_button.clicked.connect(self._clear_focus)
self.config_widget.layout.addWidget(self.config_widget.copy_button)
self.layout.addWidget(self.config_widget)
for k, parameter in enumerate(self.parameters):
idx = (k+1) % 10
self.layout.addWidget(Separator())
SIM928_dialog = SIM928Dialog(parameter, idx=idx, mini=self.mini)
self.layout.addWidget(SIM928_dialog)
Qt_index_key = getattr(Qt, f'Key_{idx}')
self.index_keys[Qt_index_key] = SIM928_dialog
self.SIM928_dialogs[parameter.name] = SIM928_dialog
SIM928_dialog.state_change.connect(self.update_parameters)
self.config_widget.ramp_button.clicked.connect(
SIM928_dialog._reset_val_textbox)
self.config_widget.ramp_zero_button.clicked.connect(
SIM928_dialog._reset_val_textbox)
[docs] def keyPressEvent(self, event):
try:
if event.key() in self.index_keys:
# Change state of SIM928 dialog
self.index_keys[event.key()].cycle_state()
elif event.key() == Qt.Key_Escape:
# Clear focus
self._clear_focus()
elif event.key() == Qt.Key_Up:
# Increase voltage for blue (up_down) dialogs
self.increase_voltages(self.state_parameters['up_down'],
self.config_widget.step['up_down'])
elif event.key() == Qt.Key_Down:
# Decrease voltage for blue (up_down) dialogs
self.increase_voltages(self.state_parameters['up_down'],
-self.config_widget.step['up_down'])
elif event.key() == Qt.Key_Right:
# Increase voltage for green (left_right) dialogs
self.increase_voltages(self.state_parameters['left_right'],
self.config_widget.step['left_right'])
elif event.key() == Qt.Key_Left:
# Decrease voltage for green (left_right) dialogs
self.increase_voltages(self.state_parameters['left_right'],
-self.config_widget.step['left_right'])
elif event.key() == Qt.Key_W:
self.config_widget.decrease_step('up_down')
elif event.key() == Qt.Key_S:
self.config_widget.increase_step('up_down')
elif event.key() == Qt.Key_A:
self.config_widget.increase_step('left_right')
elif event.key() == Qt.Key_D:
self.config_widget.decrease_step('left_right')
except:
traceback.print_exc()
[docs] def increase_voltages(self, parameters, val):
for parameter in parameters:
self.SIM928_dialogs[parameter.name].increase_voltage(val)
[docs] def update_parameters(self):
logger.debug('updating parameters')
# Clear parameters from self.state_parameters
self.state_parameters = {state: [] for state in states}
# Iterate through SIM928 dialogs and add them to state_parameters
for dialog in self.SIM928_dialogs.values():
self.state_parameters[dialog.state].append(dialog.parameter)
def _copy_from_clipboard(self, *args):
clipboard_dict = json.loads(pyperclip.paste())
for parameter_name, val in clipboard_dict.items():
self.SIM928_dialogs[parameter_name].val_textbox.setText(str(val))
def _clear_focus(self):
logger.debug('clearing focus')
focus_widget = QApplication.focusWidget()
if focus_widget is not None:
focus_widget.clearFocus()
for dialog in self.SIM928_dialogs.values():
dialog._reset_val_textbox()
[docs] def ramp_voltages(self, voltage=None, parameters=None):
if voltage is not None:
if parameters is None:
parameters = self.parameters
for parameter in parameters:
parameter(voltage)
else:
for SIM928_dialog in self.SIM928_dialogs.values():
if SIM928_dialog.modified_val:
SIM928_dialog.set_voltage(SIM928_dialog.val_textbox.text())
[docs] def changeEvent(self, event):
# Correctly determines it is activated, but cannot clear focus
super().changeEvent(event)
if event.type() == QEvent.ActivationChange:
if self.windowState() == Qt.WindowNoState:
self._clear_focus()
for SIM928_dialog in self.SIM928_dialogs.values():
SIM928_dialog._reset_val_textbox()