Phase cycling.

This commit is contained in:
jupfi 2024-11-16 13:01:00 +01:00
parent bdb5dd7cd5
commit fc99ded526
6 changed files with 176 additions and 96 deletions

View file

@ -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.""" """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 import logging
from collections import OrderedDict
from quackseq.functions import Function from quackseq.functions import Function
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -82,7 +83,6 @@ class BooleanOption(Option):
def set_value(self, value): def set_value(self, value):
"""Sets the value of the option.""" """Sets the value of the option."""
self.value = value self.value = value
self.value_changed.emit()
class NumericOption(Option): class NumericOption(Option):
@ -105,7 +105,7 @@ class NumericOption(Option):
is_float (bool): If the value is a float. is_float (bool): If the value is a float.
min_value: The minimum value of the option. min_value: The minimum value of the option.
max_value: The maximum 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) super().__init__(name, value)
self.is_float = is_float self.is_float = is_float
@ -115,12 +115,12 @@ class NumericOption(Option):
def set_value(self, value): def set_value(self, value):
"""Sets the value of the option.""" """Sets the value of the option."""
if value < self.min_value: if self.min_value is None or self.max_value is None:
self.value = self.min_value self.value = value
self.value_changed.emit() return
elif value >= self.max_value:
self.value = self.max_value if self.min_value <= value <= self.max_value:
self.value_changed.emit() self.value = value
else: else:
raise ValueError( raise ValueError(
f"Value {value} is not in the range of {self.min_value} to {self.max_value}. This should have been caught earlier." 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): class TableOption(Option):
"""A table option has rows and columns and can be used to store a table of values. """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. Associated with every row we can add a number of different values.
The number of rows can be adjusted at runtime. 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.""" """Initializes the table option."""
super().__init__(name, value) super().__init__(name, value)
self.options = [] self.columns = []
self.n_rows = 0 self.n_rows = 0
def set_value(self, value): def add_column(self, column_name: str, option: Option, default_value) -> None:
"""Sets the value of the option.""" """Adds an option to the table as column.
self.value = value
def add_option(self, option: Option) -> None: Options are added as columns.
"""Adds an option to the table.
Args: 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. """Sets the number of rows in the table.
Args: Args:
@ -269,6 +288,24 @@ class TableOption(Option):
""" """
self.n_rows = n_rows 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): def to_json(self):
"""Returns a json representation of the option. """Returns a json representation of the option.
@ -294,4 +331,51 @@ class TableOption(Option):
obj = cls(data["name"], data["value"]) obj = cls(data["name"], data["value"])
return obj 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]

View file

@ -15,7 +15,8 @@ class PhaseTable:
def __init__(self, quackseq): def __init__(self, quackseq):
"""Initializes the phase table.""" """Initializes the phase table."""
self.quackseq = quackseq self.quackseq = quackseq
self.readout_scheme = ReadoutScheme(self) # Set phase array to default value
self.phase_array = np.array([])
self.generate_phase_array() self.generate_phase_array()
def generate_phase_array(self): def generate_phase_array(self):
@ -186,9 +187,6 @@ class PhaseTable:
# First set the phase array # First set the phase array
self.phase_array = phase_array self.phase_array = phase_array
# Then update the readout scheme (always reset it)
self.readout_scheme.update_readout_scheme()
@property @property
def phase_array(self) -> np.array: def phase_array(self) -> np.array:
"""The phase array of the sequence.""" """The phase array of the sequence."""
@ -201,64 +199,11 @@ class PhaseTable:
@property @property
def n_phase_cycles(self) -> int: def n_phase_cycles(self) -> int:
"""The number of phase cycles in the sequence.""" """The number of phase cycles in the sequence."""
# Calculate the number of phase cycles
self.generate_phase_array()
return self.phase_array.shape[0] return self.phase_array.shape[0]
@property @property
def n_parameters(self) -> int: def n_parameters(self) -> int:
"""The number of TX pulse parameters in the sequence.""" """The number of TX pulse parameters in the sequence."""
return self.phase_array.shape[1] 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

View file

@ -6,11 +6,13 @@ Todo:
from __future__ import annotations from __future__ import annotations
import logging import logging
from typing import override
import numpy as np import numpy as np
from numpy.core.multiarray import array as array from numpy.core.multiarray import array as array
from quackseq.options import ( from quackseq.options import (
TableOption,
BooleanOption, BooleanOption,
FunctionOption, FunctionOption,
NumericOption, NumericOption,
@ -81,6 +83,16 @@ class PulseParameter:
return option return option
raise ValueError(f"Option with name {name} not found") 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): class TXPulse(PulseParameter):
"""Basic TX Pulse Parameter. It includes options for the relative amplitude, the phase and the pulse shape. """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" RX = "Enable RX Readout"
READOUT_SCHEME = "Readout Scheme"
PHASE = "Phase"
def __init__(self, name) -> None: def __init__(self, name) -> None:
"""Initializes the RX Readout PulseParameter. """Initializes the RX Readout PulseParameter.
@ -177,6 +191,27 @@ class RXReadout(PulseParameter):
super().__init__(name) super().__init__(name)
self.add_option(BooleanOption(self.RX, False)) 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): class Gate(PulseParameter):
"""Basic PulseParameter for the Gate. It includes an option for the Gate state. """Basic PulseParameter for the Gate. It includes an option for the Gate state.

View file

@ -242,6 +242,12 @@ class QuackSequence(PulseSequence):
self.phase_table = PhaseTable(self) 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: def add_blank_event(self, event_name: str, duration: float) -> Event:
"""Adds a blank event to the pulse sequence. """Adds a blank event to the pulse sequence.
@ -280,6 +286,8 @@ class QuackSequence(PulseSequence):
self.set_tx_phase(event, phase) self.set_tx_phase(event, phase)
self.set_tx_shape(event, shape) self.set_tx_shape(event, shape)
self.update_options()
return event return event
def add_readout_event(self, event_name: str, duration: float) -> Event: def add_readout_event(self, event_name: str, duration: float) -> Event:
@ -360,6 +368,8 @@ class QuackSequence(PulseSequence):
TXPulse.N_PHASE_CYCLES TXPulse.N_PHASE_CYCLES
).value = n_phase_cycles ).value = n_phase_cycles
self.update_options()
def set_tx_phase_cycle_group( def set_tx_phase_cycle_group(
self, event: Event | str, phase_cycle_group: int self, event: Event | str, phase_cycle_group: int
) -> None: ) -> None:
@ -376,6 +386,8 @@ class QuackSequence(PulseSequence):
TXPulse.PHASE_CYCLE_GROUP TXPulse.PHASE_CYCLE_GROUP
).value = phase_cycle_group ).value = phase_cycle_group
self.update_options()
# RX Specific functions # RX Specific functions
def set_rx(self, event: Event | str, rx: bool) -> None: 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 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: def set_rx_phase(self, event: Event | str, phase: list) -> None:
"""Sets the readout scheme of the receiver. """Sets the phase of the receiver.
Args: Args:
event (Event | str): The event to set the readout scheme for or the name of the event event (Event | str): The event to set the phase for or the name of the event
readout_scheme (list): The readout scheme of the receiver phase (list): The phase of the receiver
""" """
if isinstance(event, str): if isinstance(event, str):
event = self.get_event_by_name(event) event = self.get_event_by_name(event)
# Check that the readout scheme is valid rx_table = event.parameters[self.RX_READOUT].get_option_by_name(RXReadout.READOUT_SCHEME)
self.phase_table.generate_phase_array()
n_cycles = self.phase_table.n_phase_cycles
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( 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 # Set the values
#event.parameters[self.RX_READOUT].get_option_by_name( phase_option.values = phase
# RXReadout.READOUT_SCHEME
#).value = readout_scheme
self.phase_table.readout_scheme.readout_scheme = readout_scheme 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

View file

@ -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 # 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] readout_scheme = [0, 180, 0, 180]
COMPFID.set_rx_readout_scheme("rx", readout_scheme) COMPFID.set_rx_phase("rx", readout_scheme)
return COMPFID return COMPFID

View file

@ -29,6 +29,6 @@ def create_SEPC() -> QuackSequence:
# Readout scheme for phase cycling TX pulses have the scheme 0 90 180 270 # Readout scheme for phase cycling TX pulses have the scheme 0 90 180 270
readout_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 return sepc