mirror of
https://github.com/nqrduck/nqrduck-spectrometer.git
synced 2024-12-22 00:10:26 +00:00
commit
04231158ef
8 changed files with 474 additions and 64 deletions
|
@ -1,5 +1,11 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Version 0.0.12 (27-05-2024)
|
||||||
|
|
||||||
|
- Implemented loading and saving of settings and default settings (`6496ec6824da33fa41a38e26fdcac9a03ceb51fb`)
|
||||||
|
|
||||||
|
- Added fitting of measurement data (`d43639c2f9796d5055dd01bd2f36bae43877bfe8`)
|
||||||
|
|
||||||
## Version 0.0.11 (20-05-2024)
|
## Version 0.0.11 (20-05-2024)
|
||||||
|
|
||||||
- Measurements are now run in a separate worker thread to prevent the GUI from freezing (`27865aa6d44158e74c0e537be8407c12b4e3725b`)
|
- Measurements are now run in a separate worker thread to prevent the GUI from freezing (`27865aa6d44158e74c0e537be8407c12b4e3725b`)
|
||||||
|
|
|
@ -7,7 +7,7 @@ allow-direct-references = true
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "nqrduck-spectrometer"
|
name = "nqrduck-spectrometer"
|
||||||
version = "0.0.11"
|
version = "0.0.12"
|
||||||
authors = [
|
authors = [
|
||||||
{ name="jupfi", email="support@nqruck.cool" },
|
{ name="jupfi", email="support@nqruck.cool" },
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
"""Base class for all spectrometer controllers."""
|
"""Base class for all spectrometer controllers."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import ast
|
||||||
from nqrduck.module.module_controller import ModuleController
|
from nqrduck.module.module_controller import ModuleController
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class BaseSpectrometerController(ModuleController):
|
class BaseSpectrometerController(ModuleController):
|
||||||
"""The base class for all spectrometer controllers."""
|
"""The base class for all spectrometer controllers."""
|
||||||
|
@ -10,6 +13,52 @@ class BaseSpectrometerController(ModuleController):
|
||||||
"""Initializes the spectrometer controller."""
|
"""Initializes the spectrometer controller."""
|
||||||
super().__init__(module)
|
super().__init__(module)
|
||||||
|
|
||||||
|
def on_loading(self):
|
||||||
|
"""Called when the module is loading."""
|
||||||
|
logger.debug("Loading spectrometer controller")
|
||||||
|
self.module.model.load_default_settings()
|
||||||
|
|
||||||
|
def save_settings(self, path: str) -> None:
|
||||||
|
"""Saves the settings of the spectrometer."""
|
||||||
|
# We get the different settings objects from the model
|
||||||
|
settings = self.module.model.settings
|
||||||
|
|
||||||
|
json = {}
|
||||||
|
json["name"] = self.module.model.name
|
||||||
|
|
||||||
|
for category in settings.keys():
|
||||||
|
for setting in settings[category]:
|
||||||
|
json[setting.name] = setting.value
|
||||||
|
|
||||||
|
with open(path, "w") as f:
|
||||||
|
f.write(str(json))
|
||||||
|
|
||||||
|
def load_settings(self, path: str) -> None:
|
||||||
|
"""Loads the settings of the spectrometer."""
|
||||||
|
with open(path) as f:
|
||||||
|
json = f.read()
|
||||||
|
|
||||||
|
# string to dict
|
||||||
|
json = ast.literal_eval(json)
|
||||||
|
|
||||||
|
module_name = self.module.model.name
|
||||||
|
json_name = json["name"]
|
||||||
|
|
||||||
|
# For some reason the notification is shown twice
|
||||||
|
if module_name != json_name:
|
||||||
|
message = f"Module: {module_name} not compatible with module specified in settings file: {json_name}. Did you select the correct settings file?"
|
||||||
|
self.module.nqrduck_signal.emit("notification", ["Error", message])
|
||||||
|
return
|
||||||
|
|
||||||
|
settings = self.module.model.settings
|
||||||
|
for category in settings.keys():
|
||||||
|
for setting in settings[category]:
|
||||||
|
if setting.name in json:
|
||||||
|
setting.value = json[setting.name]
|
||||||
|
else:
|
||||||
|
message = f"Setting {setting.name} not found in settings file. A change in settings might have broken compatibility."
|
||||||
|
self.module.nqrduck_signal.emit("notification", ["Error", message])
|
||||||
|
|
||||||
def start_measurement(self):
|
def start_measurement(self):
|
||||||
"""Starts the measurement.
|
"""Starts the measurement.
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
from PyQt6.QtCore import QSettings
|
||||||
from PyQt6.QtGui import QPixmap
|
from PyQt6.QtGui import QPixmap
|
||||||
from nqrduck.module.module_model import ModuleModel
|
from nqrduck.module.module_model import ModuleModel
|
||||||
from .settings import Setting
|
from .settings import Setting
|
||||||
|
@ -22,6 +23,8 @@ class BaseSpectrometerModel(ModuleModel):
|
||||||
pulse_parameter_options (OrderedDict) : The pulse parameter options of the spectrometer
|
pulse_parameter_options (OrderedDict) : The pulse parameter options of the spectrometer
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
SETTING_FILE_EXTENSION = "setduck"
|
||||||
|
|
||||||
settings: OrderedDict
|
settings: OrderedDict
|
||||||
pulse_parameter_options: OrderedDict
|
pulse_parameter_options: OrderedDict
|
||||||
|
|
||||||
|
@ -99,6 +102,29 @@ class BaseSpectrometerModel(ModuleModel):
|
||||||
super().__init__(module)
|
super().__init__(module)
|
||||||
self.settings = OrderedDict()
|
self.settings = OrderedDict()
|
||||||
self.pulse_parameter_options = OrderedDict()
|
self.pulse_parameter_options = OrderedDict()
|
||||||
|
self.default_settings = QSettings("nqrduck-spectrometer", "nqrduck")
|
||||||
|
|
||||||
|
def set_default_settings(self) -> None:
|
||||||
|
"""Sets the default settings of the spectrometer."""
|
||||||
|
self.default_settings.clear()
|
||||||
|
for category in self.settings.keys():
|
||||||
|
for setting in self.settings[category]:
|
||||||
|
setting_string = f"{self.module.model.name},{setting.name}"
|
||||||
|
self.default_settings.setValue(setting_string, setting.value)
|
||||||
|
logger.debug(f"Setting default value for {setting_string} to {setting.value}")
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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:
|
def add_setting(self, setting: Setting, category: str) -> None:
|
||||||
"""Adds a setting to the spectrometer.
|
"""Adds a setting to the spectrometer.
|
||||||
|
|
|
@ -8,6 +8,8 @@ from PyQt6.QtWidgets import (
|
||||||
QSizePolicy,
|
QSizePolicy,
|
||||||
QSpacerItem,
|
QSpacerItem,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
|
QPushButton,
|
||||||
|
QDialog,
|
||||||
)
|
)
|
||||||
from nqrduck.module.module_view import ModuleView
|
from nqrduck.module.module_view import ModuleView
|
||||||
from nqrduck.assets.icons import Logos
|
from nqrduck.assets.icons import Logos
|
||||||
|
@ -91,3 +93,108 @@ class BaseSpectrometerView(ModuleView):
|
||||||
|
|
||||||
# Push all the settings to the top of the widget
|
# Push all the settings to the top of the widget
|
||||||
self._ui_form.verticalLayout.addStretch(1)
|
self._ui_form.verticalLayout.addStretch(1)
|
||||||
|
|
||||||
|
# Now we add a save and load button to the widget
|
||||||
|
self.button_layout = QHBoxLayout()
|
||||||
|
|
||||||
|
# Default Settings Button
|
||||||
|
self.default_button = QPushButton("Default Settings")
|
||||||
|
self.default_button.clicked.connect(self.on_default_button_clicked)
|
||||||
|
self.button_layout.addWidget(self.default_button)
|
||||||
|
|
||||||
|
# Save Button
|
||||||
|
self.save_button = QPushButton("Save Settings")
|
||||||
|
self.save_button.setIcon(Logos.Save16x16())
|
||||||
|
self.save_button.setIconSize(Logos.Save16x16().availableSizes()[0])
|
||||||
|
self.save_button.clicked.connect(self.on_save_button_clicked)
|
||||||
|
self.button_layout.addWidget(self.save_button)
|
||||||
|
|
||||||
|
# Load Button
|
||||||
|
self.load_button = QPushButton("Load Settings")
|
||||||
|
self.load_button.setIcon(Logos.Load16x16())
|
||||||
|
self.load_button.clicked.connect(self.on_load_button_clicked)
|
||||||
|
self.button_layout.addWidget(self.load_button)
|
||||||
|
self.load_button.setIconSize(Logos.Load16x16().availableSizes()[0])
|
||||||
|
|
||||||
|
self.button_layout.addStretch(1)
|
||||||
|
|
||||||
|
self._ui_form.verticalLayout.addLayout(self.button_layout)
|
||||||
|
|
||||||
|
def on_save_button_clicked(self):
|
||||||
|
"""This method is called when the save button is clicked."""
|
||||||
|
logger.debug("Save button clicked")
|
||||||
|
# Open a dialog to save the settings to a file
|
||||||
|
file_manager = self.FileManager(
|
||||||
|
extension=self.module.model.SETTING_FILE_EXTENSION, parent=self
|
||||||
|
)
|
||||||
|
path = file_manager.saveFileDialog()
|
||||||
|
if path:
|
||||||
|
self.module.controller.save_settings(path)
|
||||||
|
|
||||||
|
def on_load_button_clicked(self):
|
||||||
|
"""This method is called when the load button is clicked."""
|
||||||
|
logger.debug("Load button clicked")
|
||||||
|
# Open a dialog to load the settings from a file
|
||||||
|
file_manager = self.FileManager(
|
||||||
|
extension=self.module.model.SETTING_FILE_EXTENSION, parent=self
|
||||||
|
)
|
||||||
|
path = file_manager.loadFileDialog()
|
||||||
|
self.module.controller.load_settings(path)
|
||||||
|
if path:
|
||||||
|
self.module.controller.load_settings(path)
|
||||||
|
|
||||||
|
def on_default_button_clicked(self):
|
||||||
|
"""This method is called when the default button is clicked."""
|
||||||
|
logger.debug("Default button clicked")
|
||||||
|
dialog = self.DefaultSettingsDialog(self)
|
||||||
|
dialog.exec()
|
||||||
|
|
||||||
|
class DefaultSettingsDialog(QDialog):
|
||||||
|
"""Dialog to set or clear the default settings of the spectrometer."""
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
"""Initializes the default settings dialog."""
|
||||||
|
super().__init__(parent)
|
||||||
|
self.parent = parent
|
||||||
|
self.setWindowTitle("Default Settings")
|
||||||
|
|
||||||
|
self.layout = QVBoxLayout()
|
||||||
|
|
||||||
|
# Either we set the current settings as default
|
||||||
|
self.set_current_button = QPushButton("Set Current Settings as Default")
|
||||||
|
self.set_current_button.clicked.connect(
|
||||||
|
self.on_set_current_button_clicked
|
||||||
|
)
|
||||||
|
|
||||||
|
# Or we clear the default settings
|
||||||
|
self.clear_button = QPushButton("Clear Default Settings")
|
||||||
|
self.clear_button.clicked.connect(
|
||||||
|
self.on_clear_button_clicked
|
||||||
|
)
|
||||||
|
|
||||||
|
self.layout.addWidget(self.set_current_button)
|
||||||
|
self.layout.addWidget(self.clear_button)
|
||||||
|
|
||||||
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
|
# Ok Button
|
||||||
|
self.ok_button = QPushButton("Ok")
|
||||||
|
self.ok_button.clicked.connect(self.accept)
|
||||||
|
self.layout.addWidget(self.ok_button)
|
||||||
|
|
||||||
|
def on_set_current_button_clicked(self):
|
||||||
|
"""This method is called when the set current button is clicked."""
|
||||||
|
logger.debug("Set current button clicked")
|
||||||
|
self.parent.module.model.set_default_settings()
|
||||||
|
# Show notification that the settings have been set as default
|
||||||
|
self.parent.module.nqrduck_signal.emit(
|
||||||
|
"notification", ["Info", "Settings have been set as default."]
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_clear_button_clicked(self):
|
||||||
|
"""This method is called when the clear button is clicked."""
|
||||||
|
logger.debug("Clear button clicked")
|
||||||
|
self.parent.module.model.clear_default_settings()
|
||||||
|
# Show notification that the default settings have been cleared
|
||||||
|
self.parent.module.nqrduck_signal.emit(
|
||||||
|
"notification", ["Info", "Default settings have been cleared."]
|
||||||
|
)
|
||||||
|
|
|
@ -43,6 +43,8 @@ class SpectrometerController(ModuleController):
|
||||||
logger.debug("Adding spectrometer to spectrometer model: %s", module_name)
|
logger.debug("Adding spectrometer to spectrometer model: %s", module_name)
|
||||||
self._module.model.add_spectrometers(module_name, module)
|
self._module.model.add_spectrometers(module_name, module)
|
||||||
|
|
||||||
|
module.controller.on_loading()
|
||||||
|
|
||||||
self._module.view.create_menu_entry()
|
self._module.view.create_menu_entry()
|
||||||
|
|
||||||
def process_signals(self, key: str, value: object) -> None:
|
def process_signals(self, key: str, value: object) -> None:
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
"""Class for handling measurement data."""
|
"""This module defines the measurement data structure and the fit class for measurement data."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from scipy.optimize import curve_fit
|
||||||
from nqrduck.helpers.signalprocessing import SignalProcessing as sp
|
from nqrduck.helpers.signalprocessing import SignalProcessing as sp
|
||||||
from nqrduck.helpers.functions import Function
|
from nqrduck.helpers.functions import Function
|
||||||
|
|
||||||
|
@ -28,8 +29,8 @@ class Measurement:
|
||||||
target_frequency (float): Target frequency of the measurement.
|
target_frequency (float): Target frequency of the measurement.
|
||||||
frequency_shift (float): Frequency shift of the measurement.
|
frequency_shift (float): Frequency shift of the measurement.
|
||||||
IF_frequency (float): Intermediate frequency of the measurement.
|
IF_frequency (float): Intermediate frequency of the measurement.
|
||||||
xf (np.array): Frequency axis for the x axis of the measurement data.
|
fdx (np.array): Frequency axis for the x axis of the measurement data.
|
||||||
yf (np.array): Frequency axis for the y axis of the measurement data.
|
fdy (np.array): Frequency axis for the y axis of the measurement data.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -46,69 +47,89 @@ class Measurement:
|
||||||
self.tdx = tdx
|
self.tdx = tdx
|
||||||
self.tdy = tdy
|
self.tdy = tdy
|
||||||
self.target_frequency = target_frequency
|
self.target_frequency = target_frequency
|
||||||
self.fdx, self.fdy = sp.fft(tdx, tdy, frequency_shift)
|
self.frequency_shift = frequency_shift
|
||||||
self.IF_frequency = IF_frequency
|
self.IF_frequency = IF_frequency
|
||||||
|
self.fdx, self.fdy = sp.fft(tdx, tdy, frequency_shift)
|
||||||
|
self.fits = []
|
||||||
|
|
||||||
def apodization(self, function: Function):
|
def apodization(self, function: Function) -> "Measurement":
|
||||||
"""Applies apodization to the measurement data.
|
"""Applies apodization to the measurement data.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
function (Function): Apodization function.
|
function (Function): Apodization function.
|
||||||
|
|
||||||
returns:
|
Returns:
|
||||||
Measurement : The apodized measurement.
|
Measurement: The apodized measurement.
|
||||||
"""
|
"""
|
||||||
# Get the y data weights from the function
|
|
||||||
duration = (self.tdx[-1] - self.tdx[0]) * 1e-6
|
duration = (self.tdx[-1] - self.tdx[0]) * 1e-6
|
||||||
|
|
||||||
resolution = duration / len(self.tdx)
|
resolution = duration / len(self.tdx)
|
||||||
|
|
||||||
logger.debug("Resolution: %s", resolution)
|
logger.debug("Resolution: %s", resolution)
|
||||||
|
|
||||||
y_weight = function.get_pulse_amplitude(duration, resolution)
|
y_weight = function.get_pulse_amplitude(duration, resolution)
|
||||||
|
tdy_apodized = self.tdy * y_weight
|
||||||
tdy_measurement = self.tdy * y_weight
|
|
||||||
|
|
||||||
apodized_measurement = Measurement(
|
apodized_measurement = Measurement(
|
||||||
self.name,
|
self.name,
|
||||||
self.tdx,
|
self.tdx,
|
||||||
tdy_measurement,
|
tdy_apodized,
|
||||||
target_frequency=self.target_frequency,
|
target_frequency=self.target_frequency,
|
||||||
IF_frequency=self.IF_frequency,
|
IF_frequency=self.IF_frequency,
|
||||||
)
|
)
|
||||||
|
|
||||||
return apodized_measurement
|
return apodized_measurement
|
||||||
|
|
||||||
# Data saving and loading
|
def add_fit(self, fit: "Fit") -> None:
|
||||||
|
"""Adds a fit to the measurement.
|
||||||
|
|
||||||
def to_json(self):
|
Args:
|
||||||
"""Converts the measurement to a json-compatible format.
|
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:
|
Returns:
|
||||||
dict : The measurement in json-compatible format.
|
dict: The measurement in JSON-compatible format.
|
||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"tdx": self.tdx.tolist(),
|
"tdx": self.tdx.tolist(),
|
||||||
"tdy": [
|
"tdy": [[x.real, x.imag] for x in self.tdy],
|
||||||
[x.real, x.imag] for x in self.tdy
|
|
||||||
], # Convert complex numbers to list
|
|
||||||
"target_frequency": self.target_frequency,
|
"target_frequency": self.target_frequency,
|
||||||
"IF_frequency": self.IF_frequency,
|
"IF_frequency": self.IF_frequency,
|
||||||
|
"fits": [fit.to_json() for fit in self.fits],
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_json(cls, json: dict):
|
def from_json(cls, json: dict) -> "Measurement":
|
||||||
"""Converts the json format to a measurement.
|
"""Converts the JSON format to a measurement.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
json (dict) : The measurement in json-compatible format.
|
json (dict): The measurement in JSON-compatible format.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Measurement : The measurement.
|
Measurement: The measurement.
|
||||||
"""
|
"""
|
||||||
tdy = np.array([complex(y[0], y[1]) for y in json["tdy"]])
|
tdy = np.array([complex(y[0], y[1]) for y in json["tdy"]])
|
||||||
return cls(
|
measurement = cls(
|
||||||
json["name"],
|
json["name"],
|
||||||
np.array(json["tdx"]),
|
np.array(json["tdx"]),
|
||||||
tdy,
|
tdy,
|
||||||
|
@ -116,58 +137,227 @@ class Measurement:
|
||||||
IF_frequency=json["IF_frequency"],
|
IF_frequency=json["IF_frequency"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Measurement data
|
for fit_json in json["fits"]:
|
||||||
|
measurement.add_fit(Fit.from_json(fit_json, measurement))
|
||||||
|
|
||||||
|
return measurement
|
||||||
|
|
||||||
|
# Properties for encapsulation
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self) -> str:
|
||||||
"""Name of the measurement."""
|
"""Name of the measurement."""
|
||||||
return self._name
|
return self._name
|
||||||
|
|
||||||
@name.setter
|
@name.setter
|
||||||
def name(self, value):
|
def name(self, value: str) -> None:
|
||||||
self._name = value
|
self._name = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tdx(self):
|
def tdx(self) -> np.array:
|
||||||
"""Time axis for the x axis of the measurement data."""
|
"""Time domain data for the measurement (x)."""
|
||||||
return self._tdx
|
return self._tdx
|
||||||
|
|
||||||
@tdx.setter
|
@tdx.setter
|
||||||
def tdx(self, value):
|
def tdx(self, value: np.array) -> None:
|
||||||
self._tdx = value
|
self._tdx = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tdy(self):
|
def tdy(self) -> np.array:
|
||||||
"""Time axis for the y axis of the measurement data."""
|
"""Time domain data for the measurement (y)."""
|
||||||
return self._tdy
|
return self._tdy
|
||||||
|
|
||||||
@tdy.setter
|
@tdy.setter
|
||||||
def tdy(self, value):
|
def tdy(self, value: np.array) -> None:
|
||||||
self._tdy = value
|
self._tdy = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fdx(self):
|
def fdx(self) -> np.array:
|
||||||
"""Frequency axis for the x axis of the measurement data."""
|
"""Frequency domain data for the measurement (x)."""
|
||||||
return self._fdx
|
return self._fdx
|
||||||
|
|
||||||
@fdx.setter
|
@fdx.setter
|
||||||
def fdx(self, value):
|
def fdx(self, value: np.array) -> None:
|
||||||
self._fdx = value
|
self._fdx = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fdy(self):
|
def fdy(self) -> np.array:
|
||||||
"""Frequency axis for the y axis of the measurement data."""
|
"""Frequency domain data for the measurement (y)."""
|
||||||
return self._fdy
|
return self._fdy
|
||||||
|
|
||||||
@fdy.setter
|
@fdy.setter
|
||||||
def fdy(self, value):
|
def fdy(self, value: np.array) -> None:
|
||||||
self._fdy = value
|
self._fdy = value
|
||||||
|
|
||||||
# Pulse parameters
|
|
||||||
@property
|
@property
|
||||||
def target_frequency(self):
|
def target_frequency(self) -> float:
|
||||||
"""Target frequency of the measurement."""
|
"""Target frequency of the measurement."""
|
||||||
return self._target_frequency
|
return self._target_frequency
|
||||||
|
|
||||||
@target_frequency.setter
|
@target_frequency.setter
|
||||||
def target_frequency(self, value):
|
def target_frequency(self, value: float) -> None:
|
||||||
self._target_frequency = value
|
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]
|
||||||
|
|
|
@ -36,6 +36,7 @@ class Setting(QObject):
|
||||||
description (str): A description of the setting.
|
description (str): A description of the setting.
|
||||||
default: The default value of the setting.
|
default: The default value of the setting.
|
||||||
"""
|
"""
|
||||||
|
self.widget = None
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.name = name
|
self.name = name
|
||||||
self.description = description
|
self.description = description
|
||||||
|
@ -87,9 +88,16 @@ class NumericalSetting(Setting):
|
||||||
|
|
||||||
It can additionally have a minimum and maximum 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:
|
|
||||||
|
def __init__(
|
||||||
|
self, name: str, description: str, default, min_value=None, max_value=None
|
||||||
|
) -> None:
|
||||||
"""Create a new numerical setting."""
|
"""Create a new numerical setting."""
|
||||||
super().__init__(name, self.description_limit_info(description, min_value, max_value), default)
|
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:
|
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.
|
"""Updates the description with the limits of the setting if there are any.
|
||||||
|
@ -103,11 +111,11 @@ class NumericalSetting(Setting):
|
||||||
str: The description of the setting with the limits.
|
str: The description of the setting with the limits.
|
||||||
"""
|
"""
|
||||||
if min_value is not None and max_value is not None:
|
if min_value is not None and max_value is not None:
|
||||||
description += (f"\n (min: {min_value}, max: {max_value})")
|
description += f"\n (min: {min_value}, max: {max_value})"
|
||||||
elif min_value is not None:
|
elif min_value is not None:
|
||||||
description += (f"\n (min: {min_value})")
|
description += f"\n (min: {min_value})"
|
||||||
elif max_value is not None:
|
elif max_value is not None:
|
||||||
description += (f"\n (max: {max_value})")
|
description += f"\n (max: {max_value})"
|
||||||
|
|
||||||
return description
|
return description
|
||||||
|
|
||||||
|
@ -133,13 +141,19 @@ class FloatSetting(NumericalSetting):
|
||||||
description: str,
|
description: str,
|
||||||
min_value: float = None,
|
min_value: float = None,
|
||||||
max_value: float = None,
|
max_value: float = None,
|
||||||
spin_box: tuple = (False, False)
|
spin_box: tuple = (False, False),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Create a new float setting."""
|
"""Create a new float setting."""
|
||||||
|
self.spin_box = spin_box
|
||||||
super().__init__(name, description, default, min_value, max_value)
|
super().__init__(name, description, default, min_value, max_value)
|
||||||
|
|
||||||
if spin_box[0]:
|
if spin_box[0]:
|
||||||
self.widget = DuckSpinBox(min_value=min_value, max_value=max_value, slider=spin_box[1], double_box=True)
|
self.widget = DuckSpinBox(
|
||||||
|
min_value=min_value,
|
||||||
|
max_value=max_value,
|
||||||
|
slider=spin_box[1],
|
||||||
|
double_box=True,
|
||||||
|
)
|
||||||
self.widget.spin_box.setValue(default)
|
self.widget.spin_box.setValue(default)
|
||||||
else:
|
else:
|
||||||
self.widget = DuckFloatEdit(min_value=min_value, max_value=max_value)
|
self.widget = DuckFloatEdit(min_value=min_value, max_value=max_value)
|
||||||
|
@ -169,6 +183,12 @@ class FloatSetting(NumericalSetting):
|
||||||
self._value = float(value)
|
self._value = float(value)
|
||||||
self.settings_changed.emit()
|
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):
|
class IntSetting(NumericalSetting):
|
||||||
"""A setting that is an Integer.
|
"""A setting that is an Integer.
|
||||||
|
@ -189,13 +209,15 @@ class IntSetting(NumericalSetting):
|
||||||
description: str,
|
description: str,
|
||||||
min_value=None,
|
min_value=None,
|
||||||
max_value=None,
|
max_value=None,
|
||||||
spin_box: tuple = (False, False)
|
spin_box: tuple = (False, False),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Create a new int setting."""
|
"""Create a new int setting."""
|
||||||
|
self.spin_box = spin_box
|
||||||
super().__init__(name, description, default, min_value, max_value)
|
super().__init__(name, description, default, min_value, max_value)
|
||||||
|
if self.spin_box[0]:
|
||||||
if spin_box[0]:
|
self.widget = DuckSpinBox(
|
||||||
self.widget = DuckSpinBox(min_value=min_value, max_value=max_value, slider=spin_box[1])
|
min_value=min_value, max_value=max_value, slider=spin_box[1]
|
||||||
|
)
|
||||||
self.widget.spin_box.setValue(default)
|
self.widget.spin_box.setValue(default)
|
||||||
else:
|
else:
|
||||||
self.widget = DuckIntEdit(min_value=min_value, max_value=max_value)
|
self.widget = DuckIntEdit(min_value=min_value, max_value=max_value)
|
||||||
|
@ -225,7 +247,11 @@ class IntSetting(NumericalSetting):
|
||||||
value = int(float(value))
|
value = int(float(value))
|
||||||
self._value = value
|
self._value = value
|
||||||
self.settings_changed.emit()
|
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):
|
class BooleanSetting(Setting):
|
||||||
|
@ -253,6 +279,8 @@ class BooleanSetting(Setting):
|
||||||
def value(self, value):
|
def value(self, value):
|
||||||
try:
|
try:
|
||||||
self._value = bool(value)
|
self._value = bool(value)
|
||||||
|
if self.widget:
|
||||||
|
self.widget.setChecked(self._value)
|
||||||
self.settings_changed.emit()
|
self.settings_changed.emit()
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValueError("Value must be a bool")
|
raise ValueError("Value must be a bool")
|
||||||
|
@ -307,6 +335,8 @@ class SelectionSetting(Setting):
|
||||||
try:
|
try:
|
||||||
if value in self.options:
|
if value in self.options:
|
||||||
self._value = value
|
self._value = value
|
||||||
|
if self.widget:
|
||||||
|
self.widget.setCurrentText(value)
|
||||||
self.settings_changed.emit()
|
self.settings_changed.emit()
|
||||||
else:
|
else:
|
||||||
raise ValueError("Value must be one of the options")
|
raise ValueError("Value must be one of the options")
|
||||||
|
|
Loading…
Reference in a new issue