diff --git a/pyproject.toml b/pyproject.toml index fe56273..b960745 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ dependencies = [ "matplotlib", "pyqt6", "nqrduck-spectrometer", + "quackseq", ] [project.entry-points."nqrduck"] @@ -40,9 +41,6 @@ extend-select = [ "D", # pydocstyle ] -[tool.ruff.lint.per-file-ignores] -"__init__.py" = ["F401"] - [tool.ruff.lint.pydocstyle] convention = "google" diff --git a/src/nqrduck_pulseprogrammer/__init__.py b/src/nqrduck_pulseprogrammer/__init__.py index 40b8254..229bf24 100644 --- a/src/nqrduck_pulseprogrammer/__init__.py +++ b/src/nqrduck_pulseprogrammer/__init__.py @@ -1 +1 @@ -"""Empty file to make this directory a package.""" \ No newline at end of file +"""The visual pulse programming module for quackseq in the NQRduck project.""" \ No newline at end of file diff --git a/src/nqrduck_pulseprogrammer/controller.py b/src/nqrduck_pulseprogrammer/controller.py index 974e522..b847c2c 100644 --- a/src/nqrduck_pulseprogrammer/controller.py +++ b/src/nqrduck_pulseprogrammer/controller.py @@ -6,7 +6,7 @@ import decimal from PyQt6.QtCore import pyqtSlot from nqrduck.helpers.serializer import DecimalEncoder from nqrduck.module.module_controller import ModuleController -from nqrduck_spectrometer.pulsesequence import PulseSequence +from quackseq.pulsesequence import QuackSequence logger = logging.getLogger(__name__) @@ -17,14 +17,9 @@ class PulseProgrammerController(ModuleController): This class is responsible for handling the logic of the pulse programmer module. """ - def on_loading(self, pulse_parameter_options: dict) -> None: - """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. - """ + def on_loading(self) -> None: + """This method is called when the module is loaded. It sets the pulse parameter options in the model.""" logger.debug("Pulse programmer controller on loading") - self.module.model.pulse_parameter_options = pulse_parameter_options @pyqtSlot(str) 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. """ logger.debug("Deleting event %s", event_name) - for event in self.module.model.pulse_sequence.events: - if event.name == event_name: - self.module.model.pulse_sequence.events.remove(event) - break + self.module.model.pulse_sequence.delete_event(event_name) self.module.model.events_changed.emit() @pyqtSlot(str, str) @@ -150,8 +142,8 @@ class PulseProgrammerController(ModuleController): sequence = json.loads(sequence) - loaded_sequence = PulseSequence.load_sequence( - sequence, self.module.model.pulse_parameter_options + loaded_sequence = QuackSequence.load_sequence( + sequence ) self.module.model.pulse_sequence = loaded_sequence diff --git a/src/nqrduck_pulseprogrammer/model.py b/src/nqrduck_pulseprogrammer/model.py index bca231e..c74beea 100644 --- a/src/nqrduck_pulseprogrammer/model.py +++ b/src/nqrduck_pulseprogrammer/model.py @@ -1,10 +1,10 @@ """Model for the pulse programmer module.""" import logging -from collections import OrderedDict from PyQt6.QtCore import pyqtSignal 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__) @@ -25,7 +25,6 @@ class PulseProgrammerModel(ModuleModel): FILE_EXTENSION = "quack" - pulse_parameter_options_changed = pyqtSignal() events_changed = pyqtSignal() pulse_sequence_changed = pyqtSignal() @@ -36,8 +35,7 @@ class PulseProgrammerModel(ModuleModel): module (Module): The module to which this model belongs. """ super().__init__(module) - self.pulse_parameter_options = OrderedDict() - self.pulse_sequence = PulseSequence("Untitled pulse sequence") + self.pulse_sequence = QuackSequence("Untitled pulse sequence") def add_event(self, event_name: str, duration: float = 20): """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 duration (float): The duration of the event in µs. Defaults to 20. """ - self.pulse_sequence.events.append( - 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 - for name, pulse_parameter_class in self.pulse_parameter_options.items(): - 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(f"Adding event {event_name} with duration {duration}") + + event = Event(event_name, f"{duration}u", self.pulse_sequence) + self.pulse_sequence.add_event(event) logger.debug(self.pulse_sequence.to_json()) 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 def pulse_sequence(self): """PulseSequence: The pulse sequence.""" diff --git a/src/nqrduck_pulseprogrammer/view.py b/src/nqrduck_pulseprogrammer/view.py index 479eba0..a96cfa9 100644 --- a/src/nqrduck_pulseprogrammer/view.py +++ b/src/nqrduck_pulseprogrammer/view.py @@ -21,18 +21,23 @@ from PyQt6.QtCore import pyqtSlot, pyqtSignal from nqrduck.module.module_view import ModuleView from nqrduck.assets.icons import Logos from nqrduck.helpers.duckwidgets import DuckFloatEdit, DuckEdit -from nqrduck_spectrometer.pulseparameters import ( + +from quackseq.pulseparameters import ( BooleanOption, NumericOption, FunctionOption, + TableOption, ) from nqrduck.helpers.formbuilder import ( DuckFormBuilder, DuckFormFunctionSelectionField, DuckFormCheckboxField, DuckFormFloatField, + DuckTableField, ) +from .visual_parameter import VisualParameter + logger = logging.getLogger(__name__) @@ -51,13 +56,6 @@ class PulseProgrammerView(ModuleView): 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: """Setup the table for the variables.""" pass @@ -138,20 +136,6 @@ class PulseProgrammerView(ModuleView): ) 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() 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.""" @@ -163,13 +147,29 @@ class PulseProgrammerView(ModuleView): event_name = dialog.get_name() duration = dialog.get_duration() 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) @pyqtSlot() 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.""" + + 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) # Add label for the event lengths @@ -198,10 +198,12 @@ class PulseProgrammerView(ModuleView): def set_parameter_icons(self) -> None: """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 row_idx, parameter in enumerate( - self.module.model.pulse_parameter_options.keys() - ): + for row_idx, parameter in enumerate(pulse_parrameter_options.keys()): if row_idx == 0: event_options_widget = EventOptionsWidget(event) # 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])) button = QPushButton() - icon = event.parameters[parameter].get_pixmap() + + icon = VisualParameter(event.parameters[parameter]).get_pixmap() logger.debug("Icon size: %s", icon.availableSizes()) button.setIcon(icon) 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. """ 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. description = f"Set options for {parameter}" dialog = DuckFormBuilder(parameter, description=description, parent=self) @@ -277,50 +283,33 @@ class PulseProgrammerView(ModuleView): # 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 + 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) - numeric_field = DuckFormFloatField( - option.name, - tooltip=None, - default=option.value, - min_value=option.min_value, - max_value=option.max_value, - slider=slider, - ) + columns = option.columns - dialog.add_field(numeric_field) - form_options.append(option) - elif isinstance(option, FunctionOption): - logger.debug(f"Functions: {option.functions}") + 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)) - # 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 + name = column.name - index = option.functions.index(default_function) + logger.debug(f"Adding column {name} with fields {fields}") + table.add_column(option=column, fields=fields) - 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) + 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() @@ -329,10 +318,67 @@ class PulseProgrammerView(ModuleView): if result: values = dialog.get_values() for i, value in enumerate(values): - options[i].value = value + 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): + field = DuckFormCheckboxField( + option.name, tooltip=None, default=option.value + ) + 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 + + if slider: + slider = option.slider + + field = DuckFormFloatField( + option.name, + tooltip=None, + default=option.value, + min_value=option.min_value, + max_value=option.max_value, + slider=slider, + ) + + 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) + + field = DuckFormFunctionSelectionField( + option.name, + tooltip=None, + functions=option.functions, + duration=event.duration, + default_function=index, + ) + + logger.debug(f"Returning Field: {field}") + return field + @pyqtSlot() def on_save_button_clicked(self) -> None: """This method is called whenever the save button is clicked. It opens a dialog to select a file to save the pulse sequence to.""" @@ -446,7 +492,7 @@ class EventOptionsWidget(QWidget): dialog = QDialog(self) dialog.setWindowTitle("Edit event") layout = QVBoxLayout() - label = QLabel(f"Edit event {self.event.name}") + label = QLabel(f"Edit event: {self.event.name}") layout.addWidget(label) # Create the inputs for event name, duration @@ -454,15 +500,13 @@ class EventOptionsWidget(QWidget): name_label = QLabel("Name:") name_lineedit = QLineEdit(self.event.name) event_form_layout.addRow(name_label, name_lineedit) - duration_layout = QHBoxLayout() - duration_label = QLabel("Duration:") + + duration_label = QLabel("Duration (µs):") duration_lineedit = QLineEdit() - unit_label = QLabel("µs") + duration_lineedit.setText("%.16g" % (self.event.duration * 1e6)) - duration_layout.addWidget(duration_label) - duration_layout.addWidget(duration_lineedit) - duration_layout.addWidget(unit_label) - event_form_layout.addRow(duration_layout) + + event_form_layout.addRow(duration_label, duration_lineedit) layout.addLayout(event_form_layout) buttons = QDialogButtonBox( @@ -537,22 +581,21 @@ class AddEventDialog(QDialog): self.name_input.validator = self.NameInputValidator(self) self.name_layout.addWidget(self.label) + self.name_layout.addStretch(1) self.name_layout.addWidget(self.name_input) self.layout.addRow(self.name_layout) 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.setText("20") - self.unit_label = QLabel("µs") self.duration_layout.addWidget(self.duration_label) + self.duration_layout.addStretch(1) self.duration_layout.addWidget(self.duration_lineedit) - self.duration_layout.addWidget(self.unit_label) - self.layout.addRow(self.duration_layout) self.buttons = QDialogButtonBox( diff --git a/src/nqrduck_pulseprogrammer/visual_parameter.py b/src/nqrduck_pulseprogrammer/visual_parameter.py new file mode 100644 index 0000000..7601e2b --- /dev/null +++ b/src/nqrduck_pulseprogrammer/visual_parameter.py @@ -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