Merge pull request #12 from nqrduck/formbuilder-and-function-optimization

Formbuilder and function optimization
This commit is contained in:
Julia P 2024-04-26 17:54:54 +02:00 committed by GitHub
commit aee28b3d7a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 75 additions and 313 deletions

View file

@ -1,5 +1,7 @@
# Changelog # Changelog
### Version 0.0.3 (26-04-2024)
- Switched to new formbuilder provided by the nqrduck core
### Version 0.0.2 (18-04-2024) ### Version 0.0.2 (18-04-2024)
- Automatic deployment to PyPI - Automatic deployment to PyPI

View file

@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "nqrduck-pulseprogrammer" name = "nqrduck-pulseprogrammer"
version = "0.0.2" version = "0.0.3"
authors = [ authors = [
{ name="jupfi", email="support@nqrduck.cool" }, { name="jupfi", email="support@nqrduck.cool" },
] ]

View file

@ -1,5 +1,4 @@
import logging import logging
from decimal import Decimal
from collections import OrderedDict from collections import OrderedDict
from PyQt6.QtCore import pyqtSignal from PyQt6.QtCore import pyqtSignal
from nqrduck.module.module_model import ModuleModel from nqrduck.module.module_model import ModuleModel
@ -18,15 +17,15 @@ class PulseProgrammerModel(ModuleModel):
self.pulse_parameter_options = OrderedDict() self.pulse_parameter_options = OrderedDict()
self.pulse_sequence = PulseSequence("Untitled pulse sequence") self.pulse_sequence = PulseSequence("Untitled pulse sequence")
def add_event(self, event_name: str, duration: Decimal = 20): def add_event(self, event_name: str, duration: float = 20):
"""Add a new event to the current pulse sequence. """Add a new event to the current pulse sequence.
Args: Args:
event_name (str): A human-readable name for the event event_name (str): A human-readable name for the event
duration (Decimal): The duration of the event in µs. Defaults to 20. duration (float): The duration of the event in µs. Defaults to 20.
""" """
self.pulse_sequence.events.append( self.pulse_sequence.events.append(
PulseSequence.Event(event_name, "%.16gu" % duration) PulseSequence.Event(event_name, "%.16gu" % float(duration))
) )
logger.debug( logger.debug(
"Creating event %s with object id %s", "Creating event %s with object id %s",

View file

@ -1,11 +1,7 @@
import logging import logging
import functools import functools
from collections import OrderedDict
from decimal import Decimal
from PyQt6.QtGui import QValidator from PyQt6.QtGui import QValidator
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QMessageBox,
QGroupBox,
QFormLayout, QFormLayout,
QTableWidget, QTableWidget,
QVBoxLayout, QVBoxLayout,
@ -16,7 +12,6 @@ from PyQt6.QtWidgets import (
QLineEdit, QLineEdit,
QDialogButtonBox, QDialogButtonBox,
QWidget, QWidget,
QCheckBox,
QToolButton, QToolButton,
QFileDialog, QFileDialog,
QSizePolicy, QSizePolicy,
@ -30,6 +25,14 @@ from nqrduck_spectrometer.pulseparameters import (
NumericOption, NumericOption,
FunctionOption, FunctionOption,
) )
from nqrduck.helpers.formbuilder import (
DuckFormBuilder,
DuckFormFunctionSelectionField,
DuckFormCheckboxField,
DuckFormDropdownField,
DuckFormFloatField,
DuckFormIntField,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -175,7 +178,7 @@ class PulseProgrammerView(ModuleView):
logger.debug("Adding event to pulseprogrammer view: %s", event.name) logger.debug("Adding event to pulseprogrammer view: %s", event.name)
# Create a label for the event # Create a label for the event
event_label = QLabel( event_label = QLabel(
"%s : %.16g µs" % (event.name, (event.duration * Decimal(1e6))) "%s : %.16g µs" % (event.name, (event.duration * 1e6))
) )
event_layout.addWidget(event_label) event_layout.addWidget(event_label)
@ -260,19 +263,65 @@ class PulseProgrammerView(ModuleView):
"""This method is called whenever a button in the pulse table is clicked. It opens a dialog to set the options for the parameter.""" """This method is called whenever a button in the pulse table is clicked. It opens a dialog to set the options for the parameter."""
logger.debug("Button for event %s and parameter %s clicked", event, parameter) logger.debug("Button for event %s and parameter %s clicked", event, parameter)
# Create a QDialog to set the options for the parameter. # Create a QDialog to set the options for the parameter.
dialog = OptionsDialog(event, parameter, self) description = f"Set options for {parameter}"
dialog = DuckFormBuilder(parameter, description=description, parent=self)
# Adding fields for the options
form_options = []
for option in event.parameters[parameter].options:
if isinstance(option, BooleanOption):
boolean_form = DuckFormCheckboxField(
option.name, tooltip=None, default=option.value
)
dialog.add_field(boolean_form)
form_options.append(option)
elif isinstance(option, NumericOption):
# We only show the slider if both min and max values are set
if option.min_value is not None and option.max_value is not None:
slider = True
else:
slider = False
numeric_field = DuckFormFloatField(
option.name,
tooltip=None,
default=option.value,
min_value=option.min_value,
max_value=option.max_value,
slider=slider,
)
dialog.add_field(numeric_field)
form_options.append(option)
elif isinstance(option, FunctionOption):
logger.debug(f"Functions: {option.functions}")
# When loading a pulse sequence, the instance of the objects will be different
# Therefore we need to operate on the classes
for function in option.functions:
if function.__class__.__name__ == option.value.__class__.__name__:
default_function = function
index = option.functions.index(default_function)
function_field = DuckFormFunctionSelectionField(
option.name,
tooltip=None,
functions=option.functions,
duration=event.duration,
default_function=index,
)
dialog.add_field(function_field)
form_options.append(option)
result = dialog.exec() result = dialog.exec()
options = event.parameters[parameter].options
if result: if result:
for option, function in dialog.return_functions.items(): values = dialog.get_values()
logger.debug( for i, value in enumerate(values):
"Setting option %s of parameter %s in event %s to %s", options[i].value = value
option,
parameter,
event,
function(),
)
option.set_value(function())
self.set_parameter_icons() self.set_parameter_icons()
@ -382,7 +431,7 @@ class EventOptionsWidget(QWidget):
duration_label = QLabel("Duration:") duration_label = QLabel("Duration:")
duration_lineedit = QLineEdit() duration_lineedit = QLineEdit()
unit_label = QLabel("µs") unit_label = QLabel("µs")
duration_lineedit.setText("%.16g" % (self.event.duration * Decimal(1e6))) duration_lineedit.setText("%.16g" % (self.event.duration * 1e6))
duration_layout.addWidget(duration_label) duration_layout.addWidget(duration_label)
duration_layout.addWidget(duration_lineedit) duration_layout.addWidget(duration_lineedit)
duration_layout.addWidget(unit_label) duration_layout.addWidget(unit_label)
@ -441,294 +490,6 @@ class EventOptionsWidget(QWidget):
self.move_event_right.emit(self.event.name) self.move_event_right.emit(self.event.name)
class OptionsDialog(QDialog):
"""This dialog is created whenever the edit button for a pulse option is clicked.
It allows the user to change the options for the pulse parameter and creates the dialog in accordance to what can be set.
"""
def __init__(self, event, parameter, parent=None):
super().__init__(parent)
self.parent = parent
self.setWindowTitle("Options")
self.layout = QVBoxLayout(self)
numeric_layout = QFormLayout()
numeric_layout.setHorizontalSpacing(30)
self.label = QLabel("Change options for the pulse parameter: %s" % parameter)
self.layout.addWidget(self.label)
self.layout.addLayout(numeric_layout)
# If the parameter is a string, we first need to get the parameter object from the according event
if isinstance(parameter, str):
parameter = event.parameters[parameter]
options = parameter.get_options()
# Based on these options we will now create our selection widget
self.return_functions = OrderedDict()
# If the options are a list , we will create a QComboBox
for option in options:
if option == list:
pass
# If the options are boolean, we will create a QCheckBox
elif isinstance(option, BooleanOption):
check_box = QCheckBox()
def checkbox_result():
return check_box.isChecked()
check_box.setChecked(option.value)
self.layout.addWidget(check_box)
self.return_functions[option] = checkbox_result
# If the options are a float/int we will create a QSpinBox
elif isinstance(option, NumericOption):
numeric_label = QLabel(option.name)
numeric_lineedit = QLineEdit(str(option.value))
numeric_lineedit.setMaximumWidth(300)
numeric_layout.addRow(numeric_label, numeric_lineedit)
self.return_functions[option] = numeric_lineedit.text
# If the options are a string we will create a QLineEdit
elif option == str:
pass
elif isinstance(option, FunctionOption):
function_option = FunctionOptionWidget(option, event, parent)
self.layout.addWidget(function_option)
logger.debug("Return functions are: %s" % self.return_functions.items())
self.buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel,
self,
)
self.buttons.accepted.connect(self.accept)
self.buttons.rejected.connect(self.reject)
self.layout.addWidget(self.buttons)
class FunctionOptionWidget(QWidget):
"""This class is a widget that can be used to set the options for a pulse parameter.
It plots the given function in time and frequency domain.
One can also select the function from a list of functions represented as buttons.
"""
def __init__(self, function_option, event, parent=None):
super().__init__(parent)
self.parent = parent
self.function_option = function_option
self.event = event
layout = QVBoxLayout()
inner_layout = QHBoxLayout()
for function in function_option.functions:
button = QPushButton(function.name)
button.clicked.connect(
functools.partial(self.on_functionbutton_clicked, function=function)
)
inner_layout.addWidget(button)
layout.addLayout(inner_layout)
self.setLayout(layout)
# Add Advanced settings button
self.advanced_settings_button = QPushButton("Show Advanced settings")
self.advanced_settings_button.clicked.connect(
self.on_advanced_settings_button_clicked
)
layout.addWidget(self.advanced_settings_button)
# Add advanced settings widget
self.advanced_settings = QGroupBox("Advanced Settings")
self.advanced_settings.setHidden(True)
self.advanced_settings_layout = QFormLayout()
self.advanced_settings.setLayout(self.advanced_settings_layout)
layout.addWidget(self.advanced_settings)
# Add the advanced settings
# Advanced settings are resolution, start_x = -1, end_x and the expr of the function_option.value
resolution_layout = QHBoxLayout()
resolution_label = QLabel("Resolution:")
self.resolution_lineedit = QLineEdit(str(function_option.value.resolution))
resolution_layout.addWidget(resolution_label)
resolution_layout.addWidget(self.resolution_lineedit)
resolution_layout.addStretch(1)
self.advanced_settings_layout.addRow(resolution_label, resolution_layout)
start_x_layout = QHBoxLayout()
start_x_label = QLabel("Start x:")
self.start_x_lineedit = QLineEdit(str(function_option.value.start_x))
start_x_layout.addWidget(start_x_label)
start_x_layout.addWidget(self.start_x_lineedit)
start_x_layout.addStretch(1)
self.advanced_settings_layout.addRow(start_x_label, start_x_layout)
end_x_layout = QHBoxLayout()
end_x_label = QLabel("End x:")
self.end_x_lineedit = QLineEdit(str(function_option.value.end_x))
end_x_layout.addWidget(end_x_label)
end_x_layout.addWidget(self.end_x_lineedit)
end_x_layout.addStretch(1)
self.advanced_settings_layout.addRow(end_x_label, end_x_layout)
expr_layout = QHBoxLayout()
expr_label = QLabel("Expression:")
self.expr_lineedit = QLineEdit(str(function_option.value.expr))
expr_layout.addWidget(expr_label)
expr_layout.addWidget(self.expr_lineedit)
expr_layout.addStretch(1)
self.advanced_settings_layout.addRow(expr_label, expr_layout)
# Add buttton for replotting of the active function with the new parameters
self.replot_button = QPushButton("Replot")
self.replot_button.clicked.connect(self.on_replot_button_clicked)
layout.addWidget(self.replot_button)
# Display the active function
self.load_active_function()
@pyqtSlot()
def on_replot_button_clicked(self) -> None:
"""This function is called when the replot button is clicked.
It will update the parameters of the function and replots the function.
"""
logger.debug("Replot button clicked")
# Update the resolution, start_x, end_x and expr lineedits
self.function_option.value.resolution = self.resolution_lineedit.text()
self.function_option.value.start_x = self.start_x_lineedit.text()
self.function_option.value.end_x = self.end_x_lineedit.text()
try:
self.function_option.value.expr = self.expr_lineedit.text()
except SyntaxError:
logger.debug("Invalid expression: %s", self.expr_lineedit.text())
self.expr_lineedit.setText(str(self.function_option.value.expr))
# Create message box that tells the user that the expression is invalid
self.create_message_box(
"Invalid expression",
"The expression you entered is invalid. Please enter a valid expression.",
)
self.delete_active_function()
self.load_active_function()
@pyqtSlot()
def on_advanced_settings_button_clicked(self) -> None:
"""This function is called when the advanced settings button is clicked.
It will show or hide the advanced settings.
"""
if self.advanced_settings.isHidden():
self.advanced_settings.setHidden(False)
self.advanced_settings_button.setText("Hide Advanced Settings")
else:
self.advanced_settings.setHidden(True)
self.advanced_settings_button.setText("Show Advanced Settings")
@pyqtSlot()
def on_functionbutton_clicked(self, function) -> None:
"""This function is called when a function button is clicked.
It will update the function_option.value to the function that was clicked.
"""
logger.debug("Button for function %s clicked", function.name)
self.function_option.set_value(function)
self.delete_active_function()
self.load_active_function()
def delete_active_function(self) -> None:
"""This function is called when the active function is deleted.
It will remove the active function from the layout.
"""
# Remove the plotter with object name "plotter" from the layout
for i in reversed(range(self.layout().count())):
item = self.layout().itemAt(i)
if item.widget() and item.widget().objectName() == "active_function":
item.widget().deleteLater()
break
def load_active_function(self) -> None:
"""This function is called when the active function is loaded.
It will add the active function to the layout.
"""
# New QWidget for the active function
active_function_Widget = QWidget()
active_function_Widget.setObjectName("active_function")
function_layout = QVBoxLayout()
plot_layout = QHBoxLayout()
# Add plot for time domain
time_domain_layout = QVBoxLayout()
time_domain_label = QLabel("Time domain:")
time_domain_layout.addWidget(time_domain_label)
plot = self.function_option.value.time_domain_plot(self.event.duration)
time_domain_layout.addWidget(plot)
plot_layout.addLayout(time_domain_layout)
# Add plot for frequency domain
frequency_domain_layout = QVBoxLayout()
frequency_domain_label = QLabel("Frequency domain:")
frequency_domain_layout.addWidget(frequency_domain_label)
plot = self.function_option.value.frequency_domain_plot(self.event.duration)
frequency_domain_layout.addWidget(plot)
plot_layout.addLayout(frequency_domain_layout)
function_layout.addLayout(plot_layout)
parameter_layout = QFormLayout()
parameter_label = QLabel("Parameters:")
parameter_layout.addRow(parameter_label)
for parameter in self.function_option.value.parameters:
parameter_label = QLabel(parameter.name)
parameter_lineedit = QLineEdit(str(parameter.value))
# Add the parameter_lineedit editingFinished signal to the paramter.set_value slot
parameter_lineedit.editingFinished.connect(
lambda: parameter.set_value(parameter_lineedit.text())
)
# Create a QHBoxLayout
hbox = QHBoxLayout()
# Add your QLineEdit and a stretch to the QHBoxLayout
hbox.addWidget(parameter_lineedit)
hbox.addStretch(1)
# Use addRow() method to add label and the QHBoxLayout next to each other
parameter_layout.addRow(parameter_label, hbox)
function_layout.addLayout(parameter_layout)
function_layout.addStretch(1)
active_function_Widget.setLayout(function_layout)
self.layout().addWidget(active_function_Widget)
# Update the resolution, start_x, end_x and expr lineedits
self.resolution_lineedit.setText(str(self.function_option.value.resolution))
self.start_x_lineedit.setText(str(self.function_option.value.start_x))
self.end_x_lineedit.setText(str(self.function_option.value.end_x))
self.expr_lineedit.setText(str(self.function_option.value.expr))
def create_message_box(self, message: str, information: str) -> None:
"""Creates a message box with the given message and information and shows it.
Args:
message (str): The message to be shown in the message box
information (str): The information to be shown in the message box
"""
msg = QMessageBox(parent=self.parent)
msg.setIcon(QMessageBox.Icon.Warning)
msg.setText(message)
msg.setInformativeText(information)
msg.setWindowTitle("Warning")
msg.exec()
class AddEventDialog(QDialog): class AddEventDialog(QDialog):
"""This dialog is created whenever a new event is added to the pulse sequence. It allows the user to enter a name for the event.""" """This dialog is created whenever a new event is added to the pulse sequence. It allows the user to enter a name for the event."""
@ -782,13 +543,13 @@ class AddEventDialog(QDialog):
""" """
return self.name_input.text() return self.name_input.text()
def get_duration(self) -> Decimal: def get_duration(self) -> float:
"""Returns the duration entered by the user, or a fallback value." """Returns the duration entered by the user, or a fallback value."
Returns: Returns:
Decimal: The duration value provided by the user, or 20 float: The duration value provided by the user, or 20
""" """
return Decimal(self.duration_lineedit.text() or 20) return self.duration_lineedit.text() or 20
def check_input(self) -> None: def check_input(self) -> None:
"""Checks if the name and duration entered by the user is valid. If it is, the dialog is accepted. If not, the user is informed of the error.""" """Checks if the name and duration entered by the user is valid. If it is, the dialog is accepted. If not, the user is informed of the error."""