From 60ba648958ab41e0cb18bcc224b90bfb05a7ae00 Mon Sep 17 00:00:00 2001 From: jupfi Date: Wed, 22 May 2024 17:51:58 +0200 Subject: [PATCH 01/12] Implemented basic fitting. --- src/nqrduck_spectrometer/measurement.py | 151 ++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/src/nqrduck_spectrometer/measurement.py b/src/nqrduck_spectrometer/measurement.py index a0d59dc..5923a6b 100644 --- a/src/nqrduck_spectrometer/measurement.py +++ b/src/nqrduck_spectrometer/measurement.py @@ -2,6 +2,8 @@ import logging import numpy as np +from scipy.optimize import curve_fit +from sympy.utilities.lambdify import lambdify from nqrduck.helpers.signalprocessing import SignalProcessing as sp from nqrduck.helpers.functions import Function @@ -49,6 +51,8 @@ class Measurement: self.fdx, self.fdy = sp.fft(tdx, tdy, frequency_shift) self.IF_frequency = IF_frequency + self.fits = [] + def apodization(self, function: Function): """Applies apodization to the measurement data. @@ -79,6 +83,14 @@ class Measurement: return apodized_measurement + def add_fit(self, fit): + """Adds a fit to the measurement. + + Args: + fit (Fit): The fit to add. + """ + self.fits.append(fit) + # Data saving and loading def to_json(self): @@ -171,3 +183,142 @@ class Measurement: @target_frequency.setter def target_frequency(self, value): self._target_frequency = value + + @property + def fits(self): + """Fits of the measurement.""" + return self._fits + + @fits.setter + def fits(self, value): + 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. + + Examples for fits in time domain would be the T2* relaxation time, while in frequency domain it could be the line width. + + A fit has a name, a nqrduck function and a strategy for the algorithm to use. + """ + + def __init__(self, name: str, domain: str, measurement : Measurement) -> None: + """Initializes the fit.""" + self.name = name + self.domain = domain + self.measurement = measurement + + def fit(self): + """Fits the measurement data, sets the x and y data and returns 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() + parameters, covariance = curve_fit(self.fit_function, x, abs(y), p0=initial_guess) + + self.x = x + self.y = self.fit_function(x, *parameters) + + + return parameters, covariance + + def get_fit_parameters_string(self): + """Get the fit parameters as a string. + + Returns: + str : The fit parameters as a string. + """ + return " ".join([f"{param:.2f}" for param in self.parameters]) + + def fit_function(self, x, *parameters): + """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): + """Initial guess for the fit. + + Returns: + list : The initial guess. + """ + raise NotImplementedError + + + def to_json(self): + """Converts the fit to a json-compatible format. + + Returns: + dict : The fit in json-compatible format. + """ + return { + "name": self.name, + "function": self.function.to_json(), + "strategy": self.strategy, + } + + @classmethod + def from_json(cls, json: dict): + """Converts the json format to a fit. + + Args: + json (dict) : The fit in json-compatible format. + + Returns: + Fit : The fit. + """ + function = Function.from_json(json["function"]) + return cls(json["name"], function, json["strategy"]) + + @property + def x(self): + """The x data of the fit.""" + return self._x + + @x.setter + def x(self, value): + self._x = value + + @property + def y(self): + """The y data of the fit.""" + return self._y + + @y.setter + def y(self, value): + self._y = value + +class T2StarFit(Fit): + + def __init__(self, measurement: Measurement) -> None: + self.name = "T2*" + self.domain = "time" + self.measurement = measurement + + def fit(self): + parameters, covariance = super().fit() + # Create dict with fit parameters and covariance + self.parameters = { + "S0": parameters[0], + "T2Star": parameters[1], + "covariance": covariance + } + + def fit_function (self, t, S0, T2Star): + return S0 * np.exp(-t / T2Star) + + def initial_guess(self): + return [1, 1] \ No newline at end of file From a273bed28a647b8d50c8264fe65f66259658d52c Mon Sep 17 00:00:00 2001 From: jupfi Date: Thu, 23 May 2024 15:56:43 +0200 Subject: [PATCH 02/12] Added methods for editing fit information. --- src/nqrduck_spectrometer/measurement.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/nqrduck_spectrometer/measurement.py b/src/nqrduck_spectrometer/measurement.py index 5923a6b..d320da1 100644 --- a/src/nqrduck_spectrometer/measurement.py +++ b/src/nqrduck_spectrometer/measurement.py @@ -91,6 +91,25 @@ class Measurement: """ self.fits.append(fit) + def delete_fit(self, fit): + """Deletes a fit from the measurement. + + Args: + fit (Fit): The fit to delete. + """ + + self.fits.remove(fit) + + def edit_fit_name(self, fit, name : str): + """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 + # Data saving and loading def to_json(self): @@ -107,6 +126,7 @@ class Measurement: ], # Convert complex numbers to list "target_frequency": self.target_frequency, "IF_frequency": self.IF_frequency, + "fits": [fit.to_json() for fit in self.fits], } @classmethod @@ -126,6 +146,7 @@ class Measurement: tdy, target_frequency=json["target_frequency"], IF_frequency=json["IF_frequency"], + fits=[Fit.from_json(fit) for fit in json["fits"]], ) # Measurement data From f5b6f3a689d0ca8cece6b2e0c05a322f832e7ccb Mon Sep 17 00:00:00 2001 From: jupfi Date: Thu, 23 May 2024 16:28:39 +0200 Subject: [PATCH 03/12] Implemented loading and saving of fits. --- src/nqrduck_spectrometer/measurement.py | 58 ++++++++++++++++--------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/src/nqrduck_spectrometer/measurement.py b/src/nqrduck_spectrometer/measurement.py index d320da1..d473d28 100644 --- a/src/nqrduck_spectrometer/measurement.py +++ b/src/nqrduck_spectrometer/measurement.py @@ -140,15 +140,20 @@ class Measurement: Measurement : The measurement. """ tdy = np.array([complex(y[0], y[1]) for y in json["tdy"]]) - return cls( + obj = cls( json["name"], np.array(json["tdx"]), tdy, target_frequency=json["target_frequency"], IF_frequency=json["IF_frequency"], - fits=[Fit.from_json(fit) for fit in json["fits"]], ) + # Add fits + for fit in json["fits"]: + obj.add_fit(Fit.from_json(fit, obj)) + + return obj + # Measurement data @property def name(self): @@ -223,6 +228,12 @@ class Fit(): A fit has a name, a nqrduck function and a strategy for the algorithm to use. """ + 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, domain: str, measurement : Measurement) -> None: """Initializes the fit.""" @@ -230,8 +241,10 @@ class Fit(): self.domain = domain self.measurement = measurement + self.fit() + def fit(self): - """Fits the measurement data, sets the x and y data and returns the fit parameters and covariance. """ + """Fits the measurement data, sets the x and y data and sets the fit parameters and covariance. """ if self.domain == "time": x = self.measurement.tdx y = self.measurement.tdy @@ -246,9 +259,9 @@ class Fit(): self.x = x self.y = self.fit_function(x, *parameters) - - - return parameters, covariance + + self.parameters = parameters + self.covariance = covariance def get_fit_parameters_string(self): """Get the fit parameters as a string. @@ -287,22 +300,27 @@ class Fit(): """ return { "name": self.name, - "function": self.function.to_json(), - "strategy": self.strategy, + "class": self.__class__.__name__, } @classmethod - def from_json(cls, json: dict): + def from_json(cls, data: dict, measurement : Measurement): """Converts the json format to a fit. Args: - json (dict) : The fit in json-compatible format. + data (dict) : The fit in json-compatible format. + measurement (Measurement) : The measurement. Returns: Fit : The fit. """ - function = Function.from_json(json["function"]) - return cls(json["name"], function, json["strategy"]) + for subclass in cls.subclasses: + logger.debug(f"Keys data: {data.keys()}") + if subclass.__name__ == data["class"]: + cls = subclass + break + + return cls(measurement, data["name"]) @property def x(self): @@ -324,18 +342,18 @@ class Fit(): class T2StarFit(Fit): - def __init__(self, measurement: Measurement) -> None: - self.name = "T2*" - self.domain = "time" - self.measurement = measurement + def __init__(self, measurement: Measurement, name = "T2*") -> None: + domain = "time" + measurement = measurement + super().__init__(name, domain, measurement) def fit(self): - parameters, covariance = super().fit() + super().fit() # Create dict with fit parameters and covariance self.parameters = { - "S0": parameters[0], - "T2Star": parameters[1], - "covariance": covariance + "S0": self.parameters[0], + "T2Star": self.parameters[1], + "covariance": self.covariance } def fit_function (self, t, S0, T2Star): From 419116aff956e3cbb007341fd09ff18b7897e2d3 Mon Sep 17 00:00:00 2001 From: jupfi Date: Thu, 23 May 2024 16:39:32 +0200 Subject: [PATCH 04/12] Refactoring and Linting. --- src/nqrduck_spectrometer/measurement.py | 229 +++++++++++------------- 1 file changed, 102 insertions(+), 127 deletions(-) diff --git a/src/nqrduck_spectrometer/measurement.py b/src/nqrduck_spectrometer/measurement.py index d473d28..39c32d3 100644 --- a/src/nqrduck_spectrometer/measurement.py +++ b/src/nqrduck_spectrometer/measurement.py @@ -1,9 +1,8 @@ -"""Class for handling measurement data.""" +"""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 sympy.utilities.lambdify import lambdify from nqrduck.helpers.signalprocessing import SignalProcessing as sp from nqrduck.helpers.functions import Function @@ -30,8 +29,8 @@ class Measurement: target_frequency (float): Target frequency of the measurement. frequency_shift (float): Frequency shift of the measurement. IF_frequency (float): Intermediate frequency of the measurement. - xf (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. + 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__( @@ -48,42 +47,37 @@ class Measurement: self.tdx = tdx self.tdy = tdy 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.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. Args: function (Function): Apodization function. - returns: - Measurement : The apodized measurement. + Returns: + Measurement: The apodized measurement. """ - # Get the y data weights from the function 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_measurement = self.tdy * y_weight + tdy_apodized = self.tdy * y_weight apodized_measurement = Measurement( self.name, self.tdx, - tdy_measurement, + tdy_apodized, target_frequency=self.target_frequency, IF_frequency=self.IF_frequency, ) - return apodized_measurement - def add_fit(self, fit): + def add_fit(self, fit: "Fit") -> None: """Adds a fit to the measurement. Args: @@ -91,16 +85,15 @@ class Measurement: """ self.fits.append(fit) - def delete_fit(self, 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, name : str): + def edit_fit_name(self, fit: "Fit", name: str) -> None: """Edits the name of a fit. Args: @@ -110,37 +103,33 @@ class Measurement: logger.debug(f"Editing fit name to {name}.") fit.name = name - # Data saving and loading - - def to_json(self): - """Converts the measurement to a json-compatible format. + def to_json(self) -> dict: + """Converts the measurement to a JSON-compatible format. Returns: - dict : The measurement in json-compatible format. + 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 - ], # Convert complex numbers to list + "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): - """Converts the json format to a measurement. + def from_json(cls, json: dict) -> "Measurement": + """Converts the JSON format to a measurement. Args: - json (dict) : The measurement in json-compatible format. + json (dict): The measurement in JSON-compatible format. Returns: - Measurement : The measurement. + Measurement: The measurement. """ tdy = np.array([complex(y[0], y[1]) for y in json["tdy"]]) - obj = cls( + measurement = cls( json["name"], np.array(json["tdx"]), tdy, @@ -148,103 +137,98 @@ class Measurement: IF_frequency=json["IF_frequency"], ) - # Add fits - for fit in json["fits"]: - obj.add_fit(Fit.from_json(fit, obj)) + for fit_json in json["fits"]: + measurement.add_fit(Fit.from_json(fit_json, measurement)) - return obj + return measurement - # Measurement data + # Properties for encapsulation @property - def name(self): + def name(self) -> str: """Name of the measurement.""" return self._name - + @name.setter - def name(self, value): + def name(self, value: str) -> None: self._name = value @property - def tdx(self): - """Time axis for the x axis of the measurement data.""" + def tdx(self) -> np.array: + """Time domain data for the measurement (x).""" return self._tdx @tdx.setter - def tdx(self, value): + def tdx(self, value: np.array) -> None: self._tdx = value @property - def tdy(self): - """Time axis for the y axis of the measurement data.""" + def tdy(self) -> np.array: + """Time domain data for the measurement (y).""" return self._tdy @tdy.setter - def tdy(self, value): + def tdy(self, value: np.array) -> None: self._tdy = value @property - def fdx(self): - """Frequency axis for the x axis of the measurement data.""" + def fdx(self) -> np.array: + """Frequency domain data for the measurement (x).""" return self._fdx @fdx.setter - def fdx(self, value): + def fdx(self, value: np.array) -> None: self._fdx = value @property - def fdy(self): - """Frequency axis for the y axis of the measurement data.""" + def fdy(self) -> np.array: + """Frequency domain data for the measurement (y).""" return self._fdy @fdy.setter - def fdy(self, value): + def fdy(self, value: np.array) -> None: self._fdy = value - # Pulse parameters @property - def target_frequency(self): + def target_frequency(self) -> float: """Target frequency of the measurement.""" return self._target_frequency @target_frequency.setter - def target_frequency(self, value): + def target_frequency(self, value: float) -> None: self._target_frequency = value @property - def fits(self): + def fits(self) -> list: """Fits of the measurement.""" return self._fits - + @fits.setter - def fits(self, value): + def fits(self, value: list) -> None: self._fits = value -class Fit(): + +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. - - Examples for fits in time domain would be the T2* relaxation time, while in frequency domain it could be the line width. - - A fit has a name, a nqrduck function and a strategy for the algorithm to use. """ + subclasses = [] - def __init_subclass__(cls, **kwargs): + 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: + 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): - """Fits the measurement data, sets the x and y data and sets the fit parameters and covariance. """ + 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 @@ -255,109 +239,100 @@ class Fit(): raise ValueError("Domain not recognized.") initial_guess = self.initial_guess() - parameters, covariance = curve_fit(self.fit_function, x, abs(y), p0=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, *parameters) - - self.parameters = parameters - self.covariance = covariance - - def get_fit_parameters_string(self): - """Get the fit parameters as a string. + self.y = self.fit_function(x, *self.parameters) - Returns: - str : The fit parameters as a string. - """ - return " ".join([f"{param:.2f}" for param in self.parameters]) - - def fit_function(self, x, *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. + *parameters: The fit parameters. Returns: - np.array : The y data. + np.array: The y data. """ raise NotImplementedError - - def initial_guess(self): + + def initial_guess(self) -> list: """Initial guess for the fit. Returns: - list : The initial guess. + list: The initial guess. """ raise NotImplementedError - - def to_json(self): - """Converts the fit to a json-compatible format. + def to_json(self) -> dict: + """Converts the fit to a JSON-compatible format. Returns: - dict : The fit in json-compatible format. + dict: The fit in JSON-compatible format. """ return { "name": self.name, "class": self.__class__.__name__, } - + @classmethod - def from_json(cls, data: dict, measurement : Measurement): - """Converts the json format to a fit. + 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. + data (dict): The fit in JSON-compatible format. + measurement (Measurement): The measurement. Returns: - Fit : The fit. + Fit: The fit. """ for subclass in cls.subclasses: - logger.debug(f"Keys data: {data.keys()}") if subclass.__name__ == data["class"]: - cls = subclass - break + return subclass(name=data["name"], measurement=measurement) + + raise ValueError(f"Subclass {data['class']} not found.") - return cls(measurement, data["name"]) - @property - def x(self): + def x(self) -> np.array: """The x data of the fit.""" return self._x - + @x.setter - def x(self, value): + def x(self, value: np.array) -> None: self._x = value @property - def y(self): + def y(self) -> np.array: """The y data of the fit.""" return self._y - - @y.setter - def y(self, value): - self._y = value - -class T2StarFit(Fit): - def __init__(self, measurement: Measurement, name = "T2*") -> None: - domain = "time" - measurement = measurement - super().__init__(name, domain, measurement) - - def fit(self): + @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() - # Create dict with fit parameters and covariance self.parameters = { "S0": self.parameters[0], "T2Star": self.parameters[1], - "covariance": self.covariance + "covariance": self.covariance, } - def fit_function (self, t, S0, T2Star): + 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): - return [1, 1] \ No newline at end of file + + def initial_guess(self) -> list: + """Initial guess for the T2* fit.""" + return [1, 1] From d43639c2f9796d5055dd01bd2f36bae43877bfe8 Mon Sep 17 00:00:00 2001 From: jupfi Date: Thu, 23 May 2024 17:05:13 +0200 Subject: [PATCH 05/12] Implemented lorentzian fit. --- src/nqrduck_spectrometer/measurement.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/nqrduck_spectrometer/measurement.py b/src/nqrduck_spectrometer/measurement.py index 39c32d3..61d04d1 100644 --- a/src/nqrduck_spectrometer/measurement.py +++ b/src/nqrduck_spectrometer/measurement.py @@ -336,3 +336,28 @@ class T2StarFit(Fit): 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] From 6496ec6824da33fa41a38e26fdcac9a03ceb51fb Mon Sep 17 00:00:00 2001 From: jupfi Date: Fri, 24 May 2024 17:28:57 +0200 Subject: [PATCH 06/12] Implemented loading and saving of settings. --- .../base_spectrometer_controller.py | 42 ++++++++++++ .../base_spectrometer_model.py | 2 + .../base_spectrometer_view.py | 43 ++++++++++++- src/nqrduck_spectrometer/settings.py | 64 ++++++++++++++----- 4 files changed, 133 insertions(+), 18 deletions(-) diff --git a/src/nqrduck_spectrometer/base_spectrometer_controller.py b/src/nqrduck_spectrometer/base_spectrometer_controller.py index a96e368..6eb0f9a 100644 --- a/src/nqrduck_spectrometer/base_spectrometer_controller.py +++ b/src/nqrduck_spectrometer/base_spectrometer_controller.py @@ -1,5 +1,6 @@ """Base class for all spectrometer controllers.""" +import ast from nqrduck.module.module_controller import ModuleController @@ -10,6 +11,47 @@ class BaseSpectrometerController(ModuleController): """Initializes the spectrometer controller.""" super().__init__(module) + 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, "r") 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): """Starts the measurement. diff --git a/src/nqrduck_spectrometer/base_spectrometer_model.py b/src/nqrduck_spectrometer/base_spectrometer_model.py index 8e5e1dd..688ddee 100644 --- a/src/nqrduck_spectrometer/base_spectrometer_model.py +++ b/src/nqrduck_spectrometer/base_spectrometer_model.py @@ -22,6 +22,8 @@ class BaseSpectrometerModel(ModuleModel): pulse_parameter_options (OrderedDict) : The pulse parameter options of the spectrometer """ + SETTING_FILE_EXTENSION = "setduck" + settings: OrderedDict pulse_parameter_options: OrderedDict diff --git a/src/nqrduck_spectrometer/base_spectrometer_view.py b/src/nqrduck_spectrometer/base_spectrometer_view.py index 1b53600..e09b3bc 100644 --- a/src/nqrduck_spectrometer/base_spectrometer_view.py +++ b/src/nqrduck_spectrometer/base_spectrometer_view.py @@ -8,6 +8,7 @@ from PyQt6.QtWidgets import ( QSizePolicy, QSpacerItem, QVBoxLayout, + QPushButton, ) from nqrduck.module.module_view import ModuleView from nqrduck.assets.icons import Logos @@ -82,7 +83,7 @@ class BaseSpectrometerView(ModuleView): layout.addWidget(edit_widget) layout.addStretch(1) layout.addWidget(icon_label) - + # Add the layout to the vertical layout of the widget category_layout.addLayout(layout) @@ -91,3 +92,43 @@ class BaseSpectrometerView(ModuleView): # Push all the settings to the top of the widget self._ui_form.verticalLayout.addStretch(1) + + # Now we add a save and load button to the widget + self.button_layout = QHBoxLayout() + self.save_button = QPushButton("Save Settings") + self.save_button.setIcon(Logos.Save16x16()) + #self.save_button.setIconSize(self.save_button.size()) + self.save_button.clicked.connect(self.on_save_button_clicked) + self.button_layout.addWidget(self.save_button) + + self.load_button = QPushButton("Load Settings") + self.load_button.setIcon(Logos.Load16x16()) + #self.load_button.setIconSize(self.load_button.size()) + self.load_button.clicked.connect(self.on_load_button_clicked) + self.button_layout.addWidget(self.load_button) + + 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) diff --git a/src/nqrduck_spectrometer/settings.py b/src/nqrduck_spectrometer/settings.py index 7187f1d..4f39529 100644 --- a/src/nqrduck_spectrometer/settings.py +++ b/src/nqrduck_spectrometer/settings.py @@ -36,6 +36,7 @@ class Setting(QObject): 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 @@ -81,34 +82,41 @@ class Setting(QObject): 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: + + 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) + 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})") + description += f"\n (min: {min_value}, max: {max_value})" elif min_value is not None: - description += (f"\n (min: {min_value})") + description += f"\n (min: {min_value})" elif max_value is not None: - description += (f"\n (max: {max_value})") - + description += f"\n (max: {max_value})" + return description @@ -121,7 +129,7 @@ class FloatSetting(NumericalSetting): 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. + 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 @@ -133,13 +141,19 @@ class FloatSetting(NumericalSetting): description: str, min_value: float = None, max_value: float = None, - spin_box: tuple = (False, False) + 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 = 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) @@ -169,6 +183,12 @@ class FloatSetting(NumericalSetting): 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. @@ -189,13 +209,15 @@ class IntSetting(NumericalSetting): description: str, min_value=None, max_value=None, - spin_box: tuple = (False, False) + 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 spin_box[0]: - self.widget = DuckSpinBox(min_value=min_value, max_value=max_value, slider=spin_box[1]) + 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) @@ -225,7 +247,11 @@ class IntSetting(NumericalSetting): 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): @@ -253,6 +279,8 @@ class BooleanSetting(Setting): 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") @@ -307,6 +335,8 @@ class SelectionSetting(Setting): 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") From 3af5b8603a89207de9791b479ff6e0fc4ea57c82 Mon Sep 17 00:00:00 2001 From: jupfi Date: Sat, 25 May 2024 15:02:32 +0200 Subject: [PATCH 07/12] Fixed visualization of load and save button. --- .../base_spectrometer_view.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/nqrduck_spectrometer/base_spectrometer_view.py b/src/nqrduck_spectrometer/base_spectrometer_view.py index e09b3bc..599b7f6 100644 --- a/src/nqrduck_spectrometer/base_spectrometer_view.py +++ b/src/nqrduck_spectrometer/base_spectrometer_view.py @@ -93,23 +93,27 @@ class BaseSpectrometerView(ModuleView): # Push all the settings to the top of the widget self._ui_form.verticalLayout.addStretch(1) - # Now we add a save and load button to the widget + # Now we add a save and load button to the widget self.button_layout = QHBoxLayout() + + # Save Button self.save_button = QPushButton("Save Settings") self.save_button.setIcon(Logos.Save16x16()) - #self.save_button.setIconSize(self.save_button.size()) + 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.setIconSize(self.load_button.size()) 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") From f94ff084119b0fabd83090e2c2ee588988954cde Mon Sep 17 00:00:00 2001 From: jupfi Date: Sun, 26 May 2024 16:21:28 +0200 Subject: [PATCH 08/12] Implemented default_settings. --- .../base_spectrometer_controller.py | 7 +++++++ .../base_spectrometer_model.py | 20 +++++++++++++++++++ .../base_spectrometer_view.py | 5 +++++ src/nqrduck_spectrometer/controller.py | 2 ++ 4 files changed, 34 insertions(+) diff --git a/src/nqrduck_spectrometer/base_spectrometer_controller.py b/src/nqrduck_spectrometer/base_spectrometer_controller.py index 6eb0f9a..35bf3e7 100644 --- a/src/nqrduck_spectrometer/base_spectrometer_controller.py +++ b/src/nqrduck_spectrometer/base_spectrometer_controller.py @@ -1,8 +1,10 @@ """Base class for all spectrometer controllers.""" +import logging import ast from nqrduck.module.module_controller import ModuleController +logger = logging.getLogger(__name__) class BaseSpectrometerController(ModuleController): """The base class for all spectrometer controllers.""" @@ -11,6 +13,11 @@ class BaseSpectrometerController(ModuleController): """Initializes the spectrometer controller.""" 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 diff --git a/src/nqrduck_spectrometer/base_spectrometer_model.py b/src/nqrduck_spectrometer/base_spectrometer_model.py index 688ddee..2934f06 100644 --- a/src/nqrduck_spectrometer/base_spectrometer_model.py +++ b/src/nqrduck_spectrometer/base_spectrometer_model.py @@ -2,6 +2,7 @@ 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 @@ -101,6 +102,25 @@ class BaseSpectrometerModel(ModuleModel): super().__init__(module) self.settings = 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 add_setting(self, setting: Setting, category: str) -> None: """Adds a setting to the spectrometer. diff --git a/src/nqrduck_spectrometer/base_spectrometer_view.py b/src/nqrduck_spectrometer/base_spectrometer_view.py index 599b7f6..2193544 100644 --- a/src/nqrduck_spectrometer/base_spectrometer_view.py +++ b/src/nqrduck_spectrometer/base_spectrometer_view.py @@ -96,6 +96,11 @@ class BaseSpectrometerView(ModuleView): # 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.module.model.set_default_settings) + self.button_layout.addWidget(self.default_button) + # Save Button self.save_button = QPushButton("Save Settings") self.save_button.setIcon(Logos.Save16x16()) diff --git a/src/nqrduck_spectrometer/controller.py b/src/nqrduck_spectrometer/controller.py index 717de66..93a27c3 100644 --- a/src/nqrduck_spectrometer/controller.py +++ b/src/nqrduck_spectrometer/controller.py @@ -43,6 +43,8 @@ class SpectrometerController(ModuleController): logger.debug("Adding spectrometer to spectrometer model: %s", module_name) self._module.model.add_spectrometers(module_name, module) + module.controller.on_loading() + self._module.view.create_menu_entry() def process_signals(self, key: str, value: object) -> None: From c14c047118247b818a49a44e10177f52ccedbe32 Mon Sep 17 00:00:00 2001 From: jupfi Date: Sun, 26 May 2024 16:31:52 +0200 Subject: [PATCH 09/12] Added dialog for default settings. --- .../base_spectrometer_model.py | 4 ++ .../base_spectrometer_view.py | 65 +++++++++++++++++-- 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/src/nqrduck_spectrometer/base_spectrometer_model.py b/src/nqrduck_spectrometer/base_spectrometer_model.py index 2934f06..00c19a2 100644 --- a/src/nqrduck_spectrometer/base_spectrometer_model.py +++ b/src/nqrduck_spectrometer/base_spectrometer_model.py @@ -122,6 +122,10 @@ class BaseSpectrometerModel(ModuleModel): 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: """Adds a setting to the spectrometer. diff --git a/src/nqrduck_spectrometer/base_spectrometer_view.py b/src/nqrduck_spectrometer/base_spectrometer_view.py index 2193544..ec089e8 100644 --- a/src/nqrduck_spectrometer/base_spectrometer_view.py +++ b/src/nqrduck_spectrometer/base_spectrometer_view.py @@ -9,6 +9,7 @@ from PyQt6.QtWidgets import ( QSpacerItem, QVBoxLayout, QPushButton, + QDialog, ) from nqrduck.module.module_view import ModuleView from nqrduck.assets.icons import Logos @@ -93,12 +94,12 @@ class BaseSpectrometerView(ModuleView): # Push all the settings to the top of the widget self._ui_form.verticalLayout.addStretch(1) - # Now we add a save and load button to the widget + # 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.module.model.set_default_settings) + self.default_button.clicked.connect(self.on_default_button_clicked) self.button_layout.addWidget(self.default_button) # Save Button @@ -114,11 +115,11 @@ class BaseSpectrometerView(ModuleView): 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") @@ -141,3 +142,57 @@ class BaseSpectrometerView(ModuleView): 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): + def __init__(self, parent=None): + 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."] + ) From 979f92235cc782908e1cbae6b25ac85b2fb3cd73 Mon Sep 17 00:00:00 2001 From: jupfi Date: Mon, 27 May 2024 19:26:58 +0200 Subject: [PATCH 10/12] Linting. --- src/nqrduck_spectrometer/base_spectrometer_controller.py | 2 +- src/nqrduck_spectrometer/base_spectrometer_view.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/nqrduck_spectrometer/base_spectrometer_controller.py b/src/nqrduck_spectrometer/base_spectrometer_controller.py index 35bf3e7..156cd97 100644 --- a/src/nqrduck_spectrometer/base_spectrometer_controller.py +++ b/src/nqrduck_spectrometer/base_spectrometer_controller.py @@ -35,7 +35,7 @@ class BaseSpectrometerController(ModuleController): def load_settings(self, path: str) -> None: """Loads the settings of the spectrometer.""" - with open(path, "r") as f: + with open(path) as f: json = f.read() # string to dict diff --git a/src/nqrduck_spectrometer/base_spectrometer_view.py b/src/nqrduck_spectrometer/base_spectrometer_view.py index ec089e8..0480a7e 100644 --- a/src/nqrduck_spectrometer/base_spectrometer_view.py +++ b/src/nqrduck_spectrometer/base_spectrometer_view.py @@ -150,7 +150,9 @@ class BaseSpectrometerView(ModuleView): 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") From e95dc4e4eac4f7e23814f2f3b0947d3c245d95cf Mon Sep 17 00:00:00 2001 From: jupfi Date: Mon, 27 May 2024 19:30:59 +0200 Subject: [PATCH 11/12] Version bump. --- CHANGELOG.md | 4 ++++ pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e0df9a..2eb4df1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Version 0.0.12 (27-05-2024) + +- Implemented loading and saving of settings and default settings (`6496ec6824da33fa41a38e26fdcac9a03ceb51fb`) + ## Version 0.0.11 (20-05-2024) - Measurements are now run in a separate worker thread to prevent the GUI from freezing (`27865aa6d44158e74c0e537be8407c12b4e3725b`) diff --git a/pyproject.toml b/pyproject.toml index 460df08..6194303 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ allow-direct-references = true [project] name = "nqrduck-spectrometer" -version = "0.0.11" +version = "0.0.12" authors = [ { name="jupfi", email="support@nqruck.cool" }, ] From bd40004dd314d0d4c7e2ba5a3c552b29b66e73a8 Mon Sep 17 00:00:00 2001 From: jupfi Date: Mon, 27 May 2024 19:32:25 +0200 Subject: [PATCH 12/12] Changelog for fitting. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2eb4df1..f6f4d34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - Implemented loading and saving of settings and default settings (`6496ec6824da33fa41a38e26fdcac9a03ceb51fb`) +- Added fitting of measurement data (`d43639c2f9796d5055dd01bd2f36bae43877bfe8`) + ## Version 0.0.11 (20-05-2024) - Measurements are now run in a separate worker thread to prevent the GUI from freezing (`27865aa6d44158e74c0e537be8407c12b4e3725b`)