mirror of
https://github.com/nqrduck/nqrduck-spectrometer.git
synced 2024-11-12 21:51:53 +00:00
Change to quackseq structure.
This commit is contained in:
parent
bd40004dd3
commit
c1208103ca
9 changed files with 346 additions and 1560 deletions
|
@ -30,6 +30,7 @@ dependencies = [
|
|||
"sympy",
|
||||
"numpy",
|
||||
"scipy",
|
||||
"quackseq",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
|
@ -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
|
|
@ -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
|
|
@ -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")
|
305
src/nqrduck_spectrometer/visual_settings.py
Normal file
305
src/nqrduck_spectrometer/visual_settings.py
Normal 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
|
Loading…
Reference in a new issue