Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/Voltage Control (Draft) #232

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,942 changes: 1,267 additions & 675 deletions poetry.lock

Large diffs are not rendered by default.

38 changes: 25 additions & 13 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,10 @@ name = "qualang-tools"
version = "v0.17.7"
description = "The qualang_tools package includes various tools related to QUA programs in Python"
license = "BSD-3-Clause"
authors = [
"QM <[email protected]>",
]
packages = [
{ include = "qualang_tools" }
]
include = [
]
exclude = [
"**/tests/**",
]
authors = ["QM <[email protected]>"]
packages = [{ include = "qualang_tools" }]
include = []
exclude = ["**/tests/**"]
readme = "README.md"
homepage = "https://github.com/qua-platform/py-qua-tools"

Expand All @@ -31,14 +24,18 @@ dash-bootstrap-components = { version = "^1.0.0", optional = true }
dash-cytoscape = { version = "^0.3.0", optional = true }
dash-table = { version = "^5.0.0", optional = true }
dash-dangerously-set-inner-html = { version = "^0.0.2", optional = true }
docutils = { version = ">=0.14.0", optional = true }
docutils = { version = ">=0.14.0,<0.21", optional = true }
waitress = { version = "^2.0.0", optional = true }
dill = { version = "^0.3.4", optional = true }
pypiwin32 = { version = "^223", optional = true }
ipython = { version = "^7.31.1", optional = true }
xarray = { version = "^2023.0.0", optional = true }
scikit-learn = "^1.0.2"
grpclib = "0.4.5"
pyperclip = { version = "^1.9.0", optional = true }
pyvisa = { version = "^1.14.1", optional = true }
pyqt5 = { version = "^5.15.11", optional = true }
pyvisa-py = { version = "^0.7.2", optional = true }

[tool.poetry.dev-dependencies]
pytest = "^6.2.5"
Expand All @@ -49,8 +46,23 @@ setuptools = "^69.0.2"

[tool.poetry.extras]
interplot = ["dill", "pypiwin32", "ipython"]
configbuilder = ["pandas", "dash", "dash-html-components", "dash-core-components", "dash-bootstrap-components", "dash-cytoscape", "dash-table", "dash-dangerously-set-inner-html", "docutils", "waitress"]
configbuilder = [
"pandas",
"dash",
"dash-html-components",
"dash-core-components",
"dash-bootstrap-components",
"dash-cytoscape",
"dash-table",
"dash-dangerously-set-inner-html",
"docutils",
"waitress",
]
datahandler = ["xarray", "netcdf4"]
voltagecontrol = ["pyvisa", "pyperclip", "pyqt5", "pyvisa-py"]

[tool.poetry.group.dev.dependencies]
ipykernel = "^6.29.5"

[tool.black]
line-length = 120
Expand Down
Empty file.
52 changes: 52 additions & 0 deletions qualang_tools/control_panel/voltage_control/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import sys
import threading
from typing import Any
import pyvisa as visa
from PyQt5.QtWidgets import QApplication

from qualang_tools.control_panel.voltage_control.voltage_control_dialog import VoltageControlDialog


def start_voltage_control(use_thread: bool = False, gui_name: str = "Voltage control", *args: Any, **kwargs: Any):
"""
Start the voltage control GUI.

Args:
use_thread: Whether to run the GUI in a separate thread
gui_name: Name of the GUI thread
*args: Positional arguments to pass to VoltageControlDialog
**kwargs: Keyword arguments to pass to VoltageControlDialog

Returns:
QApplication instance or threading.Thread instance
"""
if use_thread:
if any(t.name == gui_name for t in threading.enumerate()):
raise RuntimeError(f"GUI {gui_name} already exists. Exiting")
t = threading.Thread(
target=start_voltage_control,
name=gui_name,
args=args,
kwargs={"use_thread": False, "gui_name": gui_name, **kwargs},
)
t.start()
return t
else:
qApp = QApplication(sys.argv)

aw = VoltageControlDialog(*args, **kwargs)
aw.show()
qApp.exec_()
return qApp


if __name__ == "__main__":
import numpy as np
from qualang_tools.control_panel.voltage_control.voltage_parameters import QDACII, qdac_ch

qdac = QDACII("ethernet", IP_address="192.168.8.17", port=5025)
# Create dummy parameters
parameters = [qdac_ch(qdac, idx, f"V{idx}", initial_value=0) for idx in range(1, 5)]
for parameter in parameters:
parameter.get()
start_voltage_control(parameters=parameters, mini=True, use_thread=False)
30 changes: 30 additions & 0 deletions qualang_tools/control_panel/voltage_control/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import numpy as np


def get_exponent(val: float) -> int:
"""Get decimal exponent of a value.

Args:
val: Value of which to get exponent

Returns:
Exponent

Raises:
ValueError: If val is less than or equal to zero
"""
if val <= 0:
raise ValueError(f"Value {val} must be larger than zero")
return int(np.floor(np.log10(val)))


def get_first_digit(val: float) -> int:
"""Get first nonzero digit of a value.

Args:
val: Value for which to get first nonzero digit

Returns:
First nonzero digit
"""
return int(np.floor(val * 10 ** -get_exponent(val)))
151 changes: 151 additions & 0 deletions qualang_tools/control_panel/voltage_control/voltage_control_dialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import logging
import json
from PyQt5.QtGui import QFont
import pyperclip
import traceback
from typing import List, Dict, Any

from PyQt5.QtWidgets import QDialog, QHBoxLayout, QVBoxLayout, QPushButton, QApplication
from PyQt5.QtCore import Qt, QEvent

from .widgets import VoltageSourceDialog, VoltageConfigDialog, Separator

logger = logging.getLogger(__name__)


STATES = ["up_down", "left_right", "none"]


class VoltageControlDialog(QDialog):
def __init__(self, parameters: List[Any], mini: bool = False):
# if len(parameters) > 10:
# raise ValueError("Can use at most 10 voltage sources")
super().__init__(flags=Qt.WindowMinimizeButtonHint | Qt.WindowCloseButtonHint)
self.parameters = parameters
self.mini = mini
self.index_keys = {}
self.state_parameters = {state: [] for state in ["up_down", "left_right", "none"]}
self.voltage_source_dialogs: Dict[str, VoltageSourceDialog] = {}
self.initUI()
rect = QApplication.desktop().screenGeometry()
self.move(rect.height() * 7 // 10, rect.width() * 3 // 10)

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 = VoltageConfigDialog(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
self.layout.addWidget(Separator())
voltage_source_dialog = VoltageSourceDialog(parameter, idx=idx, mini=self.mini)

# Wrap the VoltageSourceDialog in a QHBoxLayout to make it expand properly
dialog_layout = QHBoxLayout()
dialog_layout.addWidget(voltage_source_dialog)
dialog_layout.setContentsMargins(0, 0, 0, 0)

self.layout.addLayout(dialog_layout)

if idx < 10:
Qt_index_key = getattr(Qt, f"Key_{idx}")
self.index_keys[Qt_index_key] = voltage_source_dialog
self.voltage_source_dialogs[parameter.name] = voltage_source_dialog

voltage_source_dialog.state_change.connect(self.update_parameters)
self.config_widget.ramp_button.clicked.connect(voltage_source_dialog._reset_val_textbox)
self.config_widget.ramp_zero_button.clicked.connect(voltage_source_dialog._reset_val_textbox)

def keyPressEvent(self, event):
try:
if event.key() in self.index_keys:
# Change state of VoltageSource 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()

def increase_voltages(self, parameters, val):
for parameter in parameters:
self.voltage_source_dialogs[parameter.name].increase_voltage(val)

def update_parameters(self):
logger.debug("updating parameters")
# Clear parameters from self.state_parameters
self.state_parameters = {state: [] for state in STATES}

# Iterate through VoltageSource dialogs and add them to state_parameters
for dialog in self.voltage_source_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.voltage_source_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.voltage_source_dialogs.values():
dialog._reset_val_textbox()

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 voltage_source_dialog in self.voltage_source_dialogs.values():
if voltage_source_dialog.modified_val:
voltage_source_dialog.set_voltage(voltage_source_dialog.val_textbox.text())

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 voltage_source_dialog in self.voltage_source_dialogs.values():
voltage_source_dialog._reset_val_textbox()
Loading