Implemented basic fitting.

This commit is contained in:
jupfi 2024-05-22 17:51:41 +02:00
parent 6124ee80fd
commit 5a2c2790cc
4 changed files with 142 additions and 7 deletions

View file

@ -5,12 +5,15 @@ A module for the [nqrduck](https://github.com/nqrduck/nqrduck) project. This mod
## Installation ## Installation
### Requirements ### Requirements
Dependencies are handled via the pyproject.toml file. Dependencies are handled via the pyproject.toml file.
### Setup ### Setup
To install the module you need the NQRduck core. You can find the installation instructions for the NQRduck core [here](https://github.com/nqrduck/nqrduck). To install the module you need the NQRduck core. You can find the installation instructions for the NQRduck core [here](https://github.com/nqrduck/nqrduck).
Ideally you should install the module in a virtual environment. You can create a virtual environment by running the following command in the terminal: Ideally you should install the module in a virtual environment. You can create a virtual environment by running the following command in the terminal:
```bash ```bash
python -m venv nqrduck python -m venv nqrduck
# Activate the virtual environment # Activate the virtual environment
@ -18,22 +21,25 @@ python -m venv nqrduck
``` ```
You can install this module and the dependencies by running the following command in the terminal while the virtual environment is activated and you are in the root directory of this module: You can install this module and the dependencies by running the following command in the terminal while the virtual environment is activated and you are in the root directory of this module:
```bash ```bash
pip install . pip install .
``` ```
Alternatively, you can install the module and the dependencies by running the following command in the terminal while the virtual environment is activated: Alternatively, you can install the module and the dependencies by running the following command in the terminal while the virtual environment is activated:
```bash ```bash
pip install nqrduck-measurement pip install nqrduck-measurement
``` ```
## Usage ## Usage
The module is used with the [Spectrometer](https://github.com/nqrduck/nqrduck-spectrometer) module. However you need to use an actual submodule of the spectrometer module like: The module is used with the [Spectrometer](https://github.com/nqrduck/nqrduck-spectrometer) module. However you need to use an actual submodule of the spectrometer module like:
- [nqrduck-spectrometer-limenqr](https://github.com/nqrduck/nqrduck-spectrometer-limenqr) A module used for magnetic resonance experiments with the LimeSDR (USB or Mini 2.0). - [nqrduck-spectrometer-limenqr](https://github.com/nqrduck/nqrduck-spectrometer-limenqr) A module used for magnetic resonance experiments with the LimeSDR (USB or Mini 2.0).
- [nqrduck-spectrometer-simulator](https://github.com/nqrduck/nqrduck-spectrometer-simulator) A module used for simulating magnetic resonance experiments. - [nqrduck-spectrometer-simulator](https://github.com/nqrduck/nqrduck-spectrometer-simulator) A module used for simulating magnetic resonance experiments.
The pulse sequence and spectrometer settings can be adjusted using the 'Spectrometer' tab. The pulse sequence and spectrometer settings can be adjusted using the 'Spectrometer' tab.
<img src="https://github.com/nqrduck/nqrduck-measurement/raw/0b28ae6b33230c6ca9eda85bd18de7cbcade27d1/docs/img/measurement_ui_labeled_v2.png" alt="drawing" width="800"> <img src="https://github.com/nqrduck/nqrduck-measurement/raw/0b28ae6b33230c6ca9eda85bd18de7cbcade27d1/docs/img/measurement_ui_labeled_v2.png" alt="drawing" width="800">
@ -42,8 +48,12 @@ The pulse sequence and spectrometer settings can be adjusted using the 'Spectrom
- c.) The 'Measurement Plot'. Here the measured data is displayed. One can switch time and frequency domain plots. - c.) The 'Measurement Plot'. Here the measured data is displayed. One can switch time and frequency domain plots.
- d.) The import and export buttons for the measurement data. - d.) The import and export buttons for the measurement data.
You can then remove the folder of the virtual environment.
## License ## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details
## Contributing ## Contributing
If you're interested in contributing to the project, start by checking out our [nqrduck-module template](https://github.com/nqrduck/nqrduck-module). To contribute to existing modules, please first open an issue in the respective module repository to discuss your ideas or report bugs. If you're interested in contributing to the project, start by checking out our [nqrduck-module template](https://github.com/nqrduck/nqrduck-module). To contribute to existing modules, please first open an issue in the respective module repository to discuss your ideas or report bugs.

View file

@ -4,7 +4,7 @@ import logging
import json import json
from PyQt6.QtCore import pyqtSlot, pyqtSignal from PyQt6.QtCore import pyqtSlot, pyqtSignal
from PyQt6.QtWidgets import QApplication from PyQt6.QtWidgets import QApplication
from .signalprocessing_options import Apodization from .signalprocessing_options import Apodization, Fitting
from nqrduck.module.module_controller import ModuleController from nqrduck.module.module_controller import ModuleController
from nqrduck_spectrometer.measurement import Measurement from nqrduck_spectrometer.measurement import Measurement
@ -231,6 +231,38 @@ class MeasurementController(ModuleController):
self.module.model.displayed_measurement = apodized_measurement self.module.model.displayed_measurement = apodized_measurement
self.module.model.add_measurement(apodized_measurement) self.module.model.add_measurement(apodized_measurement)
def show_fitting_dialog(self) -> None:
"""Show fitting dialog."""
logger.debug("Showing fitting dialog.")
# First we check if there is a measurement.
if not self.module.model.displayed_measurement:
logger.debug("No measurement to fit.")
self.module.nqrduck_signal.emit(
"notification", ["Error", "No measurement to fit."]
)
return
measurement = self.module.model.displayed_measurement
dialog = Fitting(measurement, parent=self.module.view)
result = dialog.exec()
logger.debug("Dialog result: %s", result)
if not result:
return
fit = dialog.get_fit()[1]
logger.debug("Fitting function: %s", fit)
params = fit.fit()
measurement.add_fit(fit)
self.module.view.update_displayed_measurement()
dialog.deleteLater()
@pyqtSlot() @pyqtSlot()
def change_displayed_measurement(self, measurement=None) -> None: def change_displayed_measurement(self, measurement=None) -> None:

View file

@ -2,9 +2,14 @@
import logging import logging
import sympy import sympy
from nqrduck_spectrometer.measurement import Measurement from nqrduck_spectrometer.measurement import Measurement, Fit, T2StarFit
from nqrduck.helpers.functions import Function, GaussianFunction, CustomFunction from nqrduck.helpers.functions import Function, GaussianFunction, CustomFunction
from nqrduck.helpers.formbuilder import DuckFormBuilder, DuckFormFunctionSelectionField from nqrduck.helpers.formbuilder import (
DuckFormBuilder,
DuckFormFunctionSelectionField,
DuckFormDropdownField,
DuckLabelField,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -24,6 +29,21 @@ class FIDFunction(Function):
self.add_parameter(Function.Parameter("T2star (microseconds)", "T2star", 10)) self.add_parameter(Function.Parameter("T2star (microseconds)", "T2star", 10))
class LorentzianFunction(Function):
"""The Lorentzian function."""
name = "Lorentzian"
def __init__(self) -> None:
"""Lorentzian function."""
expr = sympy.sympify("1 / (1 + (x / T2star)^2)")
super().__init__(expr)
self.start_x = 0
self.end_x = 30
self.add_parameter(Function.Parameter("T2star (microseconds)", "T2star", 10))
class Apodization(DuckFormBuilder): class Apodization(DuckFormBuilder):
"""Apodization parameter. """Apodization parameter.
@ -62,3 +82,37 @@ class Apodization(DuckFormBuilder):
Function: The selected function. Function: The selected function.
""" """
return self.get_values()[0] return self.get_values()[0]
class Fitting(DuckFormBuilder):
"""Fitting parameter.
This parameter is used to apply fitting functions to the signal.
The fitting functions are used to reduce the noise in the signal.
"""
def __init__(self, measurement: Measurement, parent=None) -> None:
"""Fitting parameter."""
super().__init__("Fitting", parent=parent)
self.measurement = measurement
fits = {}
fits["T2*"] = T2StarFit(self.measurement)
selection_field = DuckFormDropdownField(
text=None,
tooltip=None,
options=fits,
default_option=0,
)
self.add_field(selection_field)
def get_fit(self) -> Fit:
"""Get the selected fit.
Returns:
Fit: The selected fit.
"""
return self.get_values()[0]

View file

@ -89,6 +89,10 @@ class MeasurementView(ModuleView):
self.module.controller.show_apodization_dialog self.module.controller.show_apodization_dialog
) )
self._ui_form.fittingButton.clicked.connect(
self.module.controller.show_fitting_dialog
)
# Add logos # Add logos
self._ui_form.buttonStart.setIcon(Logos.Play_16x16()) self._ui_form.buttonStart.setIcon(Logos.Play_16x16())
self._ui_form.buttonStart.setIconSize(self._ui_form.buttonStart.size()) self._ui_form.buttonStart.setIconSize(self._ui_form.buttonStart.size())
@ -120,7 +124,7 @@ class MeasurementView(ModuleView):
self._ui_form.averagesEdit.set_min_value(1) self._ui_form.averagesEdit.set_min_value(1)
self._ui_form.averagesEdit.set_max_value(1e6) self._ui_form.averagesEdit.set_max_value(1e6)
# Connect selectionBox signal fors switching the displayed measurement # Connect selectionBox signal for switching the displayed measurement
self._ui_form.selectionBox.valueChanged.connect( self._ui_form.selectionBox.valueChanged.connect(
self.module.controller.change_displayed_measurement self.module.controller.change_displayed_measurement
) )
@ -207,6 +211,9 @@ class MeasurementView(ModuleView):
x, np.abs(y), label="Magnitude", color="blue" x, np.abs(y), label="Magnitude", color="blue"
) )
# Plot fits
self.plot_fits()
# Add legend # Add legend
self._ui_form.plotter.canvas.ax.legend() self._ui_form.plotter.canvas.ax.legend()
@ -230,10 +237,42 @@ class MeasurementView(ModuleView):
) )
break break
except AttributeError: except AttributeError as e:
logger.debug("No measurement data to display.") logger.debug(f"No measurement data to display: {e}")
self._ui_form.plotter.canvas.draw() self._ui_form.plotter.canvas.draw()
def plot_fits(self):
"""Plots the according fits to the displayed measurement if there are any and if the view mode is correct."""
measurement = self.module.model.displayed_measurement
if not measurement.fits:
logger.debug("No fits to plot.")
return
for fit in measurement.fits:
logger.debug(f"Plotting fit {fit.name}.")
if fit.domain == self.module.model.view_mode:
x = fit.x
y = fit.y
self._ui_form.plotter.canvas.ax.plot(x, y, label=f"{fit.name} Fit", color="black", linestyle="--")
# Add the parameters to the plot
offset = 0
for name, value in fit.parameters.items():
if name == "covariance":
continue
# Only two digits after the comma
value = round(value, 2)
self._ui_form.plotter.canvas.ax.text(
max(x) / 90,
max(y)/2 + offset,
f"{name}: {value}",
)
offset += max(y)/10
@pyqtSlot() @pyqtSlot()
def on_measurement_start_button_clicked(self) -> None: def on_measurement_start_button_clicked(self) -> None:
"""Slot for when the measurement start button is clicked.""" """Slot for when the measurement start button is clicked."""