# Copyright (c) 2023 JARA Institute for Quantum Information
#
# This file is part of QuMADA.
#
# QuMADA is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# QuMADA is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# QuMADA. If not, see <https://www.gnu.org/licenses/>.
#
# Contributors:
# - Daniel Grothe
# - Jonas Mertens
from __future__ import annotations
import json
from collections.abc import Iterable, Mapping
from typing import Any
from PyQt5.QtCore import QItemSelectionModel, Qt, QTimer, pyqtSignal, pyqtSlot
from PyQt5.QtGui import (
QBrush,
QColor,
QDropEvent,
QFocusEvent,
QKeyEvent,
QMouseEvent,
QStandardItem,
QStandardItemModel,
)
from PyQt5.QtWidgets import (
QAction,
QApplication,
QDesktopWidget,
QHBoxLayout,
QInputDialog,
QLabel,
QMainWindow,
QMessageBox,
QPushButton,
QScrollArea,
QSizePolicy,
QSplitter,
QTreeView,
QVBoxLayout,
QWidget,
)
from qcodes.instrument.channel import InstrumentModule
from qcodes.instrument.instrument import Instrument
from qcodes.instrument.parameter import Parameter
from qcodes.utils.metadata import Metadatable
from qumada.instrument.mapping.base import TerminalParameters, filter_flatten_parameters
from qumada.metadata import Metadata
RED = QColor(255, 0, 0)
WHITE = QColor(255, 255, 255)
GREEN = QColor(0, 255, 0)
YELLOW = QColor(255, 255, 0)
PINK = QColor(255, 192, 203)
BLUE = QColor(0, 0, 255)
# TODO: terminal_parameter attributes
[docs]class TerminalTreeView(QTreeView):
"""
QTreeView, that displays QuMADA `TerminalParameters` datastructure
(`Mapping[Any, Mapping[Any, Parameter] | Parameter]`).
Items are draggable to map them to instruments.
"""
def __init__(self, monitoring=False):
super().__init__()
model = QStandardItemModel()
model.setHorizontalHeaderLabels(["Terminal", "Mapped Instrument", "Monitoring"])
self.setModel(model)
self.terminal_parameters = None
self.selected_terminal_tree_elem = None
# Parameter Monitoring Functionality
self.monitoring_enable = monitoring
self.monitoring_timer = QTimer()
self.monitoring_timer.timeout.connect(self.update_monitoring)
self.setDragEnabled(True)
# forward keypressevents "hack". keyPressEvent of QMainWindow doesnt fire for letter keys
[docs] def keyPressEvent(self, event: QKeyEvent) -> None:
if event.key() == Qt.Key_A or event.key() == Qt.Key_R or event.key() == Qt.Key_E or event.key() == Qt.Key_U:
self.parentWidget().keyPressEvent(event)
else:
return super().keyPressEvent(event)
# overwrite default double click event behaviour
[docs] def mouseDoubleClickEvent(self, event: QMouseEvent) -> None:
pass
[docs] def update_monitoring(self):
"""
This is called periodically, gets the mapped parameter values and updates the labels
"""
# TODO: maybe format cells based on settable
# if(param.settable):
# pass # maybe do coloring based on settable
root = self.model().invisibleRootItem()
for terminal in get_children(root):
for terminal_param in get_children(terminal):
param = self.terminal_parameters[terminal_param.source[0]][terminal_param.source[1]]
if param is not None:
if self.monitoring_get_type == "get" and param.gettable:
# Use get command
try:
val = param.get()
except Exception:
val = param.cache.get(get_if_invalid=False)
else:
# Use cached value (also applicable to non-gettable parameters (last set value))
val = param.cache.get(get_if_invalid=False)
if val is not None and (type(val) is int or type(val) is float):
if param.unit is not None:
self.model().setData(terminal_param.index().siblingAtColumn(2), f"{val:.2f} {param.unit}")
else:
self.model().setData(terminal_param.index().siblingAtColumn(2), f"{val:.2f}")
else:
self.model().setData(terminal_param.index().siblingAtColumn(2), "")
self.resizeColumnToContents(2)
[docs] def update_tree(self):
"""
Function that sets all visual elements (labels, colors) based on state of mapping.
This is a clean solution (not super efficient) because changing the mapping for one
parameter can have an influence on any other visual elements (duplicate markings etc.)
"""
# Set mapped instrument labels and colors, set colors for duplicates
tree = self.model()
parameter_duplicates = {}
for terminal in get_children(tree.invisibleRootItem()):
tree.setData(terminal.index(), QBrush(WHITE), Qt.BackgroundRole)
tree.setData(terminal.index().siblingAtColumn(1), QBrush(RED), Qt.BackgroundRole)
tree.setData(terminal.index().siblingAtColumn(1), "")
all_mapped = True
any_mapped = False
channel_names = set()
for terminal_param in get_children(terminal):
tree.setData(terminal_param.index(), QBrush(WHITE), Qt.BackgroundRole)
tree.setData(terminal_param.index().siblingAtColumn(1), QBrush(RED), Qt.BackgroundRole)
tree.setData(terminal_param.index().siblingAtColumn(1), "")
param = self.terminal_parameters[terminal_param.source[0]][terminal_param.source[1]]
if param is not None:
if isinstance(param, Parameter):
param_hash = hash(param)
if param_hash in parameter_duplicates:
parameter_duplicates[param_hash].append(terminal_param)
else:
parameter_duplicates[param_hash] = [terminal_param]
channel_names.add(param.instrument.full_name)
tree.setData(terminal_param.index().siblingAtColumn(1), param.full_name)
tree.setData(terminal_param.index().siblingAtColumn(1), QBrush(GREEN), Qt.BackgroundRole)
any_mapped = True
else:
raise TypeError("Gate parameters have to be either None or of type Parameter.")
else:
all_mapped = False
# color and label terminals
if all_mapped:
tree.setData(terminal.index().siblingAtColumn(1), QBrush(GREEN), Qt.BackgroundRole)
if len(channel_names) == 1:
tree.setData(terminal.index().siblingAtColumn(1), channel_names.pop())
else:
tree.setData(terminal.index().siblingAtColumn(1), "")
elif any_mapped:
tree.setData(terminal.index().siblingAtColumn(1), QBrush(YELLOW), Qt.BackgroundRole)
# color duplicates
for items_with_param in parameter_duplicates.values():
if len(items_with_param) != 1:
for duplicate in items_with_param:
duplicate.setData(QBrush(PINK), Qt.BackgroundRole)
duplicate.parent().setData(QBrush(PINK), Qt.BackgroundRole)
# tree.setData(duplicate.index(), QBrush(PINK), Qt.BackgroundRole)
# tree.setData(duplicate.parent().index(), QBrush(PINK), Qt.BackgroundRole)
# any item selected?
if self.selected_terminal_tree_elem is not None:
self.selected_terminal_tree_elem.setData(QBrush(BLUE), Qt.BackgroundRole)
# tree.setData(self.selected_terminal_tree_elem, QBrush(BLUE), Qt.BackgroundRole)
self.resizeColumnToContents(1)
self.update_monitoring()
[docs] def import_data(self, terminal_parameters: TerminalParameters) -> None:
"""Build up tree with provided terminal parameters."""
root = self.model().invisibleRootItem()
self.terminal_parameters = terminal_parameters
for terminal_name, terminal_params in terminal_parameters.items():
item = QStandardItem(terminal_name)
# item.setData((terminal_name, tuple(terminal_params.keys())))
# print(item.data(),item.text())
item.source = (terminal_name, tuple(terminal_params.keys()))
root.appendRow(item)
# if isinstance(terminal_params, Mapping):
for terminal_param_name in terminal_params.keys():
subitem = QStandardItem(terminal_param_name)
# subitem.setData((terminal_name, terminal_param_name))
# print(subitem.data(),subitem.text())
subitem.source = (terminal_name, terminal_param_name)
item.appendRow(subitem)
qidx = item.index()
self.model().setData(qidx.siblingAtColumn(1), QBrush(RED), Qt.BackgroundRole)
self.model().insertColumn(1, qidx)
self.model().insertColumn(2, qidx)
for i in range(len(terminal_params.keys())):
self.model().setData(qidx.child(i, 1), "")
self.model().setData(qidx.child(i, 1), QBrush(RED), Qt.BackgroundRole)
self.model().setData(qidx.child(i, 2), "")
self.setColumnHidden(2, not self.monitoring_enable)
[docs]class InstrumentTreeView(QTreeView):
"""QTreeView, that displays qcodes instruments."""
drag_terminal_drop_instr = pyqtSignal(QStandardItem, QStandardItem)
def __init__(self):
super().__init__()
model = QStandardItemModel()
# model.setHorizontalHeaderLabels(["Name", "Terminal"])
model.setHorizontalHeaderLabels(["Instrument Name"])
self.setModel(model)
self.setAcceptDrops(True)
self.setDropIndicatorShown(True)
[docs] def mouseDoubleClickEvent(self, event: QMouseEvent) -> None:
pass
[docs] def focusOutEvent(self, a0: QFocusEvent) -> None:
mainwindow = self.parentWidget()
for child in mainwindow.children():
if isinstance(child, TerminalTreeView):
terminal_tree = child
break
if terminal_tree.selected_terminal_tree_elem is not None:
terminal_tree.selected_terminal_tree_elem = None
terminal_tree.update_tree()
return super().focusOutEvent(a0)
# forward keypressevents "hack". keyPressEvent of QMainWindow doesnt fire for letter keys
[docs] def keyPressEvent(self, event: QKeyEvent) -> None:
if event.key() == Qt.Key_A or event.key() == Qt.Key_R or event.key() == Qt.Key_E or event.key() == Qt.Key_U:
self.parentWidget().keyPressEvent(event)
else:
return super().keyPressEvent(event)
[docs] def get_perfect_mappings(self, terminal_params: list[str], parent_elem=None) -> list[QStandardItem]:
"""
Given a list of terminal_parameters (names) return a list of QStandardItems in InstrumentTree
(perfect mapping candidate) that could be mapped perfectly i.e. every terminal_parameter can be
mapped uniquely to the list of all children parameters of the perfect mapping candidate.
"""
if parent_elem is None:
parent_elem = self.model().invisibleRootItem()
perfect_elems = []
for instrument_elem in get_children(parent_elem):
if instrument_elem.hasChildren():
all_mappings = self.get_all_possible_mapping_names(instrument_elem)
perfect_mapping = True
for terminal_param in terminal_params:
if all_mappings.count(terminal_param) != 1:
perfect_mapping = False
break
if perfect_mapping:
perfect_elems.append(instrument_elem)
if instrument_elem.hasChildren():
perfect_elems = perfect_elems + self.get_perfect_mappings(terminal_params, instrument_elem)
return perfect_elems
[docs] def get_all_possible_mapping_names(self: InstrumentTreeView, instrument_elem: QStandardItem) -> list[str] | None:
"""
Get all possible mapping names for all children of instrument_elem. None if
"""
mapping_names = []
if not hasattr(instrument_elem, "source"):
return None
if instrument_elem.hasChildren():
for child in get_children(instrument_elem):
mapping_names = mapping_names + self.get_all_possible_mapping_names(child)
else:
if isinstance(instrument_elem.source, Parameter):
if hasattr(instrument_elem.source, "_mapping"):
return [instrument_elem.source._mapping]
return mapping_names
[docs] def map_given_terminal_instrument_elem_selection(
self,
tree: TerminalTreeView,
terminal_tree_traversal: tuple[str, tuple[str]] | tuple[str, str],
instr_elem: Metadatable | Parameter,
) -> bool:
"""
For a selected item in terminal_tree (given via terminal_tree_traversal) and selected item in instrument_tree
(instr_elem) do the mapping process. Behaviour based on combinations like: direct mapping between parameters,
automap to all children etc.
"""
mapped = False
if isinstance(terminal_tree_traversal[1], str) and isinstance(instr_elem, Parameter):
# map directly - should mapping be forbidden if _mapping attribute of Parameter does not fit?
tree.map_parameter(instr_elem, terminal_tree_traversal)
mapped = True
# maybe later - if used this should be done somewhere else (map_parameter)
# self.add_terminal_to_view(parent, row, f"{terminal[0]}.{terminal[1]}")
elif isinstance(terminal_tree_traversal[1], tuple) and isinstance(instr_elem, (InstrumentModule, Instrument)):
# map automatically as much as possible
all_params = filter_flatten_parameters(instr_elem)
child_params_all = {
param_name: param
for param_name, param in all_params.items()
if instr_elem in param.instrument.ancestors
}
# try to map chosen terminal_parameters with child parameters of instr_elem if uniquely possible
possible_mappings = get_possible_mapping_candidates(terminal_tree_traversal[1], child_params_all)
for terminal_param_name, parameter_candidates in possible_mappings.items():
if len(parameter_candidates) == 1:
# unique mapping - iterate over terminal_parameters of terminal
terminal = get_child(tree.model().invisibleRootItem(), terminal_tree_traversal[0])
terminal_param = get_child(terminal, terminal_param_name)
tree.map_parameter(parameter_candidates[0], terminal_param.source)
mapped = True
elif isinstance(terminal_tree_traversal[1], str) and isinstance(instr_elem, (InstrumentModule, Instrument)):
# map automatically to one parameter
all_params = filter_flatten_parameters(instr_elem)
child_params_all = {
param_name: param
for param_name, param in all_params.items()
if instr_elem in param.instrument.ancestors
}
possible_mappings = get_possible_mapping_candidates((terminal_tree_traversal[1],), child_params_all)
# dict is always of size 1 (only one terminal_parameter)
terminal_param_name = terminal_tree_traversal[1]
possible_instrument_parameters = possible_mappings[terminal_param_name]
if len(possible_instrument_parameters) == 1:
if terminal_param_name == terminal_tree_traversal[1]:
tree.map_parameter(possible_instrument_parameters[0], terminal_tree_traversal)
mapped = True
elif isinstance(terminal_tree_traversal[1], tuple) and isinstance(instr_elem, Parameter):
# map parameter of chosen terminal to chosen instrument parameter
parameter = instr_elem
terminal_parameters = terminal_tree_traversal[1]
if hasattr(parameter, "_mapping"):
if parameter._mapping in terminal_parameters:
tree.map_parameter(parameter, (terminal_tree_traversal[0], parameter._mapping))
mapped = True
return mapped
[docs] def dropEvent(self, event: QDropEvent) -> None:
"""
Start mapping based on selected element in terminal tree (dragged from)
and selected element in instrument tree (dropped to).
Different mapping behaviour depending on the combination of "types" of elements
(see map_given_terminal_instrument_elem_selection).
"""
dest_index = self.indexAt(event.pos())
if not dest_index.isValid():
return
if dest_index.column() != 0:
instr_elem = self.model().itemFromIndex(dest_index.siblingAtColumn(0))
else:
instr_elem = self.model().itemFromIndex(dest_index)
tree = event.source()
assert isinstance(tree, TerminalTreeView)
terminal_tree_idx = tree.currentIndex().siblingAtColumn(0)
terminal_elem = tree.model().itemFromIndex(terminal_tree_idx)
# communicate selected instrument/terminal to main window.
# drag_terminal_drop_instr_slot catches this signal and does mapping
self.drag_terminal_drop_instr.emit(instr_elem, terminal_elem)
[docs] def add_terminal_to_view(self, parent, row, terminal_name):
model = self.model()
# Create cell, if its not there yet
if not model.hasIndex(row, 1, parent):
model.insertColumn(1, parent)
# Add terminal name to instrument row
model.setData(parent.child(row, 1), terminal_name)
[docs] def import_data(self, components: Mapping[Any, Metadatable]) -> None:
"""Build up tree with provided instruments."""
parent = self.model().invisibleRootItem()
seen: set[int] = set()
def recurse(node, parent) -> None:
"""Recursive part of the function. Fills instrument_parameters dict."""
# TODO: Change this try-except-phrase to match-case, when switched to Python3.10
try:
values = list(node.values()) if isinstance(node, dict) else list(node)
except KeyError:
values = [node]
except IndexError:
values = []
for value in values:
if isinstance(value, Parameter):
item = QStandardItem(value.name)
item.source = value
parent.appendRow(item)
else:
if isinstance(value, Iterable) and not isinstance(value, str):
recurse(value, parent)
elif isinstance(value, Metadatable):
# Object of some Metadatable type, try to get __dict__ and _filter_flatten_parameters
try:
value_hash = hash(value)
if parent is not self.model().invisibleRootItem():
try:
if value in parent.source.ancestors:
continue
if len(value.ancestors) >= 2:
if not value.ancestors[1] == parent.source:
# print(f"{value.full_name} is not a direct decendant of
# {parent.source.full_name}")
continue
except AttributeError:
continue
if value_hash not in seen:
seen.add(value_hash)
item = QStandardItem(value.name)
item.source = (
value # TODO: does it make sense to save value as .data()? or is .source ok?
)
parent.appendRow(item)
recurse(vars(value), item)
# if item has no parameters (item has no children after recurse) delete (last) row
if not item.hasChildren():
num_rows = parent.rowCount()
parent.removeRow(num_rows - 1)
except TypeError:
# End of tree
pass
recurse(components, parent)
[docs]class MainWindow(QMainWindow):
"""
Main window containing the two trees and buttons
"""
def __init__(
self,
components,
terminal_parameters,
monitoring: bool = False,
):
super().__init__()
self.components = components
self.setWindowTitle("Mapping GUI")
container = QWidget()
layout = QVBoxLayout()
# Tree views
self.instrument_tree = InstrumentTreeView()
self.instrument_tree.import_data(components)
self.instrument_tree.setSelectionMode(self.instrument_tree.SelectionMode(1))
self.instrument_tree.drag_terminal_drop_instr.connect(self.drag_terminal_drop_instr_slot)
self.instrument_tree.setSizePolicy(QSizePolicy.Policy(3), QSizePolicy.Policy(3))
self.terminal_tree = TerminalTreeView(monitoring)
self.terminal_tree.import_data(terminal_parameters)
self.terminal_tree.instrument_model = self.instrument_tree.model()
self.terminal_tree.setSelectionMode(self.terminal_tree.SelectionMode(1))
self.terminal_tree.selected_terminal_tree_elem = None
self.terminal_tree.expandAll()
self.terminal_tree.resizeColumnToContents(0)
self.terminal_tree.collapseAll()
self.terminal_tree.resizeColumnToContents(1)
self.terminal_tree.setSizePolicy(QSizePolicy.Policy(3), QSizePolicy.Policy(3))
upper_widget = QSplitter(Qt.Horizontal)
upper_widget.addWidget(self.terminal_tree)
upper_widget.addWidget(self.instrument_tree)
upper_widget.setChildrenCollapsible(False)
# Buttons
button_container = QWidget()
button_layout = QHBoxLayout()
self.button_map_auto = QPushButton("Map automatically (a)")
self.button_map_auto.clicked.connect(self.map_automatically)
self.button_reset_mapping = QPushButton("Reset mapping (r)")
self.button_reset_mapping.clicked.connect(self.reset_mapping)
self.button_unfold_terminals = QPushButton("Unfold (u)")
self.button_unfold_terminals.clicked.connect(self.unfold_terminals)
self.button_exit = QPushButton("Exit (e)")
self.button_exit.clicked.connect(self.close)
# Button layout
button_layout.addWidget(self.button_map_auto)
button_layout.addWidget(self.button_reset_mapping)
button_layout.addWidget(self.button_unfold_terminals)
button_layout.addStretch()
button_layout.addWidget(self.button_exit)
button_container.setLayout(button_layout)
button_container.setSizePolicy(QSizePolicy.Policy(3), QSizePolicy.Policy(0))
# Menu
menu = self.menuBar()
# Monitoring
Monitoring_menu = menu.addMenu("Monitoring")
self.monitoring_enable = monitoring
self.terminal_tree.monitoring_get_type = "get"
if monitoring:
self.terminal_tree.monitoring_timer.start(1000)
# Monitoring - Refresh delay
self.monitoring_refresh_delay = QAction("Refresh Delay", self)
self.monitoring_refresh_delay.triggered.connect(self.set_refresh_rate)
Monitoring_menu.addAction(self.monitoring_refresh_delay)
# Monitoring - Enable/Disable
if monitoring:
self.toggle_monitoring_action = QAction("Disable", self)
else:
self.toggle_monitoring_action = QAction("Enable", self)
self.toggle_monitoring_action.triggered.connect(self.toggle_monitoring)
Monitoring_menu.addAction(self.toggle_monitoring_action)
# Monitoring - Get type (from cache or by get() command)
get_type_menu = Monitoring_menu.addMenu("Get type")
self.use_cache_action = QAction("Only Cached values", self)
self.use_get_action = QAction("Get command", self)
self.use_cache_action.setCheckable(True)
self.use_get_action.setCheckable(True)
self.use_get_action.setChecked(True)
get_type_menu.addAction(self.use_cache_action)
get_type_menu.addAction(self.use_get_action)
self.use_get_action.triggered.connect(self.monitoring_set_get_type)
self.use_cache_action.triggered.connect(self.monitoring_set_cache_type)
# Help button
help_action = menu.addAction("Help")
help_action.triggered.connect(self.show_help)
# Main layout
layout.addWidget(upper_widget)
layout.addWidget(button_container)
container.setLayout(layout)
self.setCentralWidget(container)
self.terminal_tree.setFocus()
idx = self.terminal_tree.model().invisibleRootItem().child(0, 0).index()
self.terminal_tree.selectionModel().select(idx, QItemSelectionModel.SelectionFlag.ClearAndSelect)
self.terminal_tree.setCurrentIndex(idx)
self.resize(QDesktopWidget().availableGeometry(self).size() * 0.45)
self.terminal_parameters = terminal_parameters
self.terminal_tree.update_tree()
[docs] def closeEvent(self, ev) -> None:
"""
Before closing check if some things about the mapping (fully mapped, no duplicates). Then close the application
"""
all_mapped = True
parameter_duplicates = {}
for terminal_params in self.terminal_parameters.values():
for param in terminal_params.values():
if param is None:
all_mapped = False
elif isinstance(param, Parameter):
param_hash = hash(param)
if param_hash in parameter_duplicates:
parameter_duplicates[param_hash].append(param)
else:
parameter_duplicates[param_hash] = [param]
else:
raise TypeError("Gate parameters have to be either None or of type Parameter.")
# Not every terminal parameter is mapped
if not all_mapped:
dialog = MessageBox_notallmapped(self)
answer = dialog.exec()
if not answer == QMessageBox.Yes:
ev.ignore()
return
# Give warning if there are duplicates
for items_with_param in parameter_duplicates.values():
if len(items_with_param) != 1:
dialog = MessageBox_duplicates(self)
answer = dialog.exec()
# question = ("Do you really want to stop the mapping process? "
# "Multiple terminal parameters are mapped to the same parameter!")
# answer = dialog.question(self, "", question, QMessageBox.Yes | QMessageBox.No)
if not answer == QMessageBox.Yes:
ev.ignore()
return
# properly close application
self.terminal_tree.monitoring_timer.stop()
QApplication.exit()
return super().closeEvent(ev)
[docs] def show_help(self):
gui_help_txt_path = __file__.replace("mapping_gui.py", "GUI_help.txt")
with open(gui_help_txt_path) as f:
help_txt = f.read()
self.help_window = ScrollLabel(help_txt)
self.help_window.show()
[docs] def monitoring_set_get_type(self):
self.use_get_action.setChecked(True)
self.use_cache_action.setChecked(False)
self.terminal_tree.monitoring_get_type = "get"
[docs] def monitoring_set_cache_type(self):
self.use_cache_action.setChecked(True)
self.use_get_action.setChecked(False)
self.terminal_tree.monitoring_get_type = "cache"
[docs] @pyqtSlot(QStandardItem, QStandardItem)
def drag_terminal_drop_instr_slot(self, instr_elem, terminal_elem):
"""
This receives signal from InstrumentTree if two elements were paired via drag and drop.
Mapping is carried out from here (mainwindow)
"""
self.map_given_terminal_instrument_elem_selection(terminal_elem.source, instr_elem.source)
self.terminal_tree.update_tree()
[docs] def toggle_monitoring(self):
if self.monitoring_enable:
self.terminal_tree.monitoring_timer.stop()
self.monitoring_enable = False
self.toggle_monitoring_action.setText("Enable")
else:
self.terminal_tree.monitoring_timer.start(1000)
self.monitoring_enable = True
self.toggle_monitoring_action.setText("Disable")
self.terminal_tree.setColumnHidden(2, not self.monitoring_enable)
[docs] def set_refresh_rate(self):
"""
Set refresh rate of monitoring via input dialog.
"""
val, ok = QInputDialog.getDouble(
self, "Refresh delay [s]", "Refresh delay [s]", 1, 0.01, 100, 2, Qt.WindowFlags(), 0.1
)
if ok:
self.terminal_tree.monitoring_timer.stop()
self.terminal_tree.showColumn(2)
self.toggle_monitoring_action.setText("Disable")
self.terminal_tree.monitoring_timer.start(int(val * 1000))
self.monitoring_enable = True
[docs] def map_parameter(self, parameter: Parameter, traverse: tuple[str, str]):
"""
Maps a instrument parameter to a specific terminal parameter accessed by the given traversal info.
Doesn't do much anymore, but I kept this around for slightly better readability
(and easier refactoring if necessary).
"""
self.terminal_parameters[traverse[0]][traverse[1]] = parameter
[docs] def keyPressEvent(self, event: QKeyEvent) -> None:
"""
Handles keyboard shortcuts and mapping using enter key. Selecting an instrument in the terminal_tree and
pressing enter will switch focus to the instrument_tree and select a suitable mapping candidate.
The user can change the selection and press enter again to do the mapping. The focus switches back to the
terminal_tree and a new terminal is selected.
"""
if event.key() == Qt.Key_Enter or event.key() == Qt.Key_Return:
sel_idx = []
for idx in self.terminal_tree.selectedIndexes():
if idx.column() == 0:
sel_idx.append(idx)
if self.terminal_tree.hasFocus() and len(sel_idx) == 1:
# Element in terminal tree has been selected.
# select element and switch focus to instruments
self.terminal_tree.selected_terminal_tree_elem = sel_idx[0]
selected_item = self.terminal_tree.model().itemFromIndex(self.terminal_tree.selected_terminal_tree_elem)
self.instrument_tree.setFocus()
# select instrument element based on predicted mapping
if selected_item.hasChildren():
_terminal_params = get_children(selected_item)
terminal_params = []
for terminal_param in _terminal_params:
terminal_params.append(terminal_param.source[1])
else:
terminal_params = [selected_item.source[1]]
perfect_mappings = self.instrument_tree.get_perfect_mappings(terminal_params)
# set idx to first perfect_mapping that has not yet been fully mapped to a terminal
idx = self.instrument_tree.model().invisibleRootItem().child(0, 0).index() # default
for perfect_mapping in perfect_mappings:
instr_mapped = False
for terminal in get_children(self.terminal_tree.model().invisibleRootItem()):
if (
self.terminal_tree.model().data(terminal.index().siblingAtColumn(1))
== perfect_mapping.source.full_name
):
instr_mapped = True
break
if not instr_mapped:
idx = perfect_mapping.index()
break
self.instrument_tree.selectionModel().select(idx, QItemSelectionModel.SelectionFlag.ClearAndSelect)
self.instrument_tree.setCurrentIndex(idx)
elif (
self.instrument_tree.hasFocus()
and self.terminal_tree.selected_terminal_tree_elem is not None
and len(self.instrument_tree.selectedIndexes()) == 1
):
# Element in instrument tree selected. Start mapping
terminal_elem = self.terminal_tree.model().itemFromIndex(self.terminal_tree.selected_terminal_tree_elem)
idx_instr = self.instrument_tree.selectedIndexes()[0]
instr_elem = self.instrument_tree.model().itemFromIndex(idx_instr)
# Check if it is already mapped to display dialog
any_mapped = False
if type(terminal_elem.source[1]) is str:
terminal_params = (terminal_elem.source[1],)
else:
terminal_params = terminal_elem.source[1]
for terminal_param in terminal_params:
if not self.terminal_parameters[terminal_elem.source[0]][terminal_param] is None:
any_mapped = True
break
if any_mapped:
break
if any_mapped:
dialog = MessageBox_overwrite(self)
answer = dialog.exec()
if answer == QMessageBox.Yes:
do_mapping = True
else:
do_mapping = False
else:
do_mapping = True
if do_mapping:
mapped = self.map_given_terminal_instrument_elem_selection(terminal_elem.source, instr_elem.source)
if mapped:
self.terminal_tree.selected_terminal_tree_elem = None
# select next not fully mapped terminal (quick navigation)
self.terminal_tree.setFocus()
for terminal_name, terminal in self.terminal_tree.terminal_parameters.items():
all_mapped = True
for terminal_param_name, terminal_param in terminal.items():
if terminal_param is None:
all_mapped = False
break
if not all_mapped:
terminal_elem = get_child(self.terminal_tree.model().invisibleRootItem(), terminal_name)
self.terminal_tree.selectionModel().select(
terminal_elem.index(), QItemSelectionModel.SelectionFlag.ClearAndSelect
)
self.terminal_tree.setCurrentIndex(terminal_elem.index())
break
# self.terminal_tree.update_tree()
elif event.key() == Qt.Key_A:
self.map_automatically()
elif event.key() == Qt.Key_R:
self.reset_mapping()
elif event.key() == Qt.Key_E:
self.close()
elif event.key() == Qt.Key_U:
self.unfold_terminals()
self.terminal_tree.update_tree()
return super().keyPressEvent(event)
[docs] def map_given_terminal_instrument_elem_selection(
self,
terminal_tree_traversal: tuple[str, tuple[str]] | tuple[str, str],
instr_elem: Metadatable | Parameter,
) -> bool:
"""
For a selected item in terminal_tree (given via terminal_tree_traversal) and selected item
in instrument_tree (instr_elem) do the mapping process.
Behaviour based on combinations like: direct mapping between parameters, automap to all children etc.
"""
tree = self.terminal_tree
mapped = False
if isinstance(terminal_tree_traversal[1], str) and isinstance(instr_elem, Parameter):
# map directly
self.map_parameter(instr_elem, terminal_tree_traversal)
mapped = True
elif isinstance(terminal_tree_traversal[1], tuple) and isinstance(instr_elem, (InstrumentModule, Instrument)):
# map automatically as much as possible
all_params = filter_flatten_parameters(instr_elem)
child_params_all = {
param_name: param
for param_name, param in all_params.items()
if instr_elem in param.instrument.ancestors
}
# try to map chosen terminal_parameters with child parameters of instr_elem if uniquely possible
possible_mappings = get_possible_mapping_candidates(terminal_tree_traversal[1], child_params_all)
for terminal_param_name, parameter_candidates in possible_mappings.items():
if len(parameter_candidates) == 1:
# unique mapping - iterate over terminal_parameters of terminal
terminal = get_child(tree.model().invisibleRootItem(), terminal_tree_traversal[0])
terminal_param = get_child(terminal, terminal_param_name)
self.map_parameter(parameter_candidates[0], terminal_param.source)
mapped = True
elif isinstance(terminal_tree_traversal[1], str) and isinstance(instr_elem, (InstrumentModule, Instrument)):
# map automatically to one parameter
all_params = filter_flatten_parameters(instr_elem)
child_params_all = {
param_name: param
for param_name, param in all_params.items()
if instr_elem in param.instrument.ancestors
}
possible_mappings = get_possible_mapping_candidates((terminal_tree_traversal[1],), child_params_all)
# dict is always of size 1 (only one terminal_parameter)
terminal_param_name = terminal_tree_traversal[1]
possible_instrument_parameters = possible_mappings[terminal_param_name]
if len(possible_instrument_parameters) == 1:
if terminal_param_name == terminal_tree_traversal[1]:
self.map_parameter(possible_instrument_parameters[0], terminal_tree_traversal)
mapped = True
elif isinstance(terminal_tree_traversal[1], tuple) and isinstance(instr_elem, Parameter):
# map parameter of chosen terminal to chosen instrument parameter
parameter = instr_elem
terminal_parameters = terminal_tree_traversal[1]
if hasattr(parameter, "_mapping"):
if parameter._mapping in terminal_parameters:
self.map_parameter(parameter, (terminal_tree_traversal[0], parameter._mapping))
mapped = True
return mapped
[docs] def unfold_terminals(self):
"""
Unfolds all terminals in the terminal_tree (quickly make everything visible). Collapse all if already unfolded
"""
all_expanded = True
for terminal in get_children(self.terminal_tree.model().invisibleRootItem()):
if not self.terminal_tree.isExpanded(terminal.index()):
all_expanded = False
break
if all_expanded:
self.terminal_tree.collapseAll()
else:
self.terminal_tree.expandAll()
self.terminal_tree.update_tree()
[docs] def map_automatically(self):
"""
Map all terminals automatically. The algorithm used is (almost) equivalent to selecting the first terminal and
repeatedly pressing the enter key until the last terminal (in the tree) is mapped. This works best if the
terminals are in the same order as the instruments that they should be mapped to. Additionally the terminals
mapping to channels of an instrument should be ordered the same as the channels
(up to the driver but usually something like 0,1,2,...).
"""
self.reset_mapping()
for terminal_name, terminal in self.terminal_parameters.items():
_perfect_mappings = self.instrument_tree.get_perfect_mappings(terminal.keys())
# filtering out parent channels (instrument) that would also lead to unique mapping
# This is an edge case for instruments that have a single channel of some type
# (then both the channel and the instrument are uniquely mappable)
perfect_mappings = []
for _perfect_mapping1 in _perfect_mappings:
# check if there is a mapping which has _perfect_mapping1 as parent
child_in_perfect_mappings = False
for _perfect_mapping2 in _perfect_mappings:
if _perfect_mapping1.source is _perfect_mapping2.source.parent:
child_in_perfect_mappings = True
break
if not child_in_perfect_mappings:
perfect_mappings.append(_perfect_mapping1)
# map to first perfect_mapping that has not yet been fully mapped to a terminal
for perfect_mapping in perfect_mappings:
perfect_mapping_channel_name = perfect_mapping.source.full_name
instr_mapped = False
# find out if perfect mapping candidate has been already perfectly mapped to another terminal
for _terminal_name, _terminal in self.terminal_parameters.items():
all_mapped_to_same_channel = True
for terminal_param_name, param in _terminal.items():
if param is not None:
if param.instrument.full_name != perfect_mapping_channel_name:
all_mapped_to_same_channel = False
break
else:
all_mapped_to_same_channel = False
break
if all_mapped_to_same_channel:
instr_mapped = True
break
if not instr_mapped:
# map to channel
terminal_element = (terminal_name, tuple(terminal.keys()))
self.map_given_terminal_instrument_elem_selection(terminal_element, perfect_mapping.source)
break
self.terminal_tree.update_tree()
# Not used anymore. Unique, but algorithm to weak to be useful in practise
[docs] def map_automatically_unique(self):
"""
Automatically map all unique terminal_parameter instrument_parameter pairs. If there are multiple
terminal_parameters with the same name their unique mapping is impossible.
"""
# call get_possible_mapping_candidates for each terminal
terminal_mapping_candidates = {}
terminal_parameters_occurances = {}
all_params = filter_flatten_parameters(self.components)
for terminal_name, terminal_params in self.terminal_tree.terminal_parameters.items():
terminal_mapping_candidates[terminal_name] = get_possible_mapping_candidates(
terminal_params.keys(), all_params
)
for terminal_param in terminal_params.keys():
if terminal_param in terminal_parameters_occurances.keys():
terminal_parameters_occurances[terminal_param] += 1
else:
terminal_parameters_occurances[terminal_param] = 1
# a terminal parameter can be mapped uniquely if its mapped parameter only appears once in the candidate
# dictionaries of all terminals
# For a unique mapping between terminal parameter and instrument parameter:
# 1. a terminal parameter (name) must only occur a single time for all terminals
# 2. unique mapping from terminal parameter to instrument_parameter (true if list of candidates has length 1)
for terminal, terminal_params in self.terminal_tree.terminal_parameters.items():
all_mapped = True
for terminal_param in terminal_params.keys():
if not terminal_parameters_occurances[terminal_param] == 1:
all_mapped = False
continue
if not len(terminal_mapping_candidates[terminal][terminal_param]) == 1:
all_mapped = False
continue
self.map_parameter(terminal_mapping_candidates[terminal][terminal_param][0], (terminal, terminal_param))
if all_mapped:
self.terminal_tree.collapse(get_child(self.terminal_tree.model().invisibleRootItem(), terminal).index())
else:
self.terminal_tree.expand(get_child(self.terminal_tree.model().invisibleRootItem(), terminal).index())
self.terminal_tree.update_tree()
[docs] def reset_mapping(self):
"""
Reset all mappings. Reset dictionary which actually holds the mapping. Reset Tree representation.
"""
for terminal_name, terminal_params in self.terminal_parameters.items():
self.terminal_parameters[terminal_name] = {
terminal_param_name: None for terminal_param_name in terminal_params.keys()
}
self.terminal_tree.selected_terminal_tree_elem = None
self.terminal_tree.update_tree()
self.terminal_tree.setFocus()
[docs]def get_possible_mapping_candidates(
terminal_params: tuple[str], instrument_parameters: Mapping[Any, Parameter]
) -> Mapping[Any, list[Parameter]]:
"""
For input terminal and collection of instrument_parameters: get dictionary with key:
terminal parameter name, value: list(parameters that can be mapped to that terminal parameter)
Similar to base.py _map_gate_to_instrument
"""
mapped_parameters = {
key: parameter for key, parameter in instrument_parameters.items() if hasattr(parameter, "_mapping")
}
mapping = {}
for terminal_param in terminal_params:
candidates = [parameter for parameter in mapped_parameters.values() if parameter._mapping == terminal_param]
mapping[terminal_param] = candidates
return mapping
[docs]def get_children(parent: QStandardItem) -> list[QStandardItem]:
"""Return list of children (QStandardItem) of parent"""
children = []
num_rows = parent.rowCount()
if num_rows == 0:
return []
_row = 0
while True:
children.append(parent.child(_row, 0))
if _row == num_rows - 1:
break
_row = _row + 1
return children
[docs]def get_child(parent: QStandardItem, text: str) -> QStandardItem | None:
for child in get_children(parent):
if child.text() == text:
return child
return None
[docs]def traverse_tree(root: QStandardItem, traversal_names: list(str)) -> QStandardItem:
def traverse(parent, names):
if names == []:
return parent
child = get_child(parent, names.pop(0))
if child is None:
print("problem")
else:
return traverse(child, names)
return traverse(root, traversal_names)
[docs]class MessageBox_notallmapped(QMessageBox):
def __init__(self, parent):
super().__init__(parent)
self.setWindowTitle("Warning! Not all parameters mapped!")
self.setIcon(QMessageBox.Warning)
self.setText("Do you really want to stop the mapping process? Not all parameters are mapped!")
self.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
[docs]class MessageBox_duplicates(QMessageBox):
def __init__(self, parent):
super().__init__(parent)
self.setWindowTitle("Warning! Duplicate mapping!")
self.setIcon(QMessageBox.Warning)
self.setText(
"Do you really want to stop the mapping process? "
"Multiple terminal parameters are mapped to the same parameter!"
)
self.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
[docs]class MessageBox_overwrite(QMessageBox):
def __init__(self, parent):
super().__init__(parent)
self.setWindowTitle("Warning! Duplicate mapping!")
self.setIcon(QMessageBox.Warning)
self.setText("Do you really want to change/overwrite the existing mapping?")
self.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
[docs]def map_terminals_gui(
components: Mapping[Any, Metadatable],
terminal_parameters: TerminalParameters,
existing_terminal_parameters: TerminalParameters | None = None,
metadata: Metadata | None = None,
monitoring: bool = True,
skip_gui_if_mapped: bool = True,
) -> None:
"""
Maps the terminals, that were defined in the MeasurementScript to the instruments, that are initialized in QCoDeS.
Args:
components (Mapping[Any, Metadatable]): Instruments/Components in QCoDeS
terminal_parameters (Mapping[Any, Union[Mapping[Any, Parameter], Parameter]]):
Terminals, as defined in the measurement script
existing_terminal_parameters (Mapping[Any, Union[Mapping[Any, Parameter], Parameter]] | None):
Already existing mapping, that is used to automatically create the mapping
for already known terminals without user input.
metadata (Metadata | None): If provided, add mapping to the metadata object.
monitoring (bool):
If True the mapped parameters are periodically read out
(either by get command (default) or cached value)
skip_gui_if_mapped (bool):
If True and existing_terminal_parameters completely covers all terminal_parameters,
dont open gui and continue
"""
if existing_terminal_parameters is None:
# reset in case there is already some mapping
for terminal_name, parameter_mapping in terminal_parameters.items():
for terminal_parameter, instr_parameter in parameter_mapping.items():
terminal_parameters[terminal_name][terminal_parameter] = None
run_gui = True
else:
# try to get the mapping from an existing mapping (if the respective terminal name exists)
for terminal_name, terminal_params in terminal_parameters.items():
if terminal_name in existing_terminal_parameters:
for terminal_param_name, mapped_param in terminal_params.items():
if terminal_param_name in existing_terminal_parameters[terminal_name]:
terminal_parameters[terminal_name][terminal_param_name] = existing_terminal_parameters[
terminal_name
][terminal_param_name]
all_mapped = True
for terminal_params in terminal_parameters.values():
for param in terminal_params.values():
if param is None:
all_mapped = False
break
if not all_mapped:
break
run_gui = not all_mapped
if run_gui or not skip_gui_if_mapped:
if QApplication.instance() is None:
app = QApplication([])
else:
app = QApplication.instance()
w = MainWindow(
components,
terminal_parameters,
monitoring=monitoring,
)
w.show()
app.exec_()
# if metadata is provided, add mapping to metadata object
if metadata is not None:
j = json.dumps(terminal_parameters, default=lambda o: str(o))
metadata.add_terminal_mapping(j, "custom-mapping")