Change to quackseq structure.

This commit is contained in:
jupfi 2024-05-29 16:04:00 +02:00
parent bd40004dd3
commit c1208103ca
9 changed files with 346 additions and 1560 deletions

View file

@ -30,6 +30,7 @@ dependencies = [
"sympy",
"numpy",
"scipy",
"quackseq",
]
[project.optional-dependencies]

View file

@ -16,7 +16,7 @@ class BaseSpectrometerController(ModuleController):
def on_loading(self):
"""Called when the module is loading."""
logger.debug("Loading spectrometer controller")
self.module.model.load_default_settings()
#self.module.model.load_default_settings()
def save_settings(self, path: str) -> None:
"""Saves the settings of the spectrometer."""

View file

@ -1,11 +1,10 @@
"""The base class for all spectrometer models."""
import logging
from collections import OrderedDict
from PyQt6.QtCore import QSettings
from PyQt6.QtGui import QPixmap
from nqrduck.module.module_model import ModuleModel
from .settings import Setting
from quackseq.spectrometer.spectrometer_settings import FloatSetting, BooleanSetting, IntSetting, StringSetting
from .visual_settings import VisualFloatSetting, VisualIntSetting, VisualBooleanSetting, VisualStringSetting
logger = logging.getLogger(__name__)
@ -25,74 +24,6 @@ class BaseSpectrometerModel(ModuleModel):
SETTING_FILE_EXTENSION = "setduck"
settings: OrderedDict
pulse_parameter_options: OrderedDict
class PulseParameter:
"""A pulse parameter is a value that can be different for each event in a pulse sequence.
E.g. the transmit pulse power or the phase of the transmit pulse.
Args:
name (str) : The name of the pulse parameter
Attributes:
name (str) : The name of the pulse parameter
options (OrderedDict) : The options of the pulse parameter
"""
def __init__(self, name: str):
"""Initializes the pulse parameter.
Arguments:
name (str) : The name of the pulse parameter
"""
self.name = name
self.options = list()
def get_pixmap(self) -> QPixmap:
"""Gets the pixmap of the pulse parameter.
Implment this method in the derived class.
Returns:
QPixmap : The pixmap of the pulse parameter
"""
raise NotImplementedError
def add_option(self, option: "Option") -> None:
"""Adds an option to the pulse parameter.
Args:
option (Option) : The option to add
"""
self.options.append(option)
def get_options(self) -> list:
"""Gets the options of the pulse parameter.
Returns:
list : The options of the pulse parameter
"""
return self.options
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 specified name
Raises:
ValueError : If no option with the specified name is found
"""
for option in self.options:
if option.name == name:
return option
raise ValueError(f"Option with name {name} not found")
def __init__(self, module):
"""Initializes the spectrometer model.
@ -100,10 +31,34 @@ class BaseSpectrometerModel(ModuleModel):
module (Module) : The module that the spectrometer is connected to
"""
super().__init__(module)
self.settings = OrderedDict()
self.pulse_parameter_options = OrderedDict()
self.default_settings = QSettings("nqrduck-spectrometer", "nqrduck")
self.quackseq_model = None
self.quackseq_visuals = dict()
def visualize_settings(self):
settings = self.quackseq_model.settings
for name, setting in settings.items():
logger.debug(f"Setting: {name}, Value: {setting.value}")
# Now we need to translate for example a FloatSetting to a VisualFloat setting
if isinstance(setting, FloatSetting):
self.quackseq_visuals[name] = VisualFloatSetting(setting)
elif isinstance(setting, IntSetting):
self.quackseq_visuals[name] = VisualIntSetting(setting)
elif isinstance(setting, BooleanSetting):
self.quackseq_visuals[name] = VisualBooleanSetting(setting)
elif isinstance(setting, StringSetting):
self.quackseq_visuals[name] = VisualStringSetting(setting)
else:
logger.error(f"Setting type {type(setting)} not supported")
def set_default_settings(self) -> None:
"""Sets the default settings of the spectrometer."""
self.default_settings.clear()
@ -115,57 +70,15 @@ class BaseSpectrometerModel(ModuleModel):
def load_default_settings(self) -> None:
"""Load the default settings of the spectrometer."""
for category in self.settings.keys():
for setting in self.settings[category]:
setting_string = f"{self.module.model.name},{setting.name}"
if self.default_settings.contains(setting_string):
logger.debug(f"Loading default value for {setting_string}")
setting.value = self.default_settings.value(setting_string)
for setting in self.quackseq_model.settings.values():
setting_string = f"{self.module.model.name},{setting.name}"
setting.value = self.default_settings.value(setting_string)
logger.debug(f"Setting {setting_string} to {setting.value}")
def clear_default_settings(self) -> None:
"""Clear the default settings of the spectrometer."""
self.default_settings.clear()
def add_setting(self, setting: Setting, category: str) -> None:
"""Adds a setting to the spectrometer.
Args:
setting (Setting) : The setting to add
category (str) : The category of the setting
"""
if category not in self.settings.keys():
self.settings[category] = []
self.settings[category].append(setting)
def get_setting_by_name(self, name: str) -> Setting:
"""Gets a setting by its name.
Args:
name (str) : The name of the setting
Returns:
Setting : The setting with the specified name
Raises:
ValueError : If no setting with the specified name is found
"""
for category in self.settings.keys():
for setting in self.settings[category]:
if setting.name == name:
return setting
raise ValueError(f"Setting with name {name} not found")
def add_pulse_parameter_option(
self, name: str, pulse_parameter_class: PulseParameter
) -> None:
"""Adds a pulse parameter option to the spectrometer.
Args:
name (str) : The name of the pulse parameter
pulse_parameter_class (PulseParameter) : The pulse parameter class
"""
self.pulse_parameter_options[name] = pulse_parameter_class
@property
def target_frequency(self):
"""The target frequency of the spectrometer in Hz. This is the frequency where the magnetic resonance experiment is performed."""

View file

@ -16,7 +16,6 @@ from nqrduck.assets.icons import Logos
logger = logging.getLogger(__name__)
class BaseSpectrometerView(ModuleView):
"""The View Class for all Spectrometers."""
@ -46,7 +45,11 @@ class BaseSpectrometerView(ModuleView):
self._ui_form.verticalLayout.addWidget(label)
self._ui_form.verticalLayout.addLayout(grid)
for category_count, category in enumerate(self.module.model.settings.keys()):
settings = self.module.model.quackseq_model.settings
categories = settings.categories
for category_count, category in enumerate(categories):
logger.debug("Adding settings for category: %s", category)
category_layout = QVBoxLayout()
category_label = QLabel(f"{category}:")
@ -55,7 +58,7 @@ class BaseSpectrometerView(ModuleView):
column = category_count % 2
category_layout.addWidget(category_label)
for setting in self.module.model.settings[category]:
for key, setting in settings.get_settings_by_category(category).items():
logger.debug("Adding setting to settings view: %s", setting.name)
spacer = QSpacerItem(20, 20)
@ -63,7 +66,7 @@ class BaseSpectrometerView(ModuleView):
setting_label = QLabel(setting.name)
setting_label.setMinimumWidth(200)
edit_widget = setting.widget
edit_widget = self.module.model.quackseq_visuals[key].widget
logger.debug("Setting widget: %s", edit_widget)
# Add a icon that can be used as a tooltip

View file

@ -1,363 +0,0 @@
"""This module defines the measurement data structure and the fit class for measurement data."""
import logging
import numpy as np
from scipy.optimize import curve_fit
from nqrduck.helpers.signalprocessing import SignalProcessing as sp
from nqrduck.helpers.functions import Function
logger = logging.getLogger(__name__)
class Measurement:
"""This class defines how measurement data should look.
It includes pulse parameters necessary for further signal processing.
Every spectrometer should adhere to this data structure in order to be compatible with the rest of the nqrduck.
Args:
name (str): Name of the measurement.
tdx (np.array): Time axis for the x axis of the measurement data.
tdy (np.array): Time axis for the y axis of the measurement data.
target_frequency (float): Target frequency of the measurement.
frequency_shift (float, optional): Frequency shift of the measurement. Defaults to 0.
IF_frequency (float, optional): Intermediate frequency of the measurement. Defaults to 0.
Attributes:
tdx (np.array): Time axis for the x axis of the measurement data.
tdy (np.array): Time axis for the y axis of the measurement data.
target_frequency (float): Target frequency of the measurement.
frequency_shift (float): Frequency shift of the measurement.
IF_frequency (float): Intermediate frequency of the measurement.
fdx (np.array): Frequency axis for the x axis of the measurement data.
fdy (np.array): Frequency axis for the y axis of the measurement data.
"""
def __init__(
self,
name: str,
tdx: np.array,
tdy: np.array,
target_frequency: float,
frequency_shift: float = 0,
IF_frequency: float = 0,
) -> None:
"""Initializes the measurement."""
self.name = name
self.tdx = tdx
self.tdy = tdy
self.target_frequency = target_frequency
self.frequency_shift = frequency_shift
self.IF_frequency = IF_frequency
self.fdx, self.fdy = sp.fft(tdx, tdy, frequency_shift)
self.fits = []
def apodization(self, function: Function) -> "Measurement":
"""Applies apodization to the measurement data.
Args:
function (Function): Apodization function.
Returns:
Measurement: The apodized measurement.
"""
duration = (self.tdx[-1] - self.tdx[0]) * 1e-6
resolution = duration / len(self.tdx)
logger.debug("Resolution: %s", resolution)
y_weight = function.get_pulse_amplitude(duration, resolution)
tdy_apodized = self.tdy * y_weight
apodized_measurement = Measurement(
self.name,
self.tdx,
tdy_apodized,
target_frequency=self.target_frequency,
IF_frequency=self.IF_frequency,
)
return apodized_measurement
def add_fit(self, fit: "Fit") -> None:
"""Adds a fit to the measurement.
Args:
fit (Fit): The fit to add.
"""
self.fits.append(fit)
def delete_fit(self, fit: "Fit") -> None:
"""Deletes a fit from the measurement.
Args:
fit (Fit): The fit to delete.
"""
self.fits.remove(fit)
def edit_fit_name(self, fit: "Fit", name: str) -> None:
"""Edits the name of a fit.
Args:
fit (Fit): The fit to edit.
name (str): The new name.
"""
logger.debug(f"Editing fit name to {name}.")
fit.name = name
def to_json(self) -> dict:
"""Converts the measurement to a JSON-compatible format.
Returns:
dict: The measurement in JSON-compatible format.
"""
return {
"name": self.name,
"tdx": self.tdx.tolist(),
"tdy": [[x.real, x.imag] for x in self.tdy],
"target_frequency": self.target_frequency,
"IF_frequency": self.IF_frequency,
"fits": [fit.to_json() for fit in self.fits],
}
@classmethod
def from_json(cls, json: dict) -> "Measurement":
"""Converts the JSON format to a measurement.
Args:
json (dict): The measurement in JSON-compatible format.
Returns:
Measurement: The measurement.
"""
tdy = np.array([complex(y[0], y[1]) for y in json["tdy"]])
measurement = cls(
json["name"],
np.array(json["tdx"]),
tdy,
target_frequency=json["target_frequency"],
IF_frequency=json["IF_frequency"],
)
for fit_json in json["fits"]:
measurement.add_fit(Fit.from_json(fit_json, measurement))
return measurement
# Properties for encapsulation
@property
def name(self) -> str:
"""Name of the measurement."""
return self._name
@name.setter
def name(self, value: str) -> None:
self._name = value
@property
def tdx(self) -> np.array:
"""Time domain data for the measurement (x)."""
return self._tdx
@tdx.setter
def tdx(self, value: np.array) -> None:
self._tdx = value
@property
def tdy(self) -> np.array:
"""Time domain data for the measurement (y)."""
return self._tdy
@tdy.setter
def tdy(self, value: np.array) -> None:
self._tdy = value
@property
def fdx(self) -> np.array:
"""Frequency domain data for the measurement (x)."""
return self._fdx
@fdx.setter
def fdx(self, value: np.array) -> None:
self._fdx = value
@property
def fdy(self) -> np.array:
"""Frequency domain data for the measurement (y)."""
return self._fdy
@fdy.setter
def fdy(self, value: np.array) -> None:
self._fdy = value
@property
def target_frequency(self) -> float:
"""Target frequency of the measurement."""
return self._target_frequency
@target_frequency.setter
def target_frequency(self, value: float) -> None:
self._target_frequency = value
@property
def fits(self) -> list:
"""Fits of the measurement."""
return self._fits
@fits.setter
def fits(self, value: list) -> None:
self._fits = value
class Fit:
"""The fit class for measurement data. A fit can be performed on either the frequency or time domain data.
A measurement can have multiple fits.
"""
subclasses = []
def __init_subclass__(cls, **kwargs) -> None:
"""Adds the subclass to the list of subclasses."""
super().__init_subclass__(**kwargs)
cls.subclasses.append(cls)
def __init__(self, name: str, domain: str, measurement: Measurement) -> None:
"""Initializes the fit."""
self.name = name
self.domain = domain
self.measurement = measurement
self.fit()
def fit(self) -> None:
"""Fits the measurement data and sets the fit parameters and covariance."""
if self.domain == "time":
x = self.measurement.tdx
y = self.measurement.tdy
elif self.domain == "frequency":
x = self.measurement.fdx
y = self.measurement.fdy
else:
raise ValueError("Domain not recognized.")
initial_guess = self.initial_guess()
self.parameters, self.covariance = curve_fit(
self.fit_function, x, abs(y), p0=initial_guess
)
self.x = x
self.y = self.fit_function(x, *self.parameters)
def fit_function(self, x: np.array, *parameters) -> np.array:
"""The fit function.
Args:
x (np.array): The x data.
*parameters: The fit parameters.
Returns:
np.array: The y data.
"""
raise NotImplementedError
def initial_guess(self) -> list:
"""Initial guess for the fit.
Returns:
list: The initial guess.
"""
raise NotImplementedError
def to_json(self) -> dict:
"""Converts the fit to a JSON-compatible format.
Returns:
dict: The fit in JSON-compatible format.
"""
return {
"name": self.name,
"class": self.__class__.__name__,
}
@classmethod
def from_json(cls, data: dict, measurement: Measurement) -> "Fit":
"""Converts the JSON format to a fit.
Args:
data (dict): The fit in JSON-compatible format.
measurement (Measurement): The measurement.
Returns:
Fit: The fit.
"""
for subclass in cls.subclasses:
if subclass.__name__ == data["class"]:
return subclass(name=data["name"], measurement=measurement)
raise ValueError(f"Subclass {data['class']} not found.")
@property
def x(self) -> np.array:
"""The x data of the fit."""
return self._x
@x.setter
def x(self, value: np.array) -> None:
self._x = value
@property
def y(self) -> np.array:
"""The y data of the fit."""
return self._y
@y.setter
def y(self, value: np.array) -> None:
self._y = value
class T2StarFit(Fit):
"""T2* fit for measurement data."""
def __init__(self, measurement: Measurement, name: str = "T2*") -> None:
"""Initializes the T2* fit."""
super().__init__(name, "time", measurement)
def fit(self) -> None:
"""Fits the measurement data and sets the fit parameters and covariance."""
super().fit()
self.parameters = {
"S0": self.parameters[0],
"T2Star": self.parameters[1],
"covariance": self.covariance,
}
def fit_function(self, t: np.array, S0: float, T2Star: float) -> np.array:
"""The T2* fit function used for curve fitting."""
return S0 * np.exp(-t / T2Star)
def initial_guess(self) -> list:
"""Initial guess for the T2* fit."""
return [1, 1]
class LorentzianFit(Fit):
"""Lorentzian fit for measurement data."""
def __init__(self, measurement: Measurement, name: str = "Lorentzian") -> None:
"""Initializes the Lorentzian fit."""
super().__init__(name, "frequency", measurement)
def fit(self) -> None:
"""Fits the measurement data and sets the fit parameters and covariance."""
super().fit()
self.parameters = {
"S0": self.parameters[0],
"T2Star": self.parameters[1],
"covariance": self.covariance,
}
logger.debug("Lorentzian fit parameters: %s", self.parameters)
def fit_function(self, f: np.array, S0: float, T2Star: float) -> np.array:
"""The Lorentzian fit function used for curve fitting."""
return S0 / (1 + (2 * np.pi * f * T2Star) ** 2)
def initial_guess(self) -> list:
"""Initial guess for the Lorentzian fit."""
return [1, 1]

View file

@ -1,420 +0,0 @@
"""Contains the classes for the pulse parameters of the spectrometer. It includes the functions and the options for the pulse parameters.
Todo:
* This shouldn't be in the spectrometer module. It should be in it"s own pulse sequence module.
"""
from __future__ import annotations
import logging
from numpy.core.multiarray import array as array
from nqrduck.assets.icons import PulseParamters
from nqrduck.helpers.functions import (
Function,
RectFunction,
SincFunction,
GaussianFunction,
CustomFunction,
)
from .base_spectrometer_model import BaseSpectrometerModel
logger = logging.getLogger(__name__)
class Option:
"""Defines options for the pulse parameters which can then be set accordingly.
Options can be of different types, for example boolean, numeric or function.
Args:
name (str): The name of the option.
value: The value of the option.
Attributes:
name (str): The name of the option.
value: The value of the option.
"""
subclasses = []
def __init_subclass__(cls, **kwargs):
"""Adds the subclass to the list of subclasses."""
super().__init_subclass__(**kwargs)
cls.subclasses.append(cls)
def __init__(self, name: str, value) -> None:
"""Initializes the option."""
self.name = name
self.value = value
def set_value(self):
"""Sets the value of the option.
This method has to be implemented in the derived classes.
"""
raise NotImplementedError
def to_json(self):
"""Returns a json representation of the option.
Returns:
dict: The json representation of the option.
"""
return {
"name": self.name,
"value": self.value,
"class": self.__class__.__name__,
}
@classmethod
def from_json(cls, data) -> Option:
"""Creates an option from a json representation.
Args:
data (dict): The json representation of the option.
Returns:
Option: The option.
"""
for subclass in cls.subclasses:
logger.debug(f"Keys data: {data.keys()}")
if subclass.__name__ == data["class"]:
cls = subclass
break
# Check if from_json is implemented for the subclass
if cls.from_json.__func__ == Option.from_json.__func__:
obj = cls(data["name"], data["value"])
else:
obj = cls.from_json(data)
return obj
class BooleanOption(Option):
"""Defines a boolean option for a pulse parameter option."""
def set_value(self, value):
"""Sets the value of the option."""
self.value = value
class NumericOption(Option):
"""Defines a numeric option for a pulse parameter option."""
def __init__(
self, name: str, value, is_float=True, min_value=None, max_value=None
) -> None:
"""Initializes the NumericOption.
Args:
name (str): The name of the option.
value: The value of the 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.
"""
super().__init__(name, value)
self.is_float = is_float
self.min_value = min_value
self.max_value = max_value
def set_value(self, value):
"""Sets the value of the option."""
if value < self.min_value:
self.value = self.min_value
elif value >= self.max_value:
self.value = self.max_value
else:
raise ValueError(
f"Value {value} is not in the range of {self.min_value} to {self.max_value}. This should have been cought earlier."
)
def to_json(self):
"""Returns a json representation of the option.
Returns:
dict: The json representation of the option.
"""
return {
"name": self.name,
"value": self.value,
"class": self.__class__.__name__,
"is_float": self.is_float,
"min_value": self.min_value,
"max_value": self.max_value,
}
@classmethod
def from_json(cls, data):
"""Creates a NumericOption from a json representation.
Args:
data (dict): The json representation of the NumericOption.
Returns:
NumericOption: The NumericOption.
"""
obj = cls(
data["name"],
data["value"],
is_float=data["is_float"],
min_value=data["min_value"],
max_value=data["max_value"],
)
return obj
class FunctionOption(Option):
"""Defines a selection option for a pulse parameter option.
It takes different function objects.
Args:
name (str): The name of the option.
functions (list): The functions that can be selected.
Attributes:
name (str): The name of the option.
functions (list): The functions that can be selected.
"""
def __init__(self, name, functions) -> None:
"""Initializes the FunctionOption."""
super().__init__(name, functions[0])
self.functions = functions
def set_value(self, value):
"""Sets the value of the option.
Args:
value: The value of the option.
"""
self.value = value
def get_function_by_name(self, name):
"""Returns the function with the given name.
Args:
name (str): The name of the function.
Returns:
Function: The function with the given name.
"""
for function in self.functions:
if function.name == name:
return function
raise ValueError(f"Function with name {name} not found")
def to_json(self):
"""Returns a json representation of the option.
Returns:
dict: The json representation of the option.
"""
return {
"name": self.name,
"value": self.value.to_json(),
"class": self.__class__.__name__,
"functions": [function.to_json() for function in self.functions],
}
@classmethod
def from_json(cls, data):
"""Creates a FunctionOption from a json representation.
Args:
data (dict): The json representation of the FunctionOption.
Returns:
FunctionOption: The FunctionOption.
"""
logger.debug(f"Data: {data}")
# These are all available functions
functions = [Function.from_json(function) for function in data["functions"]]
obj = cls(data["name"], functions)
obj.value = Function.from_json(data["value"])
return obj
def get_pixmap(self):
"""Returns the pixmap of the function."""
return self.value.get_pixmap()
class TXRectFunction(RectFunction):
"""TX Rectangular function.
Adds the pixmap of the function to the class.
"""
def __init__(self) -> None:
"""Initializes the TX Rectangular function."""
super().__init__()
self.name = "Rectangular"
def get_pixmap(self):
"""Returns the pixmaps of the function."""
return PulseParamters.TXRect()
class TXSincFunction(SincFunction):
"""TX Sinc function.
Adds the pixmap of the function to the class.
"""
def __init__(self) -> None:
"""Initializes the TX Sinc function."""
super().__init__()
self.name = "Sinc"
def get_pixmap(self):
"""Returns the pixmaps of the function."""
return PulseParamters.TXSinc()
class TXGaussianFunction(GaussianFunction):
"""TX Gaussian function.
Adds the pixmap of the function to the class.
"""
def __init__(self) -> None:
"""Initializes the TX Gaussian function."""
super().__init__()
self.name = "Gaussian"
def get_pixmap(self):
"""Returns the pixmaps of the function."""
return PulseParamters.TXGauss()
class TXCustomFunction(CustomFunction):
"""TX Custom function.
Adds the pixmap of the function to the class.
"""
def __init__(self) -> None:
"""Initializes the TX Custom function."""
super().__init__()
self.name = "Custom"
def get_pixmap(self):
"""Returns the pixmaps of the function."""
return PulseParamters.TXCustom()
class TXPulse(BaseSpectrometerModel.PulseParameter):
"""Basic TX Pulse Parameter. It includes options for the relative amplitude, the phase and the pulse shape.
Args:
name (str): The name of the pulse parameter.
"""
RELATIVE_AMPLITUDE = "Relative TX Amplitude (%)"
TX_PHASE = "TX Phase"
TX_PULSE_SHAPE = "TX Pulse Shape"
def __init__(self, name: str) -> None:
"""Initializes the TX Pulse Parameter.
It adds the options for the relative amplitude, the phase and the pulse shape.
"""
super().__init__(name)
self.add_option(
NumericOption(
self.RELATIVE_AMPLITUDE, 0, is_float=False, min_value=0, max_value=100
)
)
self.add_option(NumericOption(self.TX_PHASE, 0))
self.add_option(
FunctionOption(
self.TX_PULSE_SHAPE,
[
TXRectFunction(),
TXSincFunction(),
TXGaussianFunction(),
TXCustomFunction(),
],
),
)
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.
"""
if self.get_option_by_name(self.RELATIVE_AMPLITUDE).value > 0:
return self.get_option_by_name(self.TX_PULSE_SHAPE).get_pixmap()
else:
pixmap = PulseParamters.TXOff()
return pixmap
class RXReadout(BaseSpectrometerModel.PulseParameter):
"""Basic PulseParameter for the RX Readout. It includes an option for the RX Readout state.
Args:
name (str): The name of the pulse parameter.
Attributes:
RX (str): The RX Readout state.
"""
RX = "RX"
def __init__(self, name) -> None:
"""Initializes the RX Readout PulseParameter.
It adds an option for the RX Readout state.
"""
super().__init__(name)
self.add_option(BooleanOption(self.RX, False))
def get_pixmap(self):
"""Returns the pixmap of the RX Readout PulseParameter.
Returns:
QPixmap: The pixmap of the RX Readout PulseParameter depending on the RX Readout state.
"""
if self.get_option_by_name(self.RX).value is False:
pixmap = PulseParamters.RXOff()
else:
pixmap = PulseParamters.RXOn()
return pixmap
class Gate(BaseSpectrometerModel.PulseParameter):
"""Basic PulseParameter for the Gate. It includes an option for the Gate state.
Args:
name (str): The name of the pulse parameter.
Attributes:
GATE_STATE (str): The Gate state.
"""
GATE_STATE = "Gate State"
def __init__(self, name) -> None:
"""Initializes the Gate PulseParameter.
It adds an option for the Gate state.
"""
super().__init__(name)
self.add_option(BooleanOption(self.GATE_STATE, False))
def get_pixmap(self):
"""Returns the pixmap of the Gate PulseParameter.
Returns:
QPixmap: The pixmap of the Gate PulseParameter depending on the Gate state.
"""
if self.get_option_by_name(self.GATE_STATE).value is False:
pixmap = PulseParamters.GateOff()
else:
pixmap = PulseParamters.GateOn()
return pixmap

View file

@ -1,234 +0,0 @@
"""Contains the PulseSequence class that is used to store a pulse sequence and its events."""
import logging
import importlib.metadata
from collections import OrderedDict
from nqrduck.helpers.unitconverter import UnitConverter
from nqrduck_spectrometer.pulseparameters import Option
logger = logging.getLogger(__name__)
class PulseSequence:
"""A pulse sequence is a collection of events that are executed in a certain order.
Args:
name (str): The name of the pulse sequence
Attributes:
name (str): The name of the pulse sequence
events (list): The events of the pulse sequence
"""
def __init__(self, name, version = None) -> None:
"""Initializes the pulse sequence."""
self.name = name
# Saving version to check for compatability of saved sequence
if version is not None:
self.version = version
else:
self.version = importlib.metadata.version("nqrduck_spectrometer")
self.events = list()
def get_event_names(self) -> list:
"""Returns a list of the names of the events in the pulse sequence.
Returns:
list: The names of the events
"""
return [event.name for event in self.events]
class Event:
"""An event is a part of a pulse sequence. It has a name and a duration and different parameters that have to be set.
Args:
name (str): The name of the event
duration (str): The duration of the event
Attributes:
name (str): The name of the event
duration (str): The duration of the event
parameters (OrderedDict): The parameters of the event
"""
def __init__(self, name: str, duration: str) -> None:
"""Initializes the event."""
self.parameters = OrderedDict()
self.name = name
self.duration = duration
def add_parameter(self, parameter) -> None:
"""Adds a parameter to the event.
Args:
parameter: The parameter to add
"""
self.parameters.append(parameter)
def on_duration_changed(self, duration: str) -> None:
"""This method is called when the duration of the event is changed.
Args:
duration (str): The new duration of the event
"""
logger.debug("Duration of event %s changed to %s", self.name, duration)
self.duration = duration
@classmethod
def load_event(cls, event, pulse_parameter_options):
"""Loads an event from a dict.
The pulse paramter options are needed to load the parameters
and determine if the correct spectrometer is active.
Args:
event (dict): The dict with the event data
pulse_parameter_options (dict): The dict with the pulse parameter options
Returns:
Event: The loaded event
"""
obj = cls(event["name"], event["duration"])
for parameter in event["parameters"]:
for pulse_parameter_option in pulse_parameter_options.keys():
# This checks if the pulse paramter options are the same as the ones in the pulse sequence
if pulse_parameter_option == parameter["name"]:
pulse_parameter_class = pulse_parameter_options[
pulse_parameter_option
]
obj.parameters[pulse_parameter_option] = pulse_parameter_class(
parameter["name"]
)
# Delete the default instances of the pulse parameter options
obj.parameters[pulse_parameter_option].options = []
for option in parameter["value"]:
obj.parameters[pulse_parameter_option].options.append(
Option.from_json(option)
)
return obj
@property
def duration(self):
"""The duration of the event."""
return self._duration
@duration.setter
def duration(self, duration: str):
# Duration needs to be a positive number
try:
duration = UnitConverter.to_float(duration)
except ValueError:
raise ValueError("Duration needs to be a number")
if duration < 0:
raise ValueError("Duration needs to be a positive number")
self._duration = duration
def to_json(self):
"""Returns a dict with all the data in the pulse sequence.
Returns:
dict: The dict with the sequence data
"""
# Get the versions of this package
data = {"name": self.name, "version" : self.version, "events": []}
for event in self.events:
event_data = {
"name": event.name,
"duration": event.duration,
"parameters": [],
}
for parameter in event.parameters.keys():
event_data["parameters"].append({"name": parameter, "value": []})
for option in event.parameters[parameter].options:
event_data["parameters"][-1]["value"].append(option.to_json())
data["events"].append(event_data)
return data
@classmethod
def load_sequence(cls, sequence, pulse_parameter_options):
"""Loads a pulse sequence from a dict.
The pulse paramter options are needed to load the parameters
and make sure the correct spectrometer is active.
Args:
sequence (dict): The dict with the sequence data
pulse_parameter_options (dict): The dict with the pulse parameter options
Returns:
PulseSequence: The loaded pulse sequence
Raises:
KeyError: If the pulse parameter options are not the same as the ones in the pulse sequence
"""
try:
obj = cls(sequence["name"], version = sequence["version"])
except KeyError:
logger.error("Pulse sequence version not found")
raise KeyError("Pulse sequence version not found")
for event_data in sequence["events"]:
obj.events.append(cls.Event.load_event(event_data, pulse_parameter_options))
return obj
class Variable:
"""A variable is a parameter that can be used within a pulsesequence as a placeholder.
For example the event duration a Variable with name a can be set. This variable can then be set to a list of different values.
On execution of the pulse sequence the event duration will be set to the first value in the list.
Then the pulse sequence will be executed with the second value of the list. This is repeated until the pulse sequence has
been executed with all values in the list.
"""
@property
def name(self):
"""The name of the variable."""
return self._name
@name.setter
def name(self, name: str):
if not isinstance(name, str):
raise TypeError("Name needs to be a string")
self._name = name
@property
def values(self):
"""The values of the variable. This is a list of values that the variable can take."""
return self._values
@values.setter
def values(self, values: list):
if not isinstance(values, list):
raise TypeError("Values needs to be a list")
self._values = values
class VariableGroup:
"""Variables can be grouped together.
If we have groups a and b the pulse sequence will be executed for all combinations of variables in a and b.
"""
@property
def name(self):
"""The name of the variable group."""
return self._name
@name.setter
def name(self, name: str):
if not isinstance(name, str):
raise TypeError("Name needs to be a string")
self._name = name
@property
def variables(self):
"""The variables in the group. This is a list of variables."""
return self._variables
@variables.setter
def variables(self, variables: list):
if not isinstance(variables, list):
raise TypeError("Variables needs to be a list")
self._variables = variables

View file

@ -1,419 +0,0 @@
"""Settings for the different spectrometers."""
import logging
import ipaddress
from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot
from PyQt6.QtWidgets import QLineEdit, QComboBox, QCheckBox
from nqrduck.helpers.duckwidgets import DuckFloatEdit, DuckIntEdit, DuckSpinBox
logger = logging.getLogger(__name__)
class Setting(QObject):
"""A setting for the spectrometer is a value that is the same for all events in a pulse sequence.
E.g. the Transmit gain or the number of points in a spectrum.
Args:
name (str) : The name of the setting
description (str) : A description of the setting
default : The default value of the setting
Attributes:
name (str) : The name of the setting
description (str) : A description of the setting
value : The value of the setting
widget : The widget that is used to change the setting
"""
settings_changed = pyqtSignal()
def __init__(self, name: str, description: str, default=None) -> None:
"""Create a new setting.
Args:
name (str): The name of the setting.
description (str): A description of the setting.
default: The default value of the setting.
"""
self.widget = None
super().__init__()
self.name = name
self.description = description
if default is not None:
self.value = default
# Update the description with the default value
self.description += f"\n (Default: {default})"
# This can be overridden by subclasses
self.widget = self.get_widget()
@pyqtSlot(str)
def on_value_changed(self, value):
"""This method is called when the value of the setting is changed.
Args:
value (str): The new value of the setting.
"""
logger.debug("Setting %s changed to %s", self.name, value)
self.value = value
self.settings_changed.emit()
def get_setting(self):
"""Return the value of the setting.
Returns:
The value of the setting.
"""
return float(self.value)
def get_widget(self):
"""Return a widget for the setting.
The default widget is simply a QLineEdit.
This method can be overwritten by subclasses to return a different widget.
Returns:
QLineEdit: A QLineEdit widget that can be used to change the setting.
"""
widget = QLineEdit(str(self.value))
widget.setMinimumWidth(100)
widget.editingFinished.connect(
lambda x=widget, s=self: s.on_value_changed(x.text())
)
return widget
class NumericalSetting(Setting):
"""A setting that is a numerical value.
It can additionally have a minimum and maximum value.
"""
def __init__(
self, name: str, description: str, default, min_value=None, max_value=None
) -> None:
"""Create a new numerical setting."""
super().__init__(
name,
self.description_limit_info(description, min_value, max_value),
default,
)
def description_limit_info(self, description: str, min_value, max_value) -> str:
"""Updates the description with the limits of the setting if there are any.
Args:
description (str): The description of the setting.
min_value: The minimum value of the setting.
max_value: The maximum value of the setting.
Returns:
str: The description of the setting with the limits.
"""
if min_value is not None and max_value is not None:
description += f"\n (min: {min_value}, max: {max_value})"
elif min_value is not None:
description += f"\n (min: {min_value})"
elif max_value is not None:
description += f"\n (max: {max_value})"
return description
class FloatSetting(NumericalSetting):
"""A setting that is a Float.
Args:
name (str) : The name of the setting
default : The default value of the setting
description (str) : A description of the setting
min_value : The minimum value of the setting
max_value : The maximum value of the setting
spin_box : A tuple with two booleans that determine if a spin box is used if the second value is True, a slider will be created as well.
"""
DEFAULT_LENGTH = 100
def __init__(
self,
name: str,
default: float,
description: str,
min_value: float = None,
max_value: float = None,
spin_box: tuple = (False, False),
) -> None:
"""Create a new float setting."""
self.spin_box = spin_box
super().__init__(name, description, default, min_value, max_value)
if spin_box[0]:
self.widget = DuckSpinBox(
min_value=min_value,
max_value=max_value,
slider=spin_box[1],
double_box=True,
)
self.widget.spin_box.setValue(default)
else:
self.widget = DuckFloatEdit(min_value=min_value, max_value=max_value)
self.widget.setText(str(default))
self.widget.state_updated.connect(self.on_state_updated)
def on_state_updated(self, state, text):
"""Update the value of the setting.
Args:
state (bool): The state of the input (valid or not).
text (str): The new value of the setting.
"""
if state:
self.value = text
self.settings_changed.emit()
@property
def value(self):
"""The value of the setting. In this case, a float."""
return self._value
@value.setter
def value(self, value):
logger.debug(f"Setting {self.name} to {value}")
self._value = float(value)
self.settings_changed.emit()
if self.widget:
if self.spin_box[0]:
self.widget.spin_box.setValue(self._value)
else:
self.widget.setText(str(self._value))
class IntSetting(NumericalSetting):
"""A setting that is an Integer.
Args:
name (str) : The name of the setting
default : The default value of the setting
description (str) : A description of the setting
min_value : The minimum value of the setting
max_value : The maximum value of the setting
spin_box : A tuple with two booleans that determine if a spin box is used if the second value is True, a slider will be created as well.
"""
def __init__(
self,
name: str,
default: int,
description: str,
min_value=None,
max_value=None,
spin_box: tuple = (False, False),
) -> None:
"""Create a new int setting."""
self.spin_box = spin_box
super().__init__(name, description, default, min_value, max_value)
if self.spin_box[0]:
self.widget = DuckSpinBox(
min_value=min_value, max_value=max_value, slider=spin_box[1]
)
self.widget.spin_box.setValue(default)
else:
self.widget = DuckIntEdit(min_value=min_value, max_value=max_value)
self.widget.setText(str(default))
self.widget.state_updated.connect(self.on_state_updated)
def on_state_updated(self, state, text):
"""Update the value of the setting.
Args:
state (bool): The state of the input (valid or not).
text (str): The new value of the setting.
"""
if state:
self.value = text
self.settings_changed.emit()
@property
def value(self):
"""The value of the setting. In this case, an int."""
return self._value
@value.setter
def value(self, value):
logger.debug(f"Setting {self.name} to {value}")
value = int(float(value))
self._value = value
self.settings_changed.emit()
if self.widget:
if self.spin_box[0]:
self.widget.spin_box.setValue(value)
else:
self.widget.setText(str(value))
class BooleanSetting(Setting):
"""A setting that is a Boolean.
Args:
name (str) : The name of the setting
default : The default value of the setting
description (str) : A description of the setting
"""
def __init__(self, name: str, default: bool, description: str) -> None:
"""Create a new boolean setting."""
super().__init__(name, description, default)
# Overrides the default widget
self.widget = self.get_widget()
@property
def value(self):
"""The value of the setting. In this case, a bool."""
return self._value
@value.setter
def value(self, value):
try:
self._value = bool(value)
if self.widget:
self.widget.setChecked(self._value)
self.settings_changed.emit()
except ValueError:
raise ValueError("Value must be a bool")
def get_widget(self):
"""Return a widget for the setting.
This returns a QCheckBox widget.
Returns:
QCheckBox: A QCheckBox widget that can be used to change the setting.
"""
widget = QCheckBox()
widget.setChecked(self.value)
widget.stateChanged.connect(
lambda x=widget, s=self: s.on_value_changed(bool(x))
)
return widget
class SelectionSetting(Setting):
"""A setting that is a selection from a list of options.
Args:
name (str) : The name of the setting
options (list) : A list of options to choose from
default : The default value of the setting
description (str) : A description of the setting
"""
def __init__(
self, name: str, options: list, default: str, description: str
) -> None:
"""Create a new selection setting."""
super().__init__(name, description, default)
# Check if default is in options
if default not in options:
raise ValueError("Default value must be one of the options")
self.options = options
# Overrides the default widget
self.widget = self.get_widget()
@property
def value(self):
"""The value of the setting. In this case, a string."""
return self._value
@value.setter
def value(self, value):
try:
if value in self.options:
self._value = value
if self.widget:
self.widget.setCurrentText(value)
self.settings_changed.emit()
else:
raise ValueError("Value must be one of the options")
# This fixes a bug when creating the widget when the options are not yet set
except AttributeError:
self._value = value
self.options = [value]
self.settings_changed.emit()
def get_widget(self):
"""Return a widget for the setting.
This returns a QComboBox widget.
Returns:
QComboBox: A QComboBox widget that can be used to change the setting.
"""
widget = QComboBox()
widget.addItems(self.options)
widget.setCurrentText(self.value)
widget.currentTextChanged.connect(
lambda x=widget, s=self: s.on_value_changed(x)
)
return widget
class IPSetting(Setting):
"""A setting that is an IP address.
Args:
name (str) : The name of the setting
default : The default value of the setting
description (str) : A description of the setting
"""
def __init__(self, name: str, default: str, description: str) -> None:
"""Create a new IP setting."""
super().__init__(name, description)
self.value = default
@property
def value(self):
"""The value of the setting. In this case, an IP address."""
return self._value
@value.setter
def value(self, value):
try:
ipaddress.ip_address(value)
self._value = value
except ValueError:
raise ValueError("Value must be a valid IP address")
self.settings_changed.emit()
class StringSetting(Setting):
"""A setting that is a string.
Args:
name (str) : The name of the setting
default : The default value of the setting
description (str) : A description of the setting
"""
def __init__(self, name: str, default: str, description: str) -> None:
"""Create a new string setting."""
super().__init__(name, description, default)
@property
def value(self):
"""The value of the setting. In this case, a string."""
return self._value
@value.setter
def value(self, value):
try:
self._value = str(value)
self.settings_changed.emit()
except ValueError:
raise ValueError("Value must be a string")

View file

@ -0,0 +1,305 @@
"""Settings for the different spectrometers."""
import logging
from PyQt6.QtCore import pyqtSignal, pyqtSlot, QObject
from PyQt6.QtWidgets import QLineEdit, QComboBox, QCheckBox
from nqrduck.helpers.duckwidgets import DuckFloatEdit, DuckIntEdit, DuckSpinBox
from quackseq.spectrometer.spectrometer_settings import (
FloatSetting,
IntSetting,
BooleanSetting,
SelectionSetting,
StringSetting,
)
logger = logging.getLogger(__name__)
class VisualSetting(QObject):
"""A visual setting that is created from a setting.
Args:
setting (Setting) : The setting that is used to create the visual setting.
"""
settings_changed = pyqtSignal()
def __init__(self, setting, *args, **kwargs) -> None:
"""Create a new setting.
Args:
setting (Setting): The setting that is used to create the visual setting.
"""
self.widget = None
self.setting = setting
super().__init__()
class VisualFloatSetting(VisualSetting):
"""A setting that is a Float.
Args:
setting (FloatSetting) : The setting that is used to create the visual setting.
"""
DEFAULT_LENGTH = 100
def __init__(
self,
setting: FloatSetting,
) -> None:
"""Create a new float setting."""
self.spin_box = False
super().__init__(setting)
# Create a spin box if min and max values are set
if setting.min_value is not None and setting.max_value is not None:
self.widget = DuckSpinBox(
min_value=setting.min_value,
max_value=setting.max_value,
slider=setting.slider,
double_box=True,
)
self.widget.spin_box.setValue(setting.default)
self.spin_box = True
else:
self.widget = DuckSpinBox(min_value=setting.min_value, max_value=setting.max_value, double_box=True)
self.widget.set_value(setting.default)
self.widget.state_updated.connect(self.on_state_updated)
def on_state_updated(self, state, text):
"""Update the value of the setting.
Args:
state (bool): The state of the input (valid or not).
text (str): The new value of the setting.
"""
if state:
self.value = text
self.settings_changed.emit()
@property
def value(self):
"""The value of the setting. In this case, a float."""
return self.setting.value
@value.setter
def value(self, value):
logger.debug(f"Setting {self.setting.name} to {value}")
self.setting.value = value
self.settings_changed.emit()
if self.widget:
if self.spin_box:
self.widget.spin_box.setValue(self.setting.value)
else:
self.widget.setText(str(self.setting.value))
class VisualIntSetting(VisualSetting):
"""A setting that is an Integer.
Args:
setting (IntSetting) : The setting that is used to create the visual setting.
spin_box : A tuple with two booleans that determine if a spin box is used if the second value is True, a slider will be created as well.
"""
def __init__(
self,
setting: IntSetting,
) -> None:
"""Create a new int setting."""
self.spin_box = False
super().__init__(setting)
if setting.min_value is not None and setting.max_value is not None:
self.widget = DuckSpinBox(
min_value=setting.min_value,
max_value=setting.max_value,
slider=setting.slider,
double_box=True,
)
self.spin_box = True
self.widget.spin_box.setValue(setting.default)
else:
self.widget = DuckIntEdit(
min_value=setting.min_value, max_value=setting.max_value
)
self.widget.setText(str(setting.default))
self.widget.state_updated.connect(self.on_state_updated)
def on_state_updated(self, state, text):
"""Update the value of the setting.
Args:
state (bool): The state of the input (valid or not).
text (str): The new value of the setting.
"""
if state:
self.value = text
self.settings_changed.emit()
@property
def value(self):
"""The value of the setting. In this case, an int."""
return self.setting.value
@value.setter
def value(self, value):
logger.debug(f"Setting {self.setting.name} to {value}")
value = int(float(value))
self.setting.value = value
self.settings_changed.emit()
if self.widget:
if self.spin_box:
self.widget.spin_box.setValue(value)
else:
self.widget.setText(str(value))
class VisualBooleanSetting(VisualSetting):
"""A setting that is a Boolean.
Args:
setting (BooleanSetting) : The setting that is used to create the visual setting.
"""
def __init__(self, setting: BooleanSetting) -> None:
"""Create a new boolean setting."""
super().__init(
setting
)
# Overrides the default widget
self.widget = self.get_widget()
@property
def value(self):
"""The value of the setting. In this case, a bool."""
return self.setting.value
@value.setter
def value(self, value):
try:
self.setting.value = bool(value)
if self.widget:
self.widget.setChecked(self._value)
self.settings_changed.emit()
except ValueError:
raise ValueError("Value must be a bool")
def get_widget(self):
"""Return a widget for the setting.
This returns a QCheckBox widget.
Returns:
QCheckBox: A QCheckBox widget that can be used to change the setting.
"""
widget = QCheckBox()
widget.setChecked(self.setting.value)
widget.stateChanged.connect(
lambda x=widget, s=self: s.on_value_changed(bool(x))
)
return widget
class VisualSelectionSetting(VisualSetting):
"""A setting that is a selection from a list of options.
Args:
setting (SelectionSetting) : The setting that is used to create the visual setting.
"""
def __init__(self, setting: SelectionSetting) -> None:
"""Create a new selection setting."""
super().__init__(setting)
# Overrides the default widget
self.widget = self.get_widget()
@property
def value(self):
"""The value of the setting. In this case, a string."""
return self._value
@value.setter
def value(self, value):
try:
if value in self.setting.options:
self.setting.value
if self.widget:
self.widget.setCurrentText(value)
self.settings_changed.emit()
else:
raise ValueError("Value must be one of the options")
# This fixes a bug when creating the widget when the options are not yet set
except AttributeError:
self._value = value
self.options = [value]
self.settings_changed.emit()
def get_widget(self):
"""Return a widget for the setting.
This returns a QComboBox widget.
Returns:
QComboBox: A QComboBox widget that can be used to change the setting.
"""
widget = QComboBox()
widget.addItems(self.options)
widget.setCurrentText(self.value)
widget.currentTextChanged.connect(
lambda x=widget, s=self: s.on_value_changed(x)
)
return widget
class VisualStringSetting(VisualSetting):
"""A setting that is a string.
Args:
setting (StringSetting) : The setting that is used to create the visual setting.
"""
def __init__(self, setting: StringSetting) -> None:
"""Create a new string setting."""
super().__init__(setting)
self.widget = self.get_widget()
@property
def value(self):
"""The value of the setting. In this case, a string."""
return self.setting.value
@value.setter
def value(self, value):
try:
self.setting.value = str(value)
self.settings_changed.emit()
except ValueError:
raise ValueError("Value must be a string")
def get_widget(self):
"""Return a widget for the setting.
The default widget is simply a QLineEdit.
This method can be overwritten by subclasses to return a different widget.
Returns:
QLineEdit: A QLineEdit widget that can be used to change the setting.
"""
widget = QLineEdit(str(self.value))
widget.setMinimumWidth(100)
widget.editingFinished.connect(
lambda x=widget, s=self: s.on_value_changed(x.text())
)
return widget