From fc99ded526d2942dd9c2281c810e06d062d1dc80 Mon Sep 17 00:00:00 2001 From: jupfi Date: Sat, 16 Nov 2024 13:01:00 +0100 Subject: [PATCH] Phase cycling. --- src/quackseq/options.py | 122 +++++++++++++++++++++++++----- src/quackseq/phase_table.py | 65 ++-------------- src/quackseq/pulseparameters.py | 35 +++++++++ src/quackseq/pulsesequence.py | 46 +++++++---- src/quackseq/sequences/COMPFID.py | 2 +- src/quackseq/sequences/SEPC.py | 2 +- 6 files changed, 176 insertions(+), 96 deletions(-) diff --git a/src/quackseq/options.py b/src/quackseq/options.py index 3cea427..8d04175 100644 --- a/src/quackseq/options.py +++ b/src/quackseq/options.py @@ -1,6 +1,7 @@ """Options for the pulse parameters. Options can be of different types, for example boolean, numeric or function. Generally pulse parameters have different values for the different events in a pulse sequence.""" import logging +from collections import OrderedDict from quackseq.functions import Function logger = logging.getLogger(__name__) @@ -82,7 +83,6 @@ class BooleanOption(Option): def set_value(self, value): """Sets the value of the option.""" self.value = value - self.value_changed.emit() class NumericOption(Option): @@ -105,7 +105,7 @@ class NumericOption(Option): is_float (bool): If the value is a float. min_value: The minimum value of the option. max_value: The maximum value of the option. - slider (bool): If the option should be displayed as a slider. This is not used for the pulseq module, but visualizations can use this information. + slider (bool): If the option should be displayed as a slider. This is not used for the quackseq module, but visualizations can use this information. """ super().__init__(name, value) self.is_float = is_float @@ -115,12 +115,12 @@ class NumericOption(Option): def set_value(self, value): """Sets the value of the option.""" - if value < self.min_value: - self.value = self.min_value - self.value_changed.emit() - elif value >= self.max_value: - self.value = self.max_value - self.value_changed.emit() + if self.min_value is None or self.max_value is None: + self.value = value + return + + if self.min_value <= value <= self.max_value: + self.value = value else: raise ValueError( f"Value {value} is not in the range of {self.min_value} to {self.max_value}. This should have been caught earlier." @@ -237,31 +237,50 @@ class FunctionOption(Option): class TableOption(Option): """A table option has rows and columns and can be used to store a table of values. - The table option acts as a 'meta' option, which means that we can add different types of options to the table as rows. + The table option acts as a 'meta' option, which means that we can add different types of options to the table as columns. Associated with every row we can add a number of different values. The number of rows can be adjusted at runtime. """ - def __init__(self, name: str, value = None) -> None: + def __init__(self, name: str, value=None) -> None: """Initializes the table option.""" super().__init__(name, value) - self.options = [] + self.columns = [] self.n_rows = 0 - def set_value(self, value): - """Sets the value of the option.""" - self.value = value + def add_column(self, column_name: str, option: Option, default_value) -> None: + """Adds an option to the table as column. - def add_option(self, option: Option) -> None: - """Adds an option to the table. + Options are added as columns. Args: - option (Option): The option to add. + option (Option): The class of the option to add. + column_name (str): The name of the column. + default_value: The default value of the column. """ - self.options.append(option) + column = self.Column(column_name, option, default_value, self.n_rows) + # Add the column to the table + self.columns.append(column) - def set_n_rows(self, n_rows : int) -> None: + def set_value(self, values: list) -> None: + """Sets the value of the option. + + Args: + values: The values of the different options in the table. + """ + for i, column in enumerate(values): + self.columns[i].set_row_values(column) + + def get_value(self) -> list: + """Gets the value of the option. + + Returns: + list: The values of the different options in the table. + """ + return [column.get_values() for column in self.columns] + + def set_n_rows(self, n_rows: int) -> None: """Sets the number of rows in the table. Args: @@ -269,6 +288,24 @@ class TableOption(Option): """ self.n_rows = n_rows + # Now we need to set the number of rows for all the options in the table, the last value is repeated if the number of rows is increased + for column in self.columns: + column.update_n_rows(n_rows) + + def get_option_by_name(self, name: str) -> Option: + """Gets an option by its name. + + Args: + name (str): The name of the option. + + Returns: + Option: The option with the given name. + """ + for option in self.options: + if option.name == name: + return option + raise ValueError(f"Option with name {name} not found") + def to_json(self): """Returns a json representation of the option. @@ -294,4 +331,51 @@ class TableOption(Option): obj = cls(data["name"], data["value"]) return obj + class Column: + """Defines a column option for a table option. + Args: + name (str): The name of the option. + type (type): The type of the option. + default_value: The default value of the option. + + """ + + def __init__(self, name: str, type, default_value, n_rows: int) -> None: + """Initializes the column option.""" + self.name = name + self.type = type + self.default_value = default_value + + self.options = [] + + def update_n_rows(self, n_rows: int) -> None: + """Updates the number of rows in the column. + + Args: + n_rows (int): The number of rows. + """ + if len(self.options) < n_rows: + self.options.extend( + self.type(self.name, self.default_value) + for i in range(n_rows - len(self.options)) + ) + elif len(self.options) > n_rows: + self.options = self.options[:n_rows] + + def set_row_values(self, values: list) -> None: + """Sets the values of the options in the column. + + Args: + values: The values of the options in the column. + """ + for i, value in enumerate(values): + self.options[i].set_value(value) + + def get_values(self) -> list: + """Gets the values of the options in the column. + + Returns: + list: The values of the options in the column. + """ + return [option.value for option in self.options] diff --git a/src/quackseq/phase_table.py b/src/quackseq/phase_table.py index ef54819..fa97032 100644 --- a/src/quackseq/phase_table.py +++ b/src/quackseq/phase_table.py @@ -15,7 +15,8 @@ class PhaseTable: def __init__(self, quackseq): """Initializes the phase table.""" self.quackseq = quackseq - self.readout_scheme = ReadoutScheme(self) + # Set phase array to default value + self.phase_array = np.array([]) self.generate_phase_array() def generate_phase_array(self): @@ -186,9 +187,6 @@ class PhaseTable: # First set the phase array self.phase_array = phase_array - # Then update the readout scheme (always reset it) - self.readout_scheme.update_readout_scheme() - @property def phase_array(self) -> np.array: """The phase array of the sequence.""" @@ -201,64 +199,11 @@ class PhaseTable: @property def n_phase_cycles(self) -> int: """The number of phase cycles in the sequence.""" + # Calculate the number of phase cycles + self.generate_phase_array() return self.phase_array.shape[0] @property def n_parameters(self) -> int: """The number of TX pulse parameters in the sequence.""" - return self.phase_array.shape[1] - - -class ReadoutScheme: - """Readout Scheme for the phase table. - - The rows are the phase cycles of the sequence. - - The columns have two different types of options: - - The phase value of the phase cycle. - - The function that is applied to the phase cycle. Usually this is just +1, -1 or 0. - - """ - - def __init__(self, phase_table: PhaseTable) -> None: - """Initializes the ReadoutOption.""" - self.phase_table = phase_table - - def update_readout_scheme(self): - """Update the readout scheme of the sequence. Whenever the phase array is updated it will be reset.""" - - self.readout_scheme = np.zeros( - (self.phase_table.n_phase_cycles, self.phase_table.n_parameters) - ) - - def set_phase_cycle( - self, phase_cycle: int, phase_shift: float, function: str - ) -> None: - """Sets the phase shift and function of a phase cycle. - - Args: - phase_cycle (int): The phase cycle. - phase_shift (float): The phase shift. - function (str): The function. - """ - self.readout_scheme[phase_cycle] = [phase_shift, function] - - @property - def readout_scheme(self) -> np.array: - """The readout scheme of the sequence.""" - return self._readout_scheme - - @readout_scheme.setter - def readout_scheme(self, readout_scheme: list) -> None: - """Sets the readout scheme of the sequence. - - Args: - readout_scheme (list): The readout scheme. - """ - # Sanity check - if len(readout_scheme) != self.phase_table.n_phase_cycles: - raise ValueError( - f"Length of readout scheme ({len(readout_scheme)}) does not match the number of phase cycles ({self.phase_table.n_phase_cycles})" - ) - - self._readout_scheme = readout_scheme + return self.phase_array.shape[1] \ No newline at end of file diff --git a/src/quackseq/pulseparameters.py b/src/quackseq/pulseparameters.py index 1ba5d4b..0d4f5ee 100644 --- a/src/quackseq/pulseparameters.py +++ b/src/quackseq/pulseparameters.py @@ -6,11 +6,13 @@ Todo: from __future__ import annotations import logging +from typing import override import numpy as np from numpy.core.multiarray import array as array from quackseq.options import ( + TableOption, BooleanOption, FunctionOption, NumericOption, @@ -81,6 +83,16 @@ class PulseParameter: return option raise ValueError(f"Option with name {name} not found") + def update_option(self, sequence: "QuackSequence") -> None: + """Generic update option method for pulse parameters. + + This can be implemented by subclasses to update the options of the pulse parameter whenever the parameter is called (e.g. in the GUI). + + Args: + sequence (QuackSequence): The sequence to update the options from. + """ + pass + class TXPulse(PulseParameter): """Basic TX Pulse Parameter. It includes options for the relative amplitude, the phase and the pulse shape. @@ -168,6 +180,8 @@ class RXReadout(PulseParameter): """ RX = "Enable RX Readout" + READOUT_SCHEME = "Readout Scheme" + PHASE = "Phase" def __init__(self, name) -> None: """Initializes the RX Readout PulseParameter. @@ -177,6 +191,27 @@ class RXReadout(PulseParameter): super().__init__(name) self.add_option(BooleanOption(self.RX, False)) + # Readout Scheme: + readout_option = TableOption(self.READOUT_SCHEME) + + # Add Phase Option to Readout Scheme + phase_option = NumericOption + + readout_option.add_column(self.PHASE, phase_option, 0) + + # Set number of rows to default value + readout_option.set_n_rows(1) + + self.add_option(readout_option) + + @override + def update_option(self, sequence: "QuackSequence") -> None: + """Adjusts the number of rows in the table option based on the number of phase cycles in the sequence.""" + n_phase_cycles = sequence.get_n_phase_cycles() + readout_option = self.get_option_by_name(self.READOUT_SCHEME) + readout_option.set_n_rows(n_phase_cycles) + logger.debug(f"Updated RX Readout option with {n_phase_cycles} rows") + class Gate(PulseParameter): """Basic PulseParameter for the Gate. It includes an option for the Gate state. diff --git a/src/quackseq/pulsesequence.py b/src/quackseq/pulsesequence.py index 1fdea69..88caa9a 100644 --- a/src/quackseq/pulsesequence.py +++ b/src/quackseq/pulsesequence.py @@ -242,6 +242,12 @@ class QuackSequence(PulseSequence): self.phase_table = PhaseTable(self) + def update_options(self) -> None: + """Updates the options of the pulse parameters.""" + for event in self.events: + for pulse_parameter in event.parameters.values(): + pulse_parameter.update_option(self) + def add_blank_event(self, event_name: str, duration: float) -> Event: """Adds a blank event to the pulse sequence. @@ -280,6 +286,8 @@ class QuackSequence(PulseSequence): self.set_tx_phase(event, phase) self.set_tx_shape(event, shape) + self.update_options() + return event def add_readout_event(self, event_name: str, duration: float) -> Event: @@ -360,6 +368,8 @@ class QuackSequence(PulseSequence): TXPulse.N_PHASE_CYCLES ).value = n_phase_cycles + self.update_options() + def set_tx_phase_cycle_group( self, event: Event | str, phase_cycle_group: int ) -> None: @@ -376,6 +386,8 @@ class QuackSequence(PulseSequence): TXPulse.PHASE_CYCLE_GROUP ).value = phase_cycle_group + self.update_options() + # RX Specific functions def set_rx(self, event: Event | str, rx: bool) -> None: @@ -390,30 +402,34 @@ class QuackSequence(PulseSequence): event.parameters[self.RX_READOUT].get_option_by_name(RXReadout.RX).value = rx - def set_rx_readout_scheme(self, event: Event | str, readout_scheme: list) -> None: - """Sets the readout scheme of the receiver. + def set_rx_phase(self, event: Event | str, phase: list) -> None: + """Sets the phase of the receiver. Args: - event (Event | str): The event to set the readout scheme for or the name of the event - readout_scheme (list): The readout scheme of the receiver + event (Event | str): The event to set the phase for or the name of the event + phase (list): The phase of the receiver """ if isinstance(event, str): event = self.get_event_by_name(event) - # Check that the readout scheme is valid - self.phase_table.generate_phase_array() - n_cycles = self.phase_table.n_phase_cycles + rx_table = event.parameters[self.RX_READOUT].get_option_by_name(RXReadout.READOUT_SCHEME) - rows = len(readout_scheme) + # Get the actual option + phase_option = rx_table.get_option_by_name(RXReadout.PHASE) - if rows != n_cycles: + # Check that the number of phases is the same as the number of phase cycles + if len(phase) != self.get_n_phase_cycles(): raise ValueError( - f"Readout scheme needs to have {n_cycles} cycles, but has {rows}" + f"Number of phases ({len(phase)}) needs to be the same as the number of phase cycles ({self.get_n_phase_cycles()})" ) - # Old way - implement the sequence wide readout scheme here - #event.parameters[self.RX_READOUT].get_option_by_name( - # RXReadout.READOUT_SCHEME - #).value = readout_scheme + # Set the values + phase_option.values = phase - self.phase_table.readout_scheme.readout_scheme = readout_scheme \ No newline at end of file + def get_n_phase_cycles(self) -> int: + """Returns the number of phase cycles of the pulse sequence. + + Returns: + int: The number of phase cycles + """ + return self.phase_table.n_phase_cycles \ No newline at end of file diff --git a/src/quackseq/sequences/COMPFID.py b/src/quackseq/sequences/COMPFID.py index 24e86ca..93d8642 100644 --- a/src/quackseq/sequences/COMPFID.py +++ b/src/quackseq/sequences/COMPFID.py @@ -26,6 +26,6 @@ def create_COMPFID(): # No phase shifiting of the receive data but weighting of -1 for the 45 degree pulse, +1 for the 135 degree pulse, -1 for the 225 degree pulse and +1 for the 315 degree pulse readout_scheme = [0, 180, 0, 180] - COMPFID.set_rx_readout_scheme("rx", readout_scheme) + COMPFID.set_rx_phase("rx", readout_scheme) return COMPFID diff --git a/src/quackseq/sequences/SEPC.py b/src/quackseq/sequences/SEPC.py index 04ea1d4..8bd1433 100644 --- a/src/quackseq/sequences/SEPC.py +++ b/src/quackseq/sequences/SEPC.py @@ -29,6 +29,6 @@ def create_SEPC() -> QuackSequence: # Readout scheme for phase cycling TX pulses have the scheme 0 90 180 270 readout_scheme = [0, 90, 180, 270] - sepc.set_rx_readout_scheme("rx", readout_scheme) + sepc.set_rx_phase("rx", readout_scheme) return sepc