Merge branch 'development'

This commit is contained in:
jupfi 2025-06-19 17:54:36 +02:00
commit a821ab6265
6 changed files with 182 additions and 133 deletions

View file

@ -24,6 +24,7 @@ dependencies = [
"matplotlib", "matplotlib",
"pyqt6", "pyqt6",
"nqrduck-spectrometer", "nqrduck-spectrometer",
"quackseq",
] ]
[project.entry-points."nqrduck"] [project.entry-points."nqrduck"]
@ -40,9 +41,6 @@ extend-select = [
"D", # pydocstyle "D", # pydocstyle
] ]
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"]
[tool.ruff.lint.pydocstyle] [tool.ruff.lint.pydocstyle]
convention = "google" convention = "google"

View file

@ -1 +1 @@
"""Empty file to make this directory a package.""" """The visual pulse programming module for quackseq in the NQRduck project."""

View file

@ -6,7 +6,7 @@ import decimal
from PyQt6.QtCore import pyqtSlot from PyQt6.QtCore import pyqtSlot
from nqrduck.helpers.serializer import DecimalEncoder from nqrduck.helpers.serializer import DecimalEncoder
from nqrduck.module.module_controller import ModuleController from nqrduck.module.module_controller import ModuleController
from nqrduck_spectrometer.pulsesequence import PulseSequence from quackseq.pulsesequence import QuackSequence
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -17,14 +17,9 @@ class PulseProgrammerController(ModuleController):
This class is responsible for handling the logic of the pulse programmer module. This class is responsible for handling the logic of the pulse programmer module.
""" """
def on_loading(self, pulse_parameter_options: dict) -> None: def on_loading(self) -> None:
"""This method is called when the module is loaded. It sets the pulse parameter options in the model. """This method is called when the module is loaded. It sets the pulse parameter options in the model."""
Args:
pulse_parameter_options (dict): The pulse parameter options.
"""
logger.debug("Pulse programmer controller on loading") logger.debug("Pulse programmer controller on loading")
self.module.model.pulse_parameter_options = pulse_parameter_options
@pyqtSlot(str) @pyqtSlot(str)
def delete_event(self, event_name: str) -> None: def delete_event(self, event_name: str) -> None:
@ -34,10 +29,7 @@ class PulseProgrammerController(ModuleController):
event_name (str): The name of the event to be deleted. event_name (str): The name of the event to be deleted.
""" """
logger.debug("Deleting event %s", event_name) logger.debug("Deleting event %s", event_name)
for event in self.module.model.pulse_sequence.events: self.module.model.pulse_sequence.delete_event(event_name)
if event.name == event_name:
self.module.model.pulse_sequence.events.remove(event)
break
self.module.model.events_changed.emit() self.module.model.events_changed.emit()
@pyqtSlot(str, str) @pyqtSlot(str, str)
@ -150,8 +142,8 @@ class PulseProgrammerController(ModuleController):
sequence = json.loads(sequence) sequence = json.loads(sequence)
loaded_sequence = PulseSequence.load_sequence( loaded_sequence = QuackSequence.load_sequence(
sequence, self.module.model.pulse_parameter_options sequence
) )
self.module.model.pulse_sequence = loaded_sequence self.module.model.pulse_sequence = loaded_sequence

View file

@ -1,10 +1,10 @@
"""Model for the pulse programmer module.""" """Model for the pulse programmer module."""
import logging import logging
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
from nqrduck_spectrometer.pulsesequence import PulseSequence from quackseq.pulsesequence import QuackSequence
from quackseq.event import Event
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -25,7 +25,6 @@ class PulseProgrammerModel(ModuleModel):
FILE_EXTENSION = "quack" FILE_EXTENSION = "quack"
pulse_parameter_options_changed = pyqtSignal()
events_changed = pyqtSignal() events_changed = pyqtSignal()
pulse_sequence_changed = pyqtSignal() pulse_sequence_changed = pyqtSignal()
@ -36,8 +35,7 @@ class PulseProgrammerModel(ModuleModel):
module (Module): The module to which this model belongs. module (Module): The module to which this model belongs.
""" """
super().__init__(module) super().__init__(module)
self.pulse_parameter_options = OrderedDict() self.pulse_sequence = QuackSequence("Untitled pulse sequence")
self.pulse_sequence = PulseSequence("Untitled pulse sequence")
def add_event(self, event_name: str, duration: float = 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.
@ -46,41 +44,14 @@ class PulseProgrammerModel(ModuleModel):
event_name (str): A human-readable name for the event event_name (str): A human-readable name for the event
duration (float): 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( logger.debug(f"Adding event {event_name} with duration {duration}")
PulseSequence.Event(event_name, f"{float(duration):.16g}u")
)
logger.debug(
"Creating event %s with object id %s",
event_name,
id(self.pulse_sequence.events[-1]),
)
# Create a default instance of the pulse parameter options and add it to the event event = Event(event_name, f"{duration}u", self.pulse_sequence)
for name, pulse_parameter_class in self.pulse_parameter_options.items(): self.pulse_sequence.add_event(event)
logger.debug("Adding pulse parameter %s to event %s", name, event_name)
self.pulse_sequence.events[-1].parameters[name] = pulse_parameter_class(
name
)
logger.debug(
"Created pulse parameter %s with object id %s",
name,
id(self.pulse_sequence.events[-1].parameters[name]),
)
logger.debug(self.pulse_sequence.to_json()) logger.debug(self.pulse_sequence.to_json())
self.events_changed.emit() self.events_changed.emit()
@property
def pulse_parameter_options(self):
"""dict: The pulse parameter options."""
return self._pulse_parameter_options
@pulse_parameter_options.setter
def pulse_parameter_options(self, value):
self._pulse_parameter_options = value
logger.debug("Pulse parameter options changed - emitting signal")
self.pulse_parameter_options_changed.emit()
@property @property
def pulse_sequence(self): def pulse_sequence(self):
"""PulseSequence: The pulse sequence.""" """PulseSequence: The pulse sequence."""

View file

@ -21,18 +21,23 @@ from PyQt6.QtCore import pyqtSlot, pyqtSignal
from nqrduck.module.module_view import ModuleView from nqrduck.module.module_view import ModuleView
from nqrduck.assets.icons import Logos from nqrduck.assets.icons import Logos
from nqrduck.helpers.duckwidgets import DuckFloatEdit, DuckEdit from nqrduck.helpers.duckwidgets import DuckFloatEdit, DuckEdit
from nqrduck_spectrometer.pulseparameters import (
from quackseq.pulseparameters import (
BooleanOption, BooleanOption,
NumericOption, NumericOption,
FunctionOption, FunctionOption,
TableOption,
) )
from nqrduck.helpers.formbuilder import ( from nqrduck.helpers.formbuilder import (
DuckFormBuilder, DuckFormBuilder,
DuckFormFunctionSelectionField, DuckFormFunctionSelectionField,
DuckFormCheckboxField, DuckFormCheckboxField,
DuckFormFloatField, DuckFormFloatField,
DuckTableField,
) )
from .visual_parameter import VisualParameter
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -51,13 +56,6 @@ class PulseProgrammerView(ModuleView):
self.setup_variabletables() self.setup_variabletables()
logger.debug(
"Connecting pulse parameter options changed signal to on_pulse_parameter_options_changed"
)
self.module.model.pulse_parameter_options_changed.connect(
self.on_pulse_parameter_options_changed
)
def setup_variabletables(self) -> None: def setup_variabletables(self) -> None:
"""Setup the table for the variables.""" """Setup the table for the variables."""
pass pass
@ -138,20 +136,6 @@ class PulseProgrammerView(ModuleView):
) )
self.title.setText(f"Pulse Sequence: {self.module.model.pulse_sequence.name}") self.title.setText(f"Pulse Sequence: {self.module.model.pulse_sequence.name}")
@pyqtSlot()
def on_pulse_parameter_options_changed(self) -> None:
"""This method is called whenever the pulse parameter options change. It updates the view to reflect the changes."""
logger.debug(
"Updating pulse parameter options to %s",
self.module.model.pulse_parameter_options.keys(),
)
# We set it to the length of the pulse parameter options + 1 because we want to add a row for the parameter option buttons
self.pulse_table.setRowCount(len(self.module.model.pulse_parameter_options) + 1)
# Move the vertical header labels on row down
pulse_options = [""]
pulse_options.extend(list(self.module.model.pulse_parameter_options.keys()))
self.pulse_table.setVerticalHeaderLabels(pulse_options)
@pyqtSlot() @pyqtSlot()
def on_new_event_button_clicked(self) -> None: def on_new_event_button_clicked(self) -> None:
"""This method is called whenever the new event button is clicked. It creates a new event and adds it to the pulse sequence.""" """This method is called whenever the new event button is clicked. It creates a new event and adds it to the pulse sequence."""
@ -163,13 +147,29 @@ class PulseProgrammerView(ModuleView):
event_name = dialog.get_name() event_name = dialog.get_name()
duration = dialog.get_duration() duration = dialog.get_duration()
logger.debug( logger.debug(
"Adding new event with name %s, duration %g", event_name, duration "Adding new event with name %s, duration %s", event_name, duration
) )
self.module.model.add_event(event_name, duration) self.module.model.add_event(event_name, duration)
@pyqtSlot() @pyqtSlot()
def on_events_changed(self) -> None: def on_events_changed(self) -> None:
"""This method is called whenever the events in the pulse sequence change. It updates the view to reflect the changes.""" """This method is called whenever the events in the pulse sequence change. It updates the view to reflect the changes."""
pulse_parameter_options = (
self.module.model.pulse_sequence.pulse_parameter_options
)
logger.debug(
"Updating pulse parameter options to %s",
pulse_parameter_options.keys(),
)
# We set it to the length of the pulse parameter options + 1 because we want to add a row for the parameter option buttons
self.pulse_table.setRowCount(len(pulse_parameter_options) + 1)
# Move the vertical header labels on row down
pulse_options = [""]
pulse_options.extend(list(pulse_parameter_options.keys()))
self.pulse_table.setVerticalHeaderLabels(pulse_options)
logger.debug("Updating events to %s", self.module.model.pulse_sequence.events) logger.debug("Updating events to %s", self.module.model.pulse_sequence.events)
# Add label for the event lengths # Add label for the event lengths
@ -198,10 +198,12 @@ class PulseProgrammerView(ModuleView):
def set_parameter_icons(self) -> None: def set_parameter_icons(self) -> None:
"""This method sets the icons for the pulse parameter options.""" """This method sets the icons for the pulse parameter options."""
pulse_parrameter_options = (
self.module.model.pulse_sequence.pulse_parameter_options
)
for column_idx, event in enumerate(self.module.model.pulse_sequence.events): for column_idx, event in enumerate(self.module.model.pulse_sequence.events):
for row_idx, parameter in enumerate( for row_idx, parameter in enumerate(pulse_parrameter_options.keys()):
self.module.model.pulse_parameter_options.keys()
):
if row_idx == 0: if row_idx == 0:
event_options_widget = EventOptionsWidget(event) event_options_widget = EventOptionsWidget(event)
# Connect the delete_event signal to the on_delete_event slot # Connect the delete_event signal to the on_delete_event slot
@ -238,7 +240,8 @@ class PulseProgrammerView(ModuleView):
) )
logger.debug("Parameter object id: %s", id(event.parameters[parameter])) logger.debug("Parameter object id: %s", id(event.parameters[parameter]))
button = QPushButton() button = QPushButton()
icon = event.parameters[parameter].get_pixmap()
icon = VisualParameter(event.parameters[parameter]).get_pixmap()
logger.debug("Icon size: %s", icon.availableSizes()) logger.debug("Icon size: %s", icon.availableSizes())
button.setIcon(icon) button.setIcon(icon)
button.setIconSize(icon.availableSizes()[0]) button.setIconSize(icon.availableSizes()[0])
@ -270,6 +273,9 @@ class PulseProgrammerView(ModuleView):
parameter (str): The name of the parameter for which the options should be set. parameter (str): The name of the parameter for which the options should be set.
""" """
logger.debug("Button for event %s and parameter %s clicked", event, parameter) logger.debug("Button for event %s and parameter %s clicked", event, parameter)
# We assume the pulse sequence was updated
self.module.model.pulse_sequence.update_options()
# Create a QDialog to set the options for the parameter. # Create a QDialog to set the options for the parameter.
description = f"Set options for {parameter}" description = f"Set options for {parameter}"
dialog = DuckFormBuilder(parameter, description=description, parent=self) dialog = DuckFormBuilder(parameter, description=description, parent=self)
@ -277,12 +283,61 @@ class PulseProgrammerView(ModuleView):
# Adding fields for the options # Adding fields for the options
form_options = [] form_options = []
for option in event.parameters[parameter].options: for option in event.parameters[parameter].options:
logger.debug(f"Option value is {option.value}")
if isinstance(option, TableOption):
# Every option is it's own column. Every column has a dedicated number of rows.
# Get the option name:
name = option.name
table = DuckTableField(name, tooltip=None)
columns = option.columns
for column in columns:
# Every table option has a number of rows
fields = []
for row in column.options:
fields.append(self.get_field_for_option(row, event))
name = column.name
logger.debug(f"Adding column {name} with fields {fields}")
table.add_column(option=column, fields=fields)
form_options.append(table)
dialog.add_field(table)
else:
field = self.get_field_for_option(option, event)
form_options.append(field)
dialog.add_field(field)
result = dialog.exec()
options = event.parameters[parameter].options
if result:
values = dialog.get_values()
for i, value in enumerate(values):
logger.debug(f"Setting value {value} for option {options[i]}")
options[i].set_value(value)
self.set_parameter_icons()
def get_field_for_option(self, option, event):
"""Returns the field for the given option.
Args:
option (Option): The option for which the field should be created.
event (PulseSequence.Event): The event for which the option should be created.
Returns:
DuckFormField: The field for the option
"""
logger.debug(f"Creating field with value {option.value}")
if isinstance(option, BooleanOption): if isinstance(option, BooleanOption):
boolean_form = DuckFormCheckboxField( field = DuckFormCheckboxField(
option.name, tooltip=None, default=option.value option.name, tooltip=None, default=option.value
) )
dialog.add_field(boolean_form)
form_options.append(option)
elif isinstance(option, NumericOption): elif isinstance(option, NumericOption):
# We only show the slider if both min and max values are set # 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: if option.min_value is not None and option.max_value is not None:
@ -290,7 +345,10 @@ class PulseProgrammerView(ModuleView):
else: else:
slider = False slider = False
numeric_field = DuckFormFloatField( if slider:
slider = option.slider
field = DuckFormFloatField(
option.name, option.name,
tooltip=None, tooltip=None,
default=option.value, default=option.value,
@ -299,8 +357,6 @@ class PulseProgrammerView(ModuleView):
slider=slider, slider=slider,
) )
dialog.add_field(numeric_field)
form_options.append(option)
elif isinstance(option, FunctionOption): elif isinstance(option, FunctionOption):
logger.debug(f"Functions: {option.functions}") logger.debug(f"Functions: {option.functions}")
@ -312,26 +368,16 @@ class PulseProgrammerView(ModuleView):
index = option.functions.index(default_function) index = option.functions.index(default_function)
function_field = DuckFormFunctionSelectionField( field = DuckFormFunctionSelectionField(
option.name, option.name,
tooltip=None, tooltip=None,
functions=option.functions, functions=option.functions,
duration=event.duration, duration=event.duration,
default_function=index, default_function=index,
) )
dialog.add_field(function_field)
form_options.append(option)
result = dialog.exec() logger.debug(f"Returning Field: {field}")
return field
options = event.parameters[parameter].options
if result:
values = dialog.get_values()
for i, value in enumerate(values):
options[i].value = value
self.set_parameter_icons()
@pyqtSlot() @pyqtSlot()
def on_save_button_clicked(self) -> None: def on_save_button_clicked(self) -> None:
@ -446,7 +492,7 @@ class EventOptionsWidget(QWidget):
dialog = QDialog(self) dialog = QDialog(self)
dialog.setWindowTitle("Edit event") dialog.setWindowTitle("Edit event")
layout = QVBoxLayout() layout = QVBoxLayout()
label = QLabel(f"Edit event {self.event.name}") label = QLabel(f"Edit event: {self.event.name}")
layout.addWidget(label) layout.addWidget(label)
# Create the inputs for event name, duration # Create the inputs for event name, duration
@ -454,15 +500,13 @@ class EventOptionsWidget(QWidget):
name_label = QLabel("Name:") name_label = QLabel("Name:")
name_lineedit = QLineEdit(self.event.name) name_lineedit = QLineEdit(self.event.name)
event_form_layout.addRow(name_label, name_lineedit) event_form_layout.addRow(name_label, name_lineedit)
duration_layout = QHBoxLayout()
duration_label = QLabel("Duration:") duration_label = QLabel("Duration (µs):")
duration_lineedit = QLineEdit() duration_lineedit = QLineEdit()
unit_label = QLabel("µs")
duration_lineedit.setText("%.16g" % (self.event.duration * 1e6)) duration_lineedit.setText("%.16g" % (self.event.duration * 1e6))
duration_layout.addWidget(duration_label)
duration_layout.addWidget(duration_lineedit) event_form_layout.addRow(duration_label, duration_lineedit)
duration_layout.addWidget(unit_label)
event_form_layout.addRow(duration_layout)
layout.addLayout(event_form_layout) layout.addLayout(event_form_layout)
buttons = QDialogButtonBox( buttons = QDialogButtonBox(
@ -537,22 +581,21 @@ class AddEventDialog(QDialog):
self.name_input.validator = self.NameInputValidator(self) self.name_input.validator = self.NameInputValidator(self)
self.name_layout.addWidget(self.label) self.name_layout.addWidget(self.label)
self.name_layout.addStretch(1)
self.name_layout.addWidget(self.name_input) self.name_layout.addWidget(self.name_input)
self.layout.addRow(self.name_layout) self.layout.addRow(self.name_layout)
self.duration_layout = QHBoxLayout() self.duration_layout = QHBoxLayout()
self.duration_label = QLabel("Duration:") self.duration_label = QLabel("Duration (µs):")
self.duration_lineedit = DuckFloatEdit(min_value=0) self.duration_lineedit = DuckFloatEdit(min_value=0)
self.duration_lineedit.setText("20") self.duration_lineedit.setText("20")
self.unit_label = QLabel("µs")
self.duration_layout.addWidget(self.duration_label) self.duration_layout.addWidget(self.duration_label)
self.duration_layout.addStretch(1)
self.duration_layout.addWidget(self.duration_lineedit) self.duration_layout.addWidget(self.duration_lineedit)
self.duration_layout.addWidget(self.unit_label)
self.layout.addRow(self.duration_layout) self.layout.addRow(self.duration_layout)
self.buttons = QDialogButtonBox( self.buttons = QDialogButtonBox(

View file

@ -0,0 +1,45 @@
from nqrduck.assets.icons import PulseParameters
from quackseq.pulseparameters import TXPulse, RXReadout, PulseParameter
from quackseq.functions import RectFunction, SincFunction, GaussianFunction
class VisualParameter():
def __init__(self, pulse_parameter : PulseParameter):
self.pulse_parameter = pulse_parameter
def get_pixmap(self):
"""Returns the pixmap of the TX Pulse Parameter.
Returns:
QPixmap: The pixmap of the TX Pulse Parameter depending on the relative amplitude.
"""
# Check the instance of the pulse parameter
if isinstance(self.pulse_parameter, TXPulse):
amplitude = self.pulse_parameter.get_option_by_name(TXPulse.RELATIVE_AMPLITUDE).value
if amplitude > 0:
# Get the shape
shape = self.pulse_parameter.get_option_by_name(TXPulse.TX_PULSE_SHAPE).value
if isinstance(shape, RectFunction):
pixmap = PulseParameters.TXRect()
return pixmap
elif isinstance(shape, SincFunction):
pixmap = PulseParameters.TXSinc()
return pixmap
elif isinstance(shape, GaussianFunction):
pixmap = PulseParameters.TXGauss()
return pixmap
else:
pixmap = PulseParameters.TXCustom()
return pixmap
else:
pixmap = PulseParameters.TXOff()
return pixmap
elif isinstance(self.pulse_parameter, RXReadout):
rx = self.pulse_parameter.get_option_by_name(RXReadout.RX).value
if rx:
pixmap = PulseParameters.RXOn()
return pixmap
else:
pixmap = PulseParameters.RXOff()
return pixmap