commit e13ea949bcfd1ee9d2354611fe65252a42014a01 Author: jupfi Date: Tue May 28 20:35:56 2024 +0200 Initial structure. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f9af7dd --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.pyc +*$py.class + +# Distribution / packaging +dist/ +build/ +*.egg-info/ + +# IDE-specific files +.idea/ +.vscode/ + +# Logs +*.log + +# Virtual environments +venv/ + +# Other +*.DS_Store +*.pos +*.quack \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7198416 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Version 0.0.1 (15-04-2024) + +- Initial release diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7617f05 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2023-2024 jupfi +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c282a23 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# quackseq \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d626ff1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,59 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.metadata] +allow-direct-references = true + +[project] +name = "quackseq" +version = "0.0.1" +authors = [ + { name="jupfi", email="support@nqrduck.cool" }, +] + +description = "Simple Python script to perform magnetic resonance spectroscopy experiments." +readme = "README.md" +license = { file="LICENSE" } +requires-python = ">=3.10" + +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] + +dependencies = [ + "numpy", + "scipy", +] + +[project.optional-dependencies] +dev = [ + "black", + "pydocstyle", + "pyupgrade", + "ruff", +] + +[tool.ruff] + +[tool.ruff.lint] +extend-select = [ + "UP", # pyupgrade + "D", # pydocstyle +] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"] + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[project.urls] +"Homepage" = "https://nqrduck.cool" +"Bug Tracker" = "https://github.com/nqrduck/quackseq/issues" +"Source Code" = "https://github.com/nqrduck/quackseq" + +[tool.hatch.build.targets.wheel] +packages = ["src/quackseq"] \ No newline at end of file diff --git a/src/quackseq/__init__.py b/src/quackseq/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/quackseq/event.py b/src/quackseq/event.py new file mode 100644 index 0000000..ab8fd64 --- /dev/null +++ b/src/quackseq/event.py @@ -0,0 +1,95 @@ +import logging +from collections import OrderedDict + +from quackseq.pulseparameters import Option +from quackseq.helpers import UnitConverter + +logger = logging.getLogger(__name__) + + +class Event: + """An event is a part of a pulse sequence. It has a name and a duration and different parameters that have to be set. + + Args: + name (str): The name of the event + duration (str): The duration of the event + + Attributes: + name (str): The name of the event + duration (str): The duration of the event + parameters (OrderedDict): The parameters of the event + """ + + def __init__(self, name: str, duration: str) -> None: + """Initializes the event.""" + self.parameters = OrderedDict() + self.name = name + self.duration = duration + + def add_parameter(self, parameter) -> None: + """Adds a parameter to the event. + + Args: + parameter: The parameter to add + """ + self.parameters.append(parameter) + + def on_duration_changed(self, duration: str) -> None: + """This method is called when the duration of the event is changed. + + Args: + duration (str): The new duration of the event + """ + logger.debug("Duration of event %s changed to %s", self.name, duration) + self.duration = duration + + @classmethod + def load_event(cls, event, pulse_parameter_options): + """Loads an event from a dict. + + The pulse paramter options are needed to load the parameters + and determine if the correct spectrometer is active. + + Args: + event (dict): The dict with the event data + pulse_parameter_options (dict): The dict with the pulse parameter options + + Returns: + Event: The loaded event + """ + obj = cls(event["name"], event["duration"]) + for parameter in event["parameters"]: + for pulse_parameter_option in pulse_parameter_options.keys(): + # This checks if the pulse paramter options are the same as the ones in the pulse sequence + if pulse_parameter_option == parameter["name"]: + pulse_parameter_class = pulse_parameter_options[ + pulse_parameter_option + ] + obj.parameters[pulse_parameter_option] = pulse_parameter_class( + parameter["name"] + ) + # Delete the default instances of the pulse parameter options + obj.parameters[pulse_parameter_option].options = [] + for option in parameter["value"]: + obj.parameters[pulse_parameter_option].options.append( + Option.from_json(option) + ) + + return obj + + @property + def duration(self): + """The duration of the event.""" + return self._duration + + @duration.setter + def duration(self, duration: str): + # Duration needs to be a positive number + try: + duration = UnitConverter.to_float(duration) + except ValueError: + raise ValueError("Duration needs to be a number") + if duration < 0: + raise ValueError("Duration needs to be a positive number") + + self._duration = duration diff --git a/src/quackseq/functions.py b/src/quackseq/functions.py new file mode 100644 index 0000000..48452d7 --- /dev/null +++ b/src/quackseq/functions.py @@ -0,0 +1,349 @@ +"""A module that contains functions that can be used in various modules. This used to be part of the nqrduck-spectrometer module.""" + +from __future__ import annotations +import logging +import numpy as np +import sympy + +logger = logging.getLogger(__name__) + + +class Function: + """A function that can be used as a pulse parameter. + + This class is the base class for all functions that can be used as pulse parameters. Functions can be used for pulse shapes, for example. + + Args: + expr (str | sympy.Expr): The expression of the function. + + Attributes: + name (str): The name of the function. + parameters (list): The parameters of the function. + expr (sympy.Expr): The sympy expression of the function. + resolution (float): The resolution of the function in seconds. + start_x (float): The x value where the evalution of the function starts. + end_x (float): The x value where the evalution of the function ends. + """ + + name: str + parameters: list + expression: str | sympy.Expr + resolution: float + start_x: float + end_x: float + + subclasses = [] + + def __init_subclass__(cls, **kwargs): + """Registers the subclass.""" + super().__init_subclass__(**kwargs) + cls.subclasses.append(cls) + + def __init__(self, expr) -> None: + """Initializes the function.""" + self.parameters = [] + self.expr = expr + self.resolution = 1 / 30.72e6 + self.start_x = -1 + self.end_x = 1 + + def get_time_points(self, pulse_length: float) -> np.ndarray: + """Returns the time domain points for the function with the given pulse length. + + Args: + pulse_length (float): The pulse length in seconds. + + Returns: + np.ndarray: The time domain points. + """ + # Get the time domain points + n = int(pulse_length / self.resolution) + t = np.linspace(0, float(pulse_length), n) + return t + + def evaluate(self, pulse_length: float, resolution: float = None) -> np.ndarray: + """Evaluates the function for the given pulse length. + + Args: + pulse_length (float): The pulse length in seconds. + resolution (float, optional): The resolution of the function in seconds. Defaults to None. + + Returns: + np.ndarray: The evaluated function. + """ + if resolution is None: + resolution = self.resolution + n = int(pulse_length / resolution) + t = np.linspace(self.start_x, self.end_x, n) + x = sympy.symbols("x") + + found_variables = dict() + # Create a dictionary of the parameters and their values + for parameter in self.parameters: + found_variables[parameter.symbol] = parameter.value + + final_expr = self.expr.subs(found_variables) + # If the expression is a number (does not depend on x), return an array of that number + if final_expr.is_number: + return np.full(t.shape, float(final_expr)) + + f = sympy.lambdify([x], final_expr, "numpy") + + return f(t) + + def get_tdx(self, pulse_length: float) -> np.ndarray: + """Returns the time domain points and the evaluated function for the given pulse length. + + Args: + pulse_length (float): The pulse length in seconds. + + Returns: + np.ndarray: The time domain points. + """ + n = int(pulse_length / self.resolution) + t = np.linspace(self.start_x, self.end_x, n) + return t + + def get_pulse_amplitude( + self, pulse_length: float, resolution: float = None + ) -> np.array: + """Returns the pulse amplitude in the time domain. + + Args: + pulse_length (Float): The pulse length in seconds. + resolution (float, optional): The resolution of the function in seconds. Defaults to None. + + Returns: + np.array: The pulse amplitude. + """ + return self.evaluate(pulse_length, resolution=resolution) + + def add_parameter(self, parameter: Function.Parameter) -> None: + """Adds a parameter to the function. + + Args: + parameter (Function.Parameter): The parameter to add. + """ + self.parameters.append(parameter) + + def to_json(self) -> dict: + """Returns a json representation of the function. + + Returns: + dict: The json representation of the function. + """ + return { + "name": self.name, + "class" : self.__class__.__name__, + "parameters": [parameter.to_json() for parameter in self.parameters], + "expression": str(self.expr), + "resolution": self.resolution, + "start_x": self.start_x, + "end_x": self.end_x, + } + + @classmethod + def from_json(cls, data: dict) -> Function: + """Creates a function from a json representation. + + Args: + data (dict): The json representation of the function. + + Returns: + Function: The function. + """ + logger.debug(f"Data: {data}") + for subclass in cls.subclasses: + logger.debug("Checking subclass %s", subclass) + logger.debug("Subclass name %s", subclass.__name__) + if subclass.__name__ == data["class"]: + cls = subclass + logger.debug("Found subclass %s", cls) + break + + obj = cls() + obj.expr = data["expression"] + obj.name = data["name"] + obj.resolution = data["resolution"] + obj.start_x = data["start_x"] + obj.end_x = data["end_x"] + + obj.parameters = [] + for parameter in data["parameters"]: + obj.add_parameter(Function.Parameter.from_json(parameter)) + + return obj + + @property + def expr(self): + """The sympy expression of the function.""" + return self._expr + + @expr.setter + def expr(self, expr): + if isinstance(expr, str): + try: + self._expr = sympy.sympify(expr) + except ValueError: + logger.error(f"Could not convert {expr} to a sympy expression") + raise SyntaxError(f"Could not convert {expr} to a sympy expression") + elif isinstance(expr, sympy.Expr): + self._expr = expr + + @property + def resolution(self): + """The resolution of the function in seconds.""" + return self._resolution + + @resolution.setter + def resolution(self, resolution): + try: + self._resolution = float(resolution) + except ValueError: + logger.error("Could not convert %s to a float", resolution) + raise SyntaxError(f"Could not convert {resolution} to a float") + + @property + def start_x(self): + """The x value where the evalution of the function starts.""" + return self._start_x + + @start_x.setter + def start_x(self, start_x): + try: + self._start_x = float(start_x) + except ValueError: + logger.error("Could not convert %s to a float", start_x) + raise SyntaxError(f"Could not convert {start_x} to a float") + + @property + def end_x(self): + """The x value where the evalution of the function ends.""" + return self._end_x + + @end_x.setter + def end_x(self, end_x): + try: + self._end_x = float(end_x) + except ValueError: + logger.error("Could not convert %s to a float", end_x) + raise SyntaxError(f"Could not convert {end_x} to a float") + + class Parameter: + """A parameter of a function. + + This can be for example the standard deviation of a Gaussian function. + + Args: + name (str): The name of the parameter. + symbol (str): The symbol of the parameter. + value (float): The value of the parameter. + + Attributes: + name (str): The name of the parameter. + symbol (str): The symbol of the parameter. + value (float): The value of the parameter. + default (float): The default value of the parameter. + """ + + def __init__(self, name: str, symbol: str, value: float) -> None: + """Initializes the parameter.""" + self.name = name + self.symbol = symbol + self.value = value + self.default = value + + def set_value(self, value: float) -> None: + """Sets the value of the parameter. + + Args: + value (float): The new value of the parameter. + """ + self.value = value + logger.debug("Parameter %s set to %s", self.name, self.value) + + def to_json(self) -> dict: + """Returns a json representation of the parameter. + + Returns: + dict: The json representation of the parameter. + """ + return { + "name": self.name, + "symbol": self.symbol, + "value": self.value, + "default": self.default, + } + + @classmethod + def from_json(cls, data): + """Creates a parameter from a json representation. + + Args: + data (dict): The json representation of the parameter. + + Returns: + Function.Parameter: The parameter. + """ + obj = cls(data["name"], data["symbol"], data["value"]) + obj.default = data["default"] + return obj + + +class RectFunction(Function): + """The rectangular function.""" + + name = "Rectangular" + + def __init__(self) -> None: + """Initializes the RecFunction.""" + expr = sympy.sympify("1") + super().__init__(expr) + + +class SincFunction(Function): + """The sinc function. + + The sinc function is defined as sin(x * l) / (x * l). + The parameter is the scale factor l. + """ + + name = "Sinc" + + def __init__(self) -> None: + """Initializes the SincFunction.""" + expr = sympy.sympify("sin(x * l)/ (x * l)") + super().__init__(expr) + self.add_parameter(Function.Parameter("Scale Factor", "l", 2)) + self.start_x = -np.pi + self.end_x = np.pi + + +class GaussianFunction(Function): + """The Gaussian function. + + The Gaussian function is defined as exp(-0.5 * ((x - mu) / sigma)**2). + The parameters are the mean and the standard deviation. + """ + + name = "Gaussian" + + def __init__(self) -> None: + """Initializes the GaussianFunction.""" + expr = sympy.sympify("exp(-0.5 * ((x - mu) / sigma)**2)") + super().__init__(expr) + self.add_parameter(Function.Parameter("Mean", "mu", 0)) + self.add_parameter(Function.Parameter("Standard Deviation", "sigma", 1)) + self.start_x = -np.pi + self.end_x = np.pi + + +class CustomFunction(Function): + """A custom function.""" + + name = "Custom" + + def __init__(self) -> None: + """Initializes the Custom Function.""" + expr = sympy.sympify(" 2 * x**2 + 3 * x + 1") + super().__init__(expr) diff --git a/src/quackseq/helpers.py b/src/quackseq/helpers.py new file mode 100644 index 0000000..226c385 --- /dev/null +++ b/src/quackseq/helpers.py @@ -0,0 +1,51 @@ +"""Helper used for unit conversion.""" + +import decimal + + +class UnitConverter: + """This class provides methods for unit conversion.""" + + @classmethod + def to_decimal(cls, value: str) -> decimal.Decimal: + """This method checks if the last character of the string is a suffix. + + The available suffixes are: + - n for nano + - u for micro + - m for milli + + Args: + value (str): The value to be converted + + Returns: + decimal.Decimal: The converted value + """ + try: + if value[-1] == "n": + return decimal.Decimal(value[:-1]) * decimal.Decimal("1e-9") + elif value[-1] == "u": + return decimal.Decimal(value[:-1]) * decimal.Decimal("1e-6") + elif value[-1] == "m": + return decimal.Decimal(value[:-1]) * decimal.Decimal("1e-3") + else: + return decimal.Decimal(value) + except TypeError: + return decimal.Decimal(value) + + @classmethod + def to_float(cls, value: str) -> float: + """This method checks if the last character of the string is a suffix. + + The available suffixes are: + - n for nano + - u for micro + - m for milli + + Args: + value (str): The value to be converted + + Returns: + float: The converted value + """ + return float(cls.to_decimal(value)) diff --git a/src/quackseq/measurement.py b/src/quackseq/measurement.py new file mode 100644 index 0000000..111e802 --- /dev/null +++ b/src/quackseq/measurement.py @@ -0,0 +1,364 @@ +"""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 quackseq.functions import Function +from quackseq.signalprocessing import SignalProcessing as sp + +logger = logging.getLogger(__name__) + + +class Measurement: + """This class defines how measurement data should look. + + It includes pulse parameters necessary for further signal processing. + Every spectrometer should adhere to this data structure in order to be compatible with the rest of the nqrduck. + + Args: + name (str): Name of the measurement. + tdx (np.array): Time axis for the x axis of the measurement data. + tdy (np.array): Time axis for the y axis of the measurement data. + target_frequency (float): Target frequency of the measurement. + frequency_shift (float, optional): Frequency shift of the measurement. Defaults to 0. + IF_frequency (float, optional): Intermediate frequency of the measurement. Defaults to 0. + + Attributes: + tdx (np.array): Time axis for the x axis of the measurement data. + tdy (np.array): Time axis for the y axis of the measurement data. + target_frequency (float): Target frequency of the measurement. + frequency_shift (float): Frequency shift of the measurement. + IF_frequency (float): Intermediate frequency of the measurement. + fdx (np.array): Frequency axis for the x axis of the measurement data. + fdy (np.array): Frequency axis for the y axis of the measurement data. + """ + + def __init__( + self, + name: str, + tdx: np.array, + tdy: np.array, + target_frequency: float, + frequency_shift: float = 0, + IF_frequency: float = 0, + ) -> None: + """Initializes the measurement.""" + self.name = name + self.tdx = tdx + self.tdy = tdy + self.target_frequency = target_frequency + self.frequency_shift = frequency_shift + self.IF_frequency = IF_frequency + self.fdx, self.fdy = sp.fft(tdx, tdy, frequency_shift) + self.fits = [] + + def apodization(self, function: Function) -> "Measurement": + """Applies apodization to the measurement data. + + Args: + function (Function): Apodization function. + + Returns: + Measurement: The apodized measurement. + """ + duration = (self.tdx[-1] - self.tdx[0]) * 1e-6 + resolution = duration / len(self.tdx) + logger.debug("Resolution: %s", resolution) + + y_weight = function.get_pulse_amplitude(duration, resolution) + tdy_apodized = self.tdy * y_weight + + apodized_measurement = Measurement( + self.name, + self.tdx, + tdy_apodized, + target_frequency=self.target_frequency, + IF_frequency=self.IF_frequency, + ) + return apodized_measurement + + def add_fit(self, fit: "Fit") -> None: + """Adds a fit to the measurement. + + Args: + fit (Fit): The fit to add. + """ + self.fits.append(fit) + + def delete_fit(self, fit: "Fit") -> None: + """Deletes a fit from the measurement. + + Args: + fit (Fit): The fit to delete. + """ + self.fits.remove(fit) + + def edit_fit_name(self, fit: "Fit", name: str) -> None: + """Edits the name of a fit. + + Args: + fit (Fit): The fit to edit. + name (str): The new name. + """ + logger.debug(f"Editing fit name to {name}.") + fit.name = name + + def to_json(self) -> dict: + """Converts the measurement to a JSON-compatible format. + + Returns: + dict: The measurement in JSON-compatible format. + """ + return { + "name": self.name, + "tdx": self.tdx.tolist(), + "tdy": [[x.real, x.imag] for x in self.tdy], + "target_frequency": self.target_frequency, + "IF_frequency": self.IF_frequency, + "fits": [fit.to_json() for fit in self.fits], + } + + @classmethod + def from_json(cls, json: dict) -> "Measurement": + """Converts the JSON format to a measurement. + + Args: + json (dict): The measurement in JSON-compatible format. + + Returns: + Measurement: The measurement. + """ + tdy = np.array([complex(y[0], y[1]) for y in json["tdy"]]) + measurement = cls( + json["name"], + np.array(json["tdx"]), + tdy, + target_frequency=json["target_frequency"], + IF_frequency=json["IF_frequency"], + ) + + for fit_json in json["fits"]: + measurement.add_fit(Fit.from_json(fit_json, measurement)) + + return measurement + + # Properties for encapsulation + @property + def name(self) -> str: + """Name of the measurement.""" + return self._name + + @name.setter + def name(self, value: str) -> None: + self._name = value + + @property + def tdx(self) -> np.array: + """Time domain data for the measurement (x).""" + return self._tdx + + @tdx.setter + def tdx(self, value: np.array) -> None: + self._tdx = value + + @property + def tdy(self) -> np.array: + """Time domain data for the measurement (y).""" + return self._tdy + + @tdy.setter + def tdy(self, value: np.array) -> None: + self._tdy = value + + @property + def fdx(self) -> np.array: + """Frequency domain data for the measurement (x).""" + return self._fdx + + @fdx.setter + def fdx(self, value: np.array) -> None: + self._fdx = value + + @property + def fdy(self) -> np.array: + """Frequency domain data for the measurement (y).""" + return self._fdy + + @fdy.setter + def fdy(self, value: np.array) -> None: + self._fdy = value + + @property + def target_frequency(self) -> float: + """Target frequency of the measurement.""" + return self._target_frequency + + @target_frequency.setter + def target_frequency(self, value: float) -> None: + self._target_frequency = value + + @property + def fits(self) -> list: + """Fits of the measurement.""" + return self._fits + + @fits.setter + def fits(self, value: list) -> None: + self._fits = value + + +class Fit: + """The fit class for measurement data. A fit can be performed on either the frequency or time domain data. + + A measurement can have multiple fits. + """ + + subclasses = [] + + def __init_subclass__(cls, **kwargs) -> None: + """Adds the subclass to the list of subclasses.""" + super().__init_subclass__(**kwargs) + cls.subclasses.append(cls) + + def __init__(self, name: str, domain: str, measurement: Measurement) -> None: + """Initializes the fit.""" + self.name = name + self.domain = domain + self.measurement = measurement + self.fit() + + def fit(self) -> None: + """Fits the measurement data and sets the fit parameters and covariance.""" + if self.domain == "time": + x = self.measurement.tdx + y = self.measurement.tdy + elif self.domain == "frequency": + x = self.measurement.fdx + y = self.measurement.fdy + else: + raise ValueError("Domain not recognized.") + + initial_guess = self.initial_guess() + self.parameters, self.covariance = curve_fit( + self.fit_function, x, abs(y), p0=initial_guess + ) + + self.x = x + self.y = self.fit_function(x, *self.parameters) + + def fit_function(self, x: np.array, *parameters) -> np.array: + """The fit function. + + Args: + x (np.array): The x data. + *parameters: The fit parameters. + + Returns: + np.array: The y data. + """ + raise NotImplementedError + + def initial_guess(self) -> list: + """Initial guess for the fit. + + Returns: + list: The initial guess. + """ + raise NotImplementedError + + def to_json(self) -> dict: + """Converts the fit to a JSON-compatible format. + + Returns: + dict: The fit in JSON-compatible format. + """ + return { + "name": self.name, + "class": self.__class__.__name__, + } + + @classmethod + def from_json(cls, data: dict, measurement: Measurement) -> "Fit": + """Converts the JSON format to a fit. + + Args: + data (dict): The fit in JSON-compatible format. + measurement (Measurement): The measurement. + + Returns: + Fit: The fit. + """ + for subclass in cls.subclasses: + if subclass.__name__ == data["class"]: + return subclass(name=data["name"], measurement=measurement) + + raise ValueError(f"Subclass {data['class']} not found.") + + @property + def x(self) -> np.array: + """The x data of the fit.""" + return self._x + + @x.setter + def x(self, value: np.array) -> None: + self._x = value + + @property + def y(self) -> np.array: + """The y data of the fit.""" + return self._y + + @y.setter + def y(self, value: np.array) -> None: + self._y = value + + +class T2StarFit(Fit): + """T2* fit for measurement data.""" + + def __init__(self, measurement: Measurement, name: str = "T2*") -> None: + """Initializes the T2* fit.""" + super().__init__(name, "time", measurement) + + def fit(self) -> None: + """Fits the measurement data and sets the fit parameters and covariance.""" + super().fit() + self.parameters = { + "S0": self.parameters[0], + "T2Star": self.parameters[1], + "covariance": self.covariance, + } + + def fit_function(self, t: np.array, S0: float, T2Star: float) -> np.array: + """The T2* fit function used for curve fitting.""" + return S0 * np.exp(-t / T2Star) + + def initial_guess(self) -> list: + """Initial guess for the T2* fit.""" + return [1, 1] + +class LorentzianFit(Fit): + """Lorentzian fit for measurement data.""" + + def __init__(self, measurement: Measurement, name: str = "Lorentzian") -> None: + """Initializes the Lorentzian fit.""" + super().__init__(name, "frequency", measurement) + + def fit(self) -> None: + """Fits the measurement data and sets the fit parameters and covariance.""" + super().fit() + self.parameters = { + "S0": self.parameters[0], + "T2Star": self.parameters[1], + "covariance": self.covariance, + } + logger.debug("Lorentzian fit parameters: %s", self.parameters) + + def fit_function(self, f: np.array, S0: float, T2Star: float) -> np.array: + """The Lorentzian fit function used for curve fitting.""" + return S0 / (1 + (2 * np.pi * f * T2Star) ** 2) + + def initial_guess(self) -> list: + """Initial guess for the Lorentzian fit.""" + return [1, 1] diff --git a/src/quackseq/options.py b/src/quackseq/options.py new file mode 100644 index 0000000..47781f2 --- /dev/null +++ b/src/quackseq/options.py @@ -0,0 +1,229 @@ +import logging +from quackseq.functions import Function + +logger = logging.getLogger(__name__) + +class Option: + """Defines options for the pulse parameters which can then be set accordingly. + + Options can be of different types, for example boolean, numeric or function. + + Args: + name (str): The name of the option. + value: The value of the option. + + Attributes: + name (str): The name of the option. + value: The value of the option. + """ + + subclasses = [] + + def __init_subclass__(cls, **kwargs): + """Adds the subclass to the list of subclasses.""" + super().__init_subclass__(**kwargs) + cls.subclasses.append(cls) + + def __init__(self, name: str, value) -> None: + """Initializes the option.""" + self.name = name + self.value = value + + def set_value(self): + """Sets the value of the option. + + This method has to be implemented in the derived classes. + """ + raise NotImplementedError + + def to_json(self): + """Returns a json representation of the option. + + Returns: + dict: The json representation of the option. + """ + return { + "name": self.name, + "value": self.value, + "class": self.__class__.__name__, + } + + @classmethod + def from_json(cls, data) -> "Option": + """Creates an option from a json representation. + + Args: + data (dict): The json representation of the option. + + Returns: + Option: The option. + """ + for subclass in cls.subclasses: + logger.debug(f"Keys data: {data.keys()}") + if subclass.__name__ == data["class"]: + cls = subclass + break + + # Check if from_json is implemented for the subclass + if cls.from_json.__func__ == Option.from_json.__func__: + obj = cls(data["name"], data["value"]) + else: + obj = cls.from_json(data) + + return obj + + +class BooleanOption(Option): + """Defines a boolean option for a pulse parameter option.""" + + def set_value(self, value): + """Sets the value of the option.""" + self.value = value + self.value_changed.emit() + + +class NumericOption(Option): + """Defines a numeric option for a pulse parameter option.""" + + def __init__( + self, + name: str, + value, + is_float=True, + min_value=None, + max_value=None, + slider=False, + ) -> None: + """Initializes the NumericOption. + + Args: + name (str): The name of the option. + value: The value of the option. + is_float (bool): If the value is a float. + min_value: The minimum value of the option. + max_value: The maximum value of the option. + """ + super().__init__(name, value) + self.is_float = is_float + self.min_value = min_value + self.max_value = max_value + + def set_value(self, value): + """Sets the value of the option.""" + if value < self.min_value: + self.value = self.min_value + self.value_changed.emit() + elif value >= self.max_value: + self.value = self.max_value + self.value_changed.emit() + else: + raise ValueError( + f"Value {value} is not in the range of {self.min_value} to {self.max_value}. This should have been caught earlier." + ) + + def to_json(self): + """Returns a json representation of the option. + + Returns: + dict: The json representation of the option. + """ + return { + "name": self.name, + "value": self.value, + "class": self.__class__.__name__, + "is_float": self.is_float, + "min_value": self.min_value, + "max_value": self.max_value, + } + + @classmethod + def from_json(cls, data): + """Creates a NumericOption from a json representation. + + Args: + data (dict): The json representation of the NumericOption. + + Returns: + NumericOption: The NumericOption. + """ + obj = cls( + data["name"], + data["value"], + is_float=data["is_float"], + min_value=data["min_value"], + max_value=data["max_value"], + ) + return obj + + +class FunctionOption(Option): + """Defines a selection option for a pulse parameter option. + + It takes different function objects. + + Args: + name (str): The name of the option. + functions (list): The functions that can be selected. + + Attributes: + name (str): The name of the option. + functions (list): The functions that can be selected. + """ + + def __init__(self, name, functions) -> None: + """Initializes the FunctionOption.""" + super().__init__(name, functions[0]) + self.functions = functions + + def set_value(self, value): + """Sets the value of the option. + + Args: + value: The value of the option. + """ + self.value = value + self.value_changed.emit() + + def get_function_by_name(self, name): + """Returns the function with the given name. + + Args: + name (str): The name of the function. + + Returns: + Function: The function with the given name. + """ + for function in self.functions: + if function.name == name: + return function + raise ValueError(f"Function with name {name} not found") + + def to_json(self): + """Returns a json representation of the option. + + Returns: + dict: The json representation of the option. + """ + return { + "name": self.name, + "value": self.value.to_json(), + "class": self.__class__.__name__, + "functions": [function.to_json() for function in self.functions], + } + + @classmethod + def from_json(cls, data): + """Creates a FunctionOption from a json representation. + + Args: + data (dict): The json representation of the FunctionOption. + + Returns: + FunctionOption: The FunctionOption. + """ + logger.debug(f"Data: {data}") + # These are all available functions + functions = [Function.from_json(function) for function in data["functions"]] + obj = cls(data["name"], functions) + obj.value = Function.from_json(data["value"]) + return obj diff --git a/src/quackseq/pulseparameters.py b/src/quackseq/pulseparameters.py new file mode 100644 index 0000000..196c3e9 --- /dev/null +++ b/src/quackseq/pulseparameters.py @@ -0,0 +1,165 @@ +"""Contains the classes for the pulse parameters of the spectrometer. It includes the functions and the options for the pulse parameters. + +Todo: + * This shouldn't be in the spectrometer module. It should be in it"s own pulse sequence module. +""" + +from __future__ import annotations +import logging + +from numpy.core.multiarray import array as array + +from quackseq.options import BooleanOption, FunctionOption, NumericOption, Option +from quackseq.functions import RectFunction, SincFunction, GaussianFunction, CustomFunction + +logger = logging.getLogger(__name__) + + +class PulseParameter: + """A pulse parameter is a value that can be different for each event in a pulse sequence. + + E.g. the transmit pulse power or the phase of the transmit pulse. + + Args: + name (str) : The name of the pulse parameter + + Attributes: + name (str) : The name of the pulse parameter + options (OrderedDict) : The options of the pulse parameter + """ + + def __init__(self, name: str): + """Initializes the pulse parameter. + + Arguments: + name (str) : The name of the pulse parameter + """ + self.name = name + self.options = list() + + def add_option(self, option: Option) -> None: + """Adds an option to the pulse parameter. + + Args: + option (Option) : The option to add + """ + self.options.append(option) + + def get_options(self) -> list: + """Gets the options of the pulse parameter. + + Returns: + list : The options of the pulse parameter + """ + return self.options + + def get_option_by_name(self, name: str) -> Option: + """Gets an option by its name. + + Args: + name (str) : The name of the option + + Returns: + Option : The option with the specified name + + Raises: + ValueError : If no option with the specified name is found + """ + for option in self.options: + if option.name == name: + return option + raise ValueError(f"Option with name {name} not found") + + + +class TXPulse(PulseParameter): + """Basic TX Pulse Parameter. It includes options for the relative amplitude, the phase and the pulse shape. + + Args: + name (str): The name of the pulse parameter. + """ + + RELATIVE_AMPLITUDE = "Relative TX Amplitude (%)" + TX_PHASE = "TX Phase" + TX_PULSE_SHAPE = "TX Pulse Shape" + N_PHASE_CYCLES = "Number of Phase Cycles" + PHASE_CYCLE_LEVEL = "Phase Cycle Level" + + def __init__(self, name: str) -> None: + """Initializes the TX Pulse Parameter. + + It adds the options for the relative amplitude, the phase and the pulse shape. + """ + super().__init__(name) + self.add_option( + NumericOption( + self.RELATIVE_AMPLITUDE, + 0, + is_float=False, + min_value=0, + max_value=100, + slider=True, + ) + ) + self.add_option(NumericOption(self.TX_PHASE, 0)) + self.add_option( + NumericOption( + self.N_PHASE_CYCLES, 1, is_float=False, min_value=1, max_value=360 + ) + ) + self.add_option( + NumericOption( + self.PHASE_CYCLE_LEVEL, 0, is_float=False, min_value=0, max_value=10 + ) + ) + self.add_option( + FunctionOption( + self.TX_PULSE_SHAPE, + [ + RectFunction(), + SincFunction(), + GaussianFunction(), + CustomFunction(), + ], + ), + ) + +class RXReadout(PulseParameter): + """Basic PulseParameter for the RX Readout. It includes an option for the RX Readout state. + + Args: + name (str): The name of the pulse parameter. + + Attributes: + RX (str): The RX Readout state. + """ + + RX = "RX" + + def __init__(self, name) -> None: + """Initializes the RX Readout PulseParameter. + + It adds an option for the RX Readout state. + """ + super().__init__(name) + self.add_option(BooleanOption(self.RX, False)) + +class Gate(PulseParameter): + """Basic PulseParameter for the Gate. It includes an option for the Gate state. + + Args: + name (str): The name of the pulse parameter. + + Attributes: + GATE_STATE (str): The Gate state. + """ + + GATE_STATE = "Gate State" + + def __init__(self, name) -> None: + """Initializes the Gate PulseParameter. + + It adds an option for the Gate state. + """ + super().__init__(name) + self.add_option(BooleanOption(self.GATE_STATE, False)) diff --git a/src/quackseq/pulsesequence.py b/src/quackseq/pulsesequence.py new file mode 100644 index 0000000..8966426 --- /dev/null +++ b/src/quackseq/pulsesequence.py @@ -0,0 +1,165 @@ +"""Contains the PulseSequence class that is used to store a pulse sequence and its events.""" + +import logging +import importlib.metadata + +logger = logging.getLogger(__name__) + + +class PulseSequence: + """A pulse sequence is a collection of events that are executed in a certain order. + + Args: + name (str): The name of the pulse sequence + + Attributes: + name (str): The name of the pulse sequence + events (list): The events of the pulse sequence + """ + + def __init__(self, name, version = None) -> None: + """Initializes the pulse sequence.""" + self.name = name + # Saving version to check for compatability of saved sequence + if version is not None: + self.version = version + else: + self.version = importlib.metadata.version("nqrduck_spectrometer") + self.events = list() + + def get_event_names(self) -> list: + """Returns a list of the names of the events in the pulse sequence. + + Returns: + list: The names of the events + """ + return [event.name for event in self.events] + + def add_event(self, event_name: str, duration: float) -> None: + """Add a new event to the pulse sequence. + + Args: + event_name (str): The name of the event + duration (float): The duration of the event + """ + self.events.append(self.Event(event_name, f"{float(duration):.16g}u")) + + + def delete_event(self, event_name: str) -> None: + """Deletes an event from the pulse sequence. + + Args: + event_name (str): The name of the event to delete + """ + for event in self.events: + if event.name == event_name: + self.events.remove(event) + break + + def to_json(self): + """Returns a dict with all the data in the pulse sequence. + + Returns: + dict: The dict with the sequence data + """ + # Get the versions of this package + data = {"name": self.name, "version" : self.version, "events": []} + for event in self.events: + event_data = { + "name": event.name, + "duration": event.duration, + "parameters": [], + } + for parameter in event.parameters.keys(): + event_data["parameters"].append({"name": parameter, "value": []}) + for option in event.parameters[parameter].options: + event_data["parameters"][-1]["value"].append(option.to_json()) + data["events"].append(event_data) + return data + + @classmethod + def load_sequence(cls, sequence, pulse_parameter_options): + """Loads a pulse sequence from a dict. + + The pulse paramter options are needed to load the parameters + and make sure the correct spectrometer is active. + + Args: + sequence (dict): The dict with the sequence data + pulse_parameter_options (dict): The dict with the pulse parameter options + + Returns: + PulseSequence: The loaded pulse sequence + + Raises: + KeyError: If the pulse parameter options are not the same as the ones in the pulse sequence + """ + try: + obj = cls(sequence["name"], version = sequence["version"]) + except KeyError: + logger.error("Pulse sequence version not found") + raise KeyError("Pulse sequence version not found") + + for event_data in sequence["events"]: + obj.events.append(cls.Event.load_event(event_data, pulse_parameter_options)) + + return obj + + class Variable: + """A variable is a parameter that can be used within a pulsesequence as a placeholder. + + For example the event duration a Variable with name a can be set. This variable can then be set to a list of different values. + On execution of the pulse sequence the event duration will be set to the first value in the list. + Then the pulse sequence will be executed with the second value of the list. This is repeated until the pulse sequence has + been executed with all values in the list. + """ + + @property + def name(self): + """The name of the variable.""" + return self._name + + @name.setter + def name(self, name: str): + if not isinstance(name, str): + raise TypeError("Name needs to be a string") + self._name = name + + @property + def values(self): + """The values of the variable. This is a list of values that the variable can take.""" + return self._values + + @values.setter + def values(self, values: list): + if not isinstance(values, list): + raise TypeError("Values needs to be a list") + self._values = values + + class VariableGroup: + """Variables can be grouped together. + + If we have groups a and b the pulse sequence will be executed for all combinations of variables in a and b. + """ + + @property + def name(self): + """The name of the variable group.""" + return self._name + + @name.setter + def name(self, name: str): + if not isinstance(name, str): + raise TypeError("Name needs to be a string") + self._name = name + + @property + def variables(self): + """The variables in the group. This is a list of variables.""" + return self._variables + + @variables.setter + def variables(self, variables: list): + if not isinstance(variables, list): + raise TypeError("Variables needs to be a list") + self._variables = variables diff --git a/src/quackseq/signalprocessing.py b/src/quackseq/signalprocessing.py new file mode 100644 index 0000000..86734ee --- /dev/null +++ b/src/quackseq/signalprocessing.py @@ -0,0 +1,86 @@ +"""Helper used for signal processing.""" +import logging +from scipy.fft import fft, fftfreq, fftshift +import numpy as np +import sympy + +logger = logging.getLogger(__name__) + +class SignalProcessing: + """This class provides various signal processing methods that can then be used by nqrduck modules.""" + + @classmethod + def fft(cls, tdx : np.array, tdy: np.array, freq_shift : float = 0, zero_padding = 1000) -> tuple[np.array, np.array]: + """This method calculates the FFT of the time domain data. + + Args: + tdx (np.array): Time domain x data in seconds. + tdy (np.array): Time domain magnitude y data. + freq_shift (float): Frequency shift in MHz - this can be useful if the spectrometer has it's frequency data in the IF band. + zero_padding (float): Zero padding to be used in the FFT. + + Returns: + np.array: Frequency domain x data in MHz. + np.array: Frequency domain magnitude y data. + """ + dwell_time = (tdx[1] - tdx[0]) + + N = len(tdx) + zero_padding + + if freq_shift != 0: + # Create the complex exponential to shift the frequency + shift_signal = np.exp(-2j * np.pi * freq_shift * tdx)[:, np.newaxis] + + # Apply the shift by multiplying the time domain signal + tdy_shift = np.abs(tdy * shift_signal) + ydf = fftshift(fft(tdy_shift, N, axis=0), axes=0) + + else: + ydf = fftshift(fft(tdy, N, axis=0), axes=0) + + xdf = fftshift(fftfreq(N, dwell_time)) + + return xdf, ydf + + @classmethod + def baseline_correction(cls, fdx : np.array, fdy : np.array, order : int) -> np.array: + """This method calculates the baseline correction of the frequency domain data. + + Args: + fdx (np.array): Frequency domain x data in MHz. + fdy (np.array): Frequency domain magnitude y data. + order (int): Order of the polynomial used for baseline correction. + + Returns: + np.array: Frequency domain magnitude y data with baseline correction. + """ + pass + + @classmethod + def apodization(cls, tdx : np.array, tdy : np.array, apodization_function : sympy.Expr) -> np.array: + """This method calculates the apodization of the time domain data. + + Args: + tdx (np.array): Time domain x data in seconds. + tdy (np.array): Time domain magnitude y data. + apodization_function (sympy.Expr): Apodization function. + + Returns: + np.array: Time domain magnitude y data with apodization. + """ + weight = np.array([apodization_function.subs("t", t) for t in tdx]) + return tdy * weight + + @classmethod + def peak_picking(cls, fdx: np.array, fdy: np.array, threshold : float = 0.05) -> tuple[np.array, np.array]: + """This method calculates the peak picking of the frequency domain data. + + Args: + fdx (np.array): Frequency domain x data in MHz. + fdy (np.array): Frequency domain magnitude y data. + threshold (float): Threshold for peak picking. + + Returns: + list: x,y data of the peaks. + """ + pass diff --git a/src/quackseq/spectrometer/spectrometer_model.py b/src/quackseq/spectrometer/spectrometer_model.py new file mode 100644 index 0000000..4a8fa9a --- /dev/null +++ b/src/quackseq/spectrometer/spectrometer_model.py @@ -0,0 +1,92 @@ +"""The base class for all spectrometer models.""" + +import logging +from collections import OrderedDict +from quackseq.spectrometer.spectrometer_setting import Setting + +logger = logging.getLogger(__name__) + + +class SpectrometerModel(): + """The base class for all spectrometer models. + + It contains the settings and pulse parameters of the spectrometer. + + Args: + module (Module) : The module that the spectrometer is connected to + + Attributes: + settings (OrderedDict) : The settings of the spectrometer + pulse_parameter_options (OrderedDict) : The pulse parameter options of the spectrometer + """ + + settings: OrderedDict + pulse_parameter_options: OrderedDict + + def __init__(self, module): + """Initializes the spectrometer model. + + Args: + module (Module) : The module that the spectrometer is connected to + """ + super().__init__(module) + self.settings = OrderedDict() + self.pulse_parameter_options = OrderedDict() + + def add_setting(self, setting: Setting, category: str) -> None: + """Adds a setting to the spectrometer. + + Args: + setting (Setting) : The setting to add + category (str) : The category of the setting + """ + if category not in self.settings.keys(): + self.settings[category] = [] + self.settings[category].append(setting) + + def get_setting_by_name(self, name: str) -> Setting: + """Gets a setting by its name. + + Args: + name (str) : The name of the setting + + Returns: + Setting : The setting with the specified name + + Raises: + ValueError : If no setting with the specified name is found + """ + for category in self.settings.keys(): + for setting in self.settings[category]: + if setting.name == name: + return setting + raise ValueError(f"Setting with name {name} not found") + + def add_pulse_parameter_option( + self, name: str, pulse_parameter_class: "PulseParameter" + ) -> None: + """Adds a pulse parameter option to the spectrometer. + + Args: + name (str) : The name of the pulse parameter + pulse_parameter_class (PulseParameter) : The pulse parameter class + """ + self.pulse_parameter_options[name] = pulse_parameter_class + + @property + def target_frequency(self): + """The target frequency of the spectrometer in Hz. This is the frequency where the magnetic resonance experiment is performed.""" + raise NotImplementedError + + @target_frequency.setter + def target_frequency(self, value): + raise NotImplementedError + + @property + def averages(self): + """The number of averages for the spectrometer.""" + raise NotImplementedError + + @averages.setter + def averages(self, value): + raise NotImplementedError diff --git a/src/quackseq/spectrometer/spectrometer_setting.py b/src/quackseq/spectrometer/spectrometer_setting.py new file mode 100644 index 0000000..e404295 --- /dev/null +++ b/src/quackseq/spectrometer/spectrometer_setting.py @@ -0,0 +1,243 @@ +"""Settings for the different spectrometers.""" + +import logging + +logger = logging.getLogger(__name__) + + +class Setting(): + """A setting for the spectrometer is a value that is the same for all events in a pulse sequence. + + E.g. the Transmit gain or the number of points in a spectrum. + + Args: + name (str) : The name of the setting + description (str) : A description of the setting + default : The default value of the setting + + Attributes: + name (str) : The name of the setting + description (str) : A description of the setting + value : The value of the setting + """ + + def __init__(self, name: str, description: str = None, default=None) -> None: + """Create a new setting. + + Args: + name (str): The name of the setting. + description (str): A description of the setting. + default: The default value of the setting. + """ + self.widget = None + super().__init__() + self.name = name + self.description = description + if default is not None: + self.value = default + # Update the description with the default value + self.description += f"\n (Default: {default})" + + +class NumericalSetting(Setting): + """A setting that is a numerical value. + + It can additionally have a minimum and maximum value. + """ + + def __init__( + self, name: str, description: str, default, min_value=None, max_value=None + ) -> None: + """Create a new numerical setting.""" + super().__init__( + name, + self.description_limit_info(description, min_value, max_value), + default, + ) + + def description_limit_info(self, description: str, min_value, max_value) -> str: + """Updates the description with the limits of the setting if there are any. + + Args: + description (str): The description of the setting. + min_value: The minimum value of the setting. + max_value: The maximum value of the setting. + + Returns: + str: The description of the setting with the limits. + """ + if min_value is not None and max_value is not None: + description += f"\n (min: {min_value}, max: {max_value})" + elif min_value is not None: + description += f"\n (min: {min_value})" + elif max_value is not None: + description += f"\n (max: {max_value})" + + return description + + +class FloatSetting(NumericalSetting): + """A setting that is a Float. + + Args: + name (str) : The name of the setting + default : The default value of the setting + description (str) : A description of the setting + min_value : The minimum value of the setting + max_value : The maximum value of the setting + """ + + DEFAULT_LENGTH = 100 + + def __init__( + self, + name: str, + default: float, + description: str, + min_value: float = None, + max_value: float = None, + ) -> None: + """Create a new float setting.""" + super().__init__(name, description, default, min_value, max_value) + + @property + def value(self): + """The value of the setting. In this case, a float.""" + return self._value + + @value.setter + def value(self, value): + logger.debug(f"Setting {self.name} to {value}") + self._value = float(value) + self.settings_changed.emit() + + +class IntSetting(NumericalSetting): + """A setting that is an Integer. + + Args: + name (str) : The name of the setting + default : The default value of the setting + description (str) : A description of the setting + min_value : The minimum value of the setting + max_value : The maximum value of the setting + """ + + def __init__( + self, + name: str, + default: int, + description: str, + min_value=None, + max_value=None, + spin_box: tuple = (False, False), + ) -> None: + """Create a new int setting.""" + self.spin_box = spin_box + super().__init__(name, description, default, min_value, max_value) + + @property + def value(self): + """The value of the setting. In this case, an int.""" + return self._value + + @value.setter + def value(self, value): + logger.debug(f"Setting {self.name} to {value}") + value = int(float(value)) + self._value = value + + +class BooleanSetting(Setting): + """A setting that is a Boolean. + + Args: + name (str) : The name of the setting + default : The default value of the setting + description (str) : A description of the setting + """ + + def __init__(self, name: str, default: bool, description: str) -> None: + """Create a new boolean setting.""" + super().__init__(name, description, default) + + + @property + def value(self): + """The value of the setting. In this case, a bool.""" + return self._value + + @value.setter + def value(self, value): + try: + self._value = bool(value) + except ValueError: + raise ValueError("Value must be a bool") + + + +class SelectionSetting(Setting): + """A setting that is a selection from a list of options. + + Args: + name (str) : The name of the setting + options (list) : A list of options to choose from + default : The default value of the setting + description (str) : A description of the setting + """ + + def __init__( + self, name: str, options: list, default: str, description: str + ) -> None: + """Create a new selection setting.""" + super().__init__(name, description, default) + # Check if default is in options + if default not in options: + raise ValueError("Default value must be one of the options") + + self.options = options + + + @property + def value(self): + """The value of the setting. In this case, a string.""" + return self._value + + @value.setter + def value(self, value): + try: + if value in self.options: + self._value = value + else: + raise ValueError("Value must be one of the options") + # This fixes a bug when creating the widget when the options are not yet set + except AttributeError: + self._value = value + self.options = [value] + self.settings_changed.emit() + + +class StringSetting(Setting): + """A setting that is a string. + + Args: + name (str) : The name of the setting + default : The default value of the setting + description (str) : A description of the setting + """ + + def __init__(self, name: str, default: str, description: str) -> None: + """Create a new string setting.""" + super().__init__(name, description, default) + + @property + def value(self): + """The value of the setting. In this case, a string.""" + return self._value + + @value.setter + def value(self, value): + try: + self._value = str(value) + except ValueError: + raise ValueError("Value must be a string") diff --git a/tests/simulator.py b/tests/simulator.py new file mode 100644 index 0000000..75e750a --- /dev/null +++ b/tests/simulator.py @@ -0,0 +1,38 @@ +# Dummy test to communicate the structure +from quackseq.pulsesequence import PulseSequence +from quackseq.event import Event +from quackseq.functions import RectFunction + + +seq = PulseSequence("test") + +tx = Event("tx", "10u") + +# tx.set_tx_amplitude(1) +#tx.set_tx_phase(0) +#tx.set_shape(RectFunction()) + +#seq.add_event(tx) + +#blank = Event("blank", "10u") + +#seq.add_event(blank) + +#rx = Event("rx", "10u") +#rx.set_rx_phase(0) +#rx.set_rx(True) + +#seq.add_event(rx) + +#TR = Event("TR", "1ms") + +#seq.add_event(TR) + +#sim = Simulator() + +#sim.set_averages(100) + +# Returns the data at the RX event +#result = sim.run(seq) + +#result.plot()