From 863753f670308157d34aca2f94e79a5310477846 Mon Sep 17 00:00:00 2001 From: jupfi Date: Thu, 30 May 2024 15:40:49 +0200 Subject: [PATCH] Working spectrometer control. --- CHANGELOG.md | 5 + LICENSE | 20 + README.md | 1 + pyproject.toml | 63 ++ src/quackseq_limenqr/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 182 bytes .../__pycache__/limenqr.cpython-312.pyc | Bin 0 -> 1649 bytes .../limenqr_controller.cpython-312.pyc | Bin 0 -> 32897 bytes .../__pycache__/limenqr_model.cpython-312.pyc | Bin 0 -> 11879 bytes src/quackseq_limenqr/limenqr.py | 24 + src/quackseq_limenqr/limenqr_controller.py | 629 ++++++++++++++++++ src/quackseq_limenqr/limenqr_model.py | 353 ++++++++++ tests/limenqr.py | 41 ++ 13 files changed, 1136 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 src/quackseq_limenqr/__init__.py create mode 100644 src/quackseq_limenqr/__pycache__/__init__.cpython-312.pyc create mode 100644 src/quackseq_limenqr/__pycache__/limenqr.cpython-312.pyc create mode 100644 src/quackseq_limenqr/__pycache__/limenqr_controller.cpython-312.pyc create mode 100644 src/quackseq_limenqr/__pycache__/limenqr_model.cpython-312.pyc create mode 100644 src/quackseq_limenqr/limenqr.py create mode 100644 src/quackseq_limenqr/limenqr_controller.py create mode 100644 src/quackseq_limenqr/limenqr_model.py create mode 100644 tests/limenqr.py 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..4cc14e7 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# quackseq-limenqr \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c01aa16 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,63 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.metadata] +allow-direct-references = true + +[project] +name = "quackseq-limenqr" +version = "0.0.1" +authors = [ + { name="jupfi", email="support@nqrduck.cool" }, +] + +description = "Simple Python script to perform magnetic resonance spectroscopy experiments with the LimeSDR." +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", + "limedriver", + "quackseq", + "h5py", + "pyserial", +] + +[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_limenqr"] \ No newline at end of file diff --git a/src/quackseq_limenqr/__init__.py b/src/quackseq_limenqr/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/quackseq_limenqr/__pycache__/__init__.cpython-312.pyc b/src/quackseq_limenqr/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7415283fa523bd6d898e29031ebef3a2377176e7 GIT binary patch literal 182 zcmX@j%ge<81eSi`X(0MBh(HIQS%4zb87dhx8U0o=6fpsLpFwJVh3aSI=cejsl@_FB z>bvA8m*%GCl@#myCKi{Z78MlbXQd{W=ogkICTAC?7V73?0;LO!^oxs<;ezon!T9*h pyv&mLc)fzkUmP~M`6;D2sdh!IKwB7rxERFv$jr#dSi}ru0RW$SFm3<< literal 0 HcmV?d00001 diff --git a/src/quackseq_limenqr/__pycache__/limenqr.cpython-312.pyc b/src/quackseq_limenqr/__pycache__/limenqr.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6ac033d97ae4c10acd06752336431ba79e5ce332 GIT binary patch literal 1649 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 literal 0 HcmV?d00001 diff --git a/src/quackseq_limenqr/__pycache__/limenqr_model.cpython-312.pyc b/src/quackseq_limenqr/__pycache__/limenqr_model.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c65823e50dd1af839d945cdf7798b5137b5698c6 GIT binary patch literal 11879 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 h*YVgB*z*J)ocXtCm literal 0 HcmV?d00001 diff --git a/src/quackseq_limenqr/limenqr.py b/src/quackseq_limenqr/limenqr.py new file mode 100644 index 0000000..19151f3 --- /dev/null +++ b/src/quackseq_limenqr/limenqr.py @@ -0,0 +1,24 @@ +from quackseq.spectrometer.spectrometer import Spectrometer + +from .limenqr_model import LimeNQRModel +from .limenqr_controller import LimeNQRController + + +class LimeNQR(Spectrometer): + def __init__(self): + self.model = LimeNQRModel() + self.controller = LimeNQRController(self) + + def run_sequence(self, sequence): + result = self.controller.run_sequence(sequence) + return result + + def set_averages(self, value: int): + self.model.averages = value + + def set_frequency(self, value: float): + self.model.target_frequency = value + + @property + def settings(self): + return self.model.settings diff --git a/src/quackseq_limenqr/limenqr_controller.py b/src/quackseq_limenqr/limenqr_controller.py new file mode 100644 index 0000000..ce672f4 --- /dev/null +++ b/src/quackseq_limenqr/limenqr_controller.py @@ -0,0 +1,629 @@ +"""Controller module for the Lime NQR spectrometer.""" + +import logging +from datetime import datetime +import tempfile +from pathlib import Path +import numpy as np +from scipy.signal import resample + +from limedriver.binding import PyLimeConfig +from limedriver.hdf_reader import HDF + +from nqrduck.helpers.unitconverter import UnitConverter +from quackseq.spectrometer.spectrometer_controller import SpectrometerController +from quackseq.measurement import Measurement +from quackseq.pulseparameters import TXPulse, RXReadout +from quackseq.pulsesequence import QuackSequence + + +logger = logging.getLogger(__name__) + + +class LimeNQRController(SpectrometerController): + """Controller class for the Lime NQR spectrometer.""" + + def __init__(self, limenqr): + """Initializes the SimulatorController.""" + super().__init__() + self.limenqr = limenqr + + def run_sequence(self, sequence : QuackSequence): + """Starts the measurement procedure.""" + + lime = self.initialize_lime(sequence) + if lime is None: + # Emit error message + logger.error("Error initializing Lime driver") + return -1 + elif lime.Npulses == 0: + # Emit error message + logger.error("No pulses in the pulse sequence") + return -1 + + self.setup_lime_parameters(lime, sequence) + self.setup_temporary_storage(lime) + + + if not self.perform_measurement(lime): + self.emit_status_message("Measurement failed") + self.emit_measurement_error( + "Error with measurement data. Did you set an RX event?" + ) + return -1 + + measurement_data = self.process_measurement_results(lime, sequence) + + # Resample the RX data to the dwell time settings + dwell_time = self.limenqr.model.settings.rx_dwell_time + + dwell_time = UnitConverter.to_float(dwell_time) * 1e6 + logger.debug("Dwell time: %s", dwell_time) + logger.debug(f"Last tdx value: {measurement_data.tdx[-1]}") + + if dwell_time: + n_data_points = int(measurement_data.tdx[-1] / dwell_time) + logger.debug("Resampling to %s data points", n_data_points) + tdx = np.linspace( + 0, measurement_data.tdx[-1], n_data_points, endpoint=False + ) + tdy = resample(measurement_data.tdy, n_data_points) + name = measurement_data.name + measurement_data = Measurement( + name, + tdx, + tdy, + self.limenqr.model.target_frequency, + IF_frequency=self.limenqr.model.if_frequency, + ) + + if measurement_data: + return measurement_data + + def initialize_lime(self, sequence : QuackSequence) -> PyLimeConfig: + """Initializes the limr object that is used to communicate with the pulseN driver. + + Returns: + PyLimeConfig: The PyLimeConfig object that is used to communicate with the pulseN driver + """ + try: + n_pulses = self.get_number_of_pulses(sequence) + lime = PyLimeConfig(n_pulses) + return lime + except ImportError as e: + logger.error("Error while importing limr: %s", e) + except Exception as e: + logger.error("Error while initializing Lime driver: %s", e) + import traceback + + traceback.print_exc() + + return None + + def setup_lime_parameters(self, lime: PyLimeConfig, sequence : QuackSequence) -> None: + """Sets the parameters of the lime config according to the settings set in the spectrometer module. + + Args: + lime (PyLimeConfig): The PyLimeConfig object that is used to communicate with the pulseN driver + """ + # lime.noi = -1 + lime.override_init = -1 + # + # lime.nrp = 1 + lime.repetitions = 1 + lime = self.update_settings(lime) + lime = self.translate_pulse_sequence(lime, sequence) + lime.averages = self.limenqr.model.averages + self.log_lime_parameters(lime) + + def setup_temporary_storage(self, lime: PyLimeConfig) -> None: + """Sets up the temporary storage for the measurement data. + + Args: + lime (PyLimeConfig): The PyLimeConfig object that is used to communicate with the pulseN driver + """ + temp_dir = tempfile.TemporaryDirectory() + logger.debug("Created temporary directory at: %s", temp_dir.name) + lime.save_path = str(Path(temp_dir.name)) + "/" # Temporary storage path + lime.file_pattern = "temp" # Temporary filename prefix or related config + + def perform_measurement(self, lime: PyLimeConfig) -> bool: + """Executes the measurement procedure. + + Args: + lime (PyLimeConfig): The PyLimeConfig object that is used to communicate with the pulseN driver + + Returns: + bool: True if the measurement was successful, False otherwise + """ + logger.debug("Running the measurement procedure") + try: + lime.run() + return True + except Exception as e: + logger.error("Failed to execute the measurement: %s", e) + return False + + def process_measurement_results(self, lime: PyLimeConfig, sequence : QuackSequence) -> Measurement: + """Processes the measurement results and returns a Measurement object. + + Args: + lime (PyLimeConfig): The PyLimeConfig object that is used to communicate with the pulseN driver + + Returns: + Measurement: The measurement data + """ + rx_begin, rx_stop = self.translate_rx_event(lime, sequence) + if rx_begin is None or rx_stop is None: + # Instead print the whole acquisition range + rx_begin = 0 + rx_stop = lime.rectime_secs * 1e6 + + logger.debug("RX event begins at: %sµs and ends at: %sµs", rx_begin, rx_stop) + return self.calculate_measurement_data(lime, rx_begin, rx_stop) + + def calculate_measurement_data( + self, lime: PyLimeConfig, rx_begin: float, rx_stop: float + ) -> Measurement: + """Calculates the measurement data from the limr object. + + Args: + lime (PyLimeConfig): The PyLimeConfig object that is used to communicate with the pulseN driver + rx_begin (float): The start time of the RX event in µs + rx_stop (float): The stop time of the RX event in µs + + Returns: + Measurement: The measurement data + """ + try: + path = lime.get_path() + hdf = HDF(path) + evidx = self.find_evaluation_range_indices(hdf, rx_begin, rx_stop) + tdx, tdy = self.extract_measurement_data(lime, hdf, evidx) + fft_shift = self.get_fft_shift() + # Measurement name date + module + target frequency + averages + sequence name + name = f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - LimeNQR - {self.limenqr.model.target_frequency / 1e6} MHz - {self.limenqr.model.averages} averages" + logger.debug(f"Measurement name: {name}") + return Measurement( + name, + tdx, + tdy, + self.limenqr.model.target_frequency, + frequency_shift=fft_shift, + IF_frequency=self.limenqr.model.if_frequency, + ) + except Exception as e: + logger.error("Error processing measurement result: %s", e) + return None + + def find_evaluation_range_indices( + self, hdf: HDF, rx_begin: float, rx_stop: float + ) -> list: + """Finds the indices of the evaluation range in the measurement data. + + Args: + hdf (HDF): The HDF object that is used to read the measurement data + rx_begin (float): The start time of the RX event in µs + rx_stop (float): The stop time of the RX event in µs + + Returns: + list: The indices of the evaluation range in the measurement data + """ + return np.where((hdf.tdx > rx_begin) & (hdf.tdx < rx_stop))[0] + + def extract_measurement_data( + self, lime: PyLimeConfig, hdf: HDF, indices: list + ) -> tuple: + """Extracts the measurement data from the PyLimeConfig object. + + Args: + lime (PyLimeConfig): The PyLimeConfig object that is used to communicate with the pulseN driver + hdf (HDF): The HDF object that is used to read the measurement data + indices (list): The indices of the evaluation range in the measurement data + + Returns: + tuple: A tuple containing the time vector and the measurement data + """ + tdx = hdf.tdx[indices] - hdf.tdx[indices][0] + tdy = hdf.tdy[indices] / lime.averages + # flatten the tdy array + tdy = tdy.flatten() + return tdx, tdy + + def get_fft_shift(self) -> int: + """Rreturns the FFT shift value from the settings. + + Returns: + int: The FFT shift value + """ + fft_shift_enabled = self.limenqr.model.settings.fft_shift + + return self.limenqr.model.if_frequency if fft_shift_enabled else 0 + + def log_lime_parameters(self, lime: PyLimeConfig) -> None: + """Logs the parameters of the limr object. + + Args: + lime (PyLimeConfig): The PyLimeConfig object that is used to communicate with the pulseN driver + """ + # for key, value in lime.__dict__.items(): + # logger.debug("Lime parameter %s has value %s", key, value) + logger.debug("Lime parameter %s has value %s", "srate", lime.srate) + + def update_settings(self, lime: PyLimeConfig) -> PyLimeConfig: + """Sets the parameters of the limr object according to the settings set in the spectrometer module. + + Args: + lime (PyLimeConfig): The PyLimeConfig object that is used to communicate with the pulseN driver + + Returns: + lime: The updated limr object + """ + c3_tim = [0, 0, 0, 0] + + lime.srate = float(self.limenqr.model.settings.sampling_frequency) + lime.channel = int(self.limenqr.model.settings.channel) + lime.TX_matching = int(self.limenqr.model.settings.tx_matching) + lime.RX_matching = int(self.limenqr.model.settings.rx_matching) + lime.frq = self.limenqr.model.target_frequency - self.limenqr.model.if_frequency + lime.rectime_secs = self.limenqr.model.settings.acquisition_time + + c3_tim[0] = self.limenqr.model.settings.gate_enable + c3_tim[1] = self.limenqr.model.settings.gate_padding_left + c3_tim[2] = self.limenqr.model.settings.gate_shift + c3_tim[3] = self.limenqr.model.settings.gate_padding_right + + lime.TX_gain = self.limenqr.model.settings.tx_gain + lime.RX_gain = self.limenqr.model.settings.rx_gain + lime.RX_LPF = self.limenqr.model.settings.rx_lpf_bw + lime.TX_LPF = self.limenqr.model.settings.tx_lpf_bw + lime.TX_IcorrDC = self.limenqr.model.settings.tx_i_dc_correction + lime.TX_QcorrDC = self.limenqr.model.settings.tx_q_dc_correction + lime.TX_IcorrGain = self.limenqr.model.settings.tx_i_gain_correction + lime.TX_QcorrGain = self.limenqr.model.settings.tx_q_gain_correction + lime.TX_IQcorrPhase = self.limenqr.model.settings.tx_phase_adjustment + lime.RX_IcorrGain = self.limenqr.model.settings.rx_i_gain_correction + lime.RX_QcorrGain = self.limenqr.model.settings.rx_q_gain_correction + lime.RX_IQcorrPhase = self.limenqr.model.settings.rx_phase_adjustment + + # This needs to be done in one go, otherwise not all values will be updated + lime.c3_tim = c3_tim + return lime + + def translate_pulse_sequence(self, lime: PyLimeConfig, sequence : QuackSequence) -> PyLimeConfig: + """Ttranslates the pulse sequence to the limr object. + + Args: + lime (PyLimeConfig): The PyLimeConfig object that is used to communicate with the pulseN driver + """ + events = sequence.events + + first_pulse = True + + for event in events: + self.log_event_details(event) + for parameter in event.parameters.values(): + self.log_parameter_details(parameter) + + if self.is_translatable_tx_parameter(parameter, sequence): + pulse_shape, pulse_amplitude = self.prepare_pulse_amplitude( + event, parameter + ) + pulse_amplitude, modulated_phase = self.modulate_pulse_amplitude( + pulse_amplitude, event, lime + ) + + if first_pulse: # If the pulse frequency list is empty + pfr, pdr, pam, pof, pph = self.initialize_pulse_lists( + lime, pulse_amplitude, pulse_shape, modulated_phase + ) + first_pulse = False + else: + pfr_ext, pdr_ext, pam_ext, pph_ext = self.extend_pulse_lists( + pulse_amplitude, pulse_shape, modulated_phase + ) + pof_ext = self.calculate_and_set_offsets( + lime, pulse_shape, events, event, pulse_amplitude + ) + + pfr.extend(pfr_ext) + pdr.extend(pdr_ext) + pam.extend(pam_ext) + pof.extend(pof_ext) + pph.extend(pph_ext) + + lime.p_frq = pfr + lime.p_dur = pdr + lime.p_amp = pam + lime.p_offs = pof + lime.p_pha = pph + # Set repetition time event as last event's duration and update number of pulses + lime.reptime_secs = float(event.duration) + lime.Npulses = len(lime.p_frq) + return lime + + def get_number_of_pulses(self, sequence : QuackSequence) -> int: + """Calculates the number of pulses in the pulse sequence before the LimeDriverBinding is initialized. + + This makes sure it"s initialized with the correct size of the pulse lists. + + Returns: + int: The number of pulses in the pulse sequence + """ + events = sequence.events + num_pulses = 0 + for event in events: + for parameter in event.parameters.values(): + if self.is_translatable_tx_parameter(parameter, sequence): + _, pulse_amplitude = self.prepare_pulse_amplitude(event, parameter) + num_pulses += len(pulse_amplitude) + logger.debug("Number of pulses: %s", num_pulses) + + return num_pulses + + + def log_event_details(self, event) -> None: + """Logs the details of an event.""" + logger.debug("Event %s has parameters: %s", event.name, event.parameters) + + def log_parameter_details(self, parameter) -> None: + """Logs the details of a parameter.""" + logger.debug("Parameter %s has options: %s", parameter.name, parameter.options) + + def is_translatable_tx_parameter(self, parameter, sequence : QuackSequence) -> bool: + """Checks if a parameter a pulse with a transmit pulse shape (amplitude nonzero). + + Args: + parameter (Parameter): The parameter to check + """ + return ( + parameter.name == sequence.TX_PULSE + and parameter.get_option_by_name(TXPulse.RELATIVE_AMPLITUDE).value > 0 + ) + + def prepare_pulse_amplitude(self, event, parameter): + """Prepares the pulse amplitude for the limr object. + + Args: + event (Event): The event that contains the parameter + parameter (Parameter): The parameter that contains the pulse shape and amplitude + + Returns: + tuple: A tuple containing the pulse shape and the pulse amplitude + """ + pulse_shape = parameter.get_option_by_name(TXPulse.TX_PULSE_SHAPE).value + pulse_amplitude = abs(pulse_shape.get_pulse_amplitude(event.duration)) * ( + parameter.get_option_by_name(TXPulse.RELATIVE_AMPLITUDE).value / 100 + ) + pulse_amplitude = np.clip(pulse_amplitude, -0.99, 0.99) + + return pulse_shape, pulse_amplitude + + def modulate_pulse_amplitude( + self, pulse_amplitude: float, event, lime: PyLimeConfig + ) -> tuple: + """Modulates the pulse amplitude for the limr object. We need to do this to have the pulse at IF frequency instead of LO frequency. + + Args: + pulse_amplitude (float): The pulse amplitude + event (Event): The event that contains the parameter + lime (PyLimeConfig) : The PyLimeConfig object that is used to communicate with the pulseN driver + + Returns: + tuple: A tuple containing the modulated pulse amplitude and the modulated phase + """ + # num_samples = int(float(event.duration) * lime.sra) + num_samples = int(float(event.duration) * lime.srate) + tdx = np.linspace(0, float(event.duration), num_samples, endpoint=False) + shift_signal = np.exp(1j * 2 * np.pi * self.limenqr.model.if_frequency * tdx) + + # The pulse amplitude needs to be resampled to the number of samples + logger.debug("Resampling pulse amplitude to %s samples", num_samples) + pulse_amplitude = resample(pulse_amplitude, num_samples) + + pulse_complex = pulse_amplitude * shift_signal + modulated_amplitude = np.abs(pulse_complex) + modulated_phase = self.unwrap_phase(np.angle(pulse_complex)) + return modulated_amplitude, modulated_phase + + def unwrap_phase(self, phase: float) -> float: + """This method unwraps the phase of the pulse. + + Args: + phase (float): The phase of the pulse + """ + return (np.unwrap(phase) + 2 * np.pi) % (2 * np.pi) + + def initialize_pulse_lists( + self, + lime: PyLimeConfig, + pulse_amplitude: np.array, + pulse_shape, + modulated_phase: np.array, + ) -> tuple: + """This method initializes the pulse lists of the limr object. + + Args: + lime (PyLimeConfig): The PyLimeConfig object that is used to communicate with the pulseN driver + pulse_amplitude (np.array): The pulse amplitude + pulse_shape (Function): The pulse shape + modulated_phase (np.array): The modulated phase + """ + pfr = [float(self.limenqr.model.if_frequency)] * len(pulse_amplitude) + # We set the first len(pulse_amplitude) of the p_dur + pdr = [float(pulse_shape.resolution)] * len(pulse_amplitude) + pam = list(pulse_amplitude) + pof = [self.limenqr.model.OFFSET_FIRST_PULSE] + [ + int(pulse_shape.resolution * lime.srate) + ] * (len(pulse_amplitude) - 1) + pph = list(modulated_phase) + + return pfr, pdr, pam, pof, pph + + def extend_pulse_lists(self, pulse_amplitude, pulse_shape, modulated_phase): + """This method extends the pulse lists of the limr object. + + Args: + pulse_amplitude (float): The pulse amplitude + pulse_shape (PulseShape): The pulse shape + modulated_phase (float): The modulated phase + + Returns: + tuple: A tuple containing the extended pulse lists (frequency, duration, amplitude, phase) + """ + pfr = [float(self.limenqr.model.if_frequency)] * len(pulse_amplitude) + pdr = [float(pulse_shape.resolution)] * len(pulse_amplitude) + pam = list(pulse_amplitude) + pph = list(modulated_phase) + + return pfr, pdr, pam, pph + + def calculate_and_set_offsets( + self, lime: PyLimeConfig, pulse_shape, events, current_event, pulse_amplitude + ) -> list: + """This method calculates and sets the offsets for the limr object. + + Args: + lime (PyLimeConfig): The PyLimeConfig object that is used to communicate with the pulseN driver + pulse_shape (Function): The pulse shape + events (list): The pulse sequence events + current_event (Event): The current event + pulse_amplitude (float): The pulse amplitude + + Returns: + list: The offsets for the limr object + """ + blank_durations = self.get_blank_durations_before_event(events, current_event) + + # Calculate the total time that has passed before the current event + total_blank_duration = sum(blank_durations) + # Calculate the offset for the current pulse + # The first pulse offset is already set, so calculate subsequent ones + offset_for_current_pulse = int(np.ceil(total_blank_duration * lime.srate)) + + # Offset for the current pulse should be added only once + pof = [(offset_for_current_pulse)] + + # Set the offset for the remaining samples of the current pulse (excluding the first sample) + # We subtract 1 because we have already set the offset for the current pulse's first sample + offset_per_sample = int(float(pulse_shape.resolution) * lime.srate) + pof.extend([offset_per_sample] * (len(pulse_amplitude) - 1)) + return pof + + # This method could be refactored in a potential pulse sequence module + def get_blank_durations_before_event(self, events, current_event) -> list: + """This method returns the blank durations before the current event. + + Args: + events (list): The pulse sequence events + current_event (Event): The current event + + Returns: + list: The blank durations before the current event + """ + blank_durations = [] + previous_events_without_tx_pulse = self.get_previous_events_without_tx_pulse( + events, current_event + ) + for event in previous_events_without_tx_pulse: + blank_durations.append(float(event.duration)) + return blank_durations + + # This method could be refactored in a potential pulse sequence module + def get_previous_events_without_tx_pulse(self, events, current_event) -> list: + """This method returns the previous events without a transmit pulse. + + Args: + events (list): The pulse sequence events + current_event (Event): The current event + + Returns: + list: The previous events without a transmit pulse + """ + index = events.index(current_event) + previous_events = events[:index] + result = [] + for event in reversed(previous_events): + translatable = any( + self.is_translatable_tx_parameter(param) + for param in event.parameters.values() + ) + if not translatable: + result.append(event) + else: + break + return reversed( + result + ) # Reversed to maintain the original order if needed elsewhere + + def translate_rx_event(self, lime: PyLimeConfig, sequence : QuackSequence) -> tuple: + """This method translates the RX event of the pulse sequence to the limr object. + + Args: + lime (PyLimeConfig): The PyLimeConfig object that is used to communicate with the pulseN driver + + Returns: + tuple: A tuple containing the start and stop time of the RX event in µs + """ + CORRECTION_FACTOR = self.limenqr.model.settings.rx_offset + + events = sequence.events + + rx_event = self.find_rx_event(events) + if not rx_event: + return None, None + + previous_events_duration = self.calculate_previous_events_duration( + events, rx_event + ) + rx_duration = float(rx_event.duration) + + offset = self.calculate_offset(lime) + + rx_begin = ( + float(previous_events_duration) + float(offset) + float(CORRECTION_FACTOR) + ) + rx_stop = rx_begin + rx_duration + return rx_begin * 1e6, rx_stop * 1e6 + + def find_rx_event(self, events): + """This method finds the RX event in the pulse sequence. + + Args: + events (list): The pulse sequence events + + Returns: + Event: The RX event + """ + for event in events: + parameter = event.parameters.get(RXReadout.RX) + if parameter and parameter.get_option_by_name(RXReadout.RX).value: + self.log_event_details(event) + self.log_parameter_details(parameter) + return event + return None + + def calculate_previous_events_duration(self, events, rx_event): + """This method calculates the duration of the previous events. + + Args: + events (list): The pulse sequence events + rx_event (Event): The RX event + + Returns: + float: The duration of the previous events + """ + previous_events = events[: events.index(rx_event)] + return sum(event.duration for event in previous_events) + + def calculate_offset(self, lime: PyLimeConfig) -> float: + """This method calculates the offset for the RX event. + + Args: + lime (limr): The limr object that is used to communicate with the pulseN driver + + Returns: + float: The offset for the RX event + """ + return self.limenqr.model.OFFSET_FIRST_PULSE * (1 / lime.srate) + diff --git a/src/quackseq_limenqr/limenqr_model.py b/src/quackseq_limenqr/limenqr_model.py new file mode 100644 index 0000000..549faa5 --- /dev/null +++ b/src/quackseq_limenqr/limenqr_model.py @@ -0,0 +1,353 @@ +"""Model for the Lime NQR spectrometer.""" + +import logging +from quackseq.spectrometer.spectrometer_model import SpectrometerModel +from quackseq.spectrometer.spectrometer_settings import ( + FloatSetting, + IntSetting, + BooleanSetting, + SelectionSetting, + StringSetting, +) + +logger = logging.getLogger(__name__) + + +class LimeNQRModel(SpectrometerModel): + """Model for the Lime NQR spectrometer.""" + + # Setting constants for the names of the spectrometer settings + CHANNEL = "TX/RX Channel" + TX_MATCHING = "TX Matching" + RX_MATCHING = "RX Matching" + SAMPLING_FREQUENCY = "Sampling Frequency (Hz)" + RX_DWELL_TIME = "RX Dwell Time (s)" + IF_FREQUENCY = "IF Frequency (Hz)" + ACQUISITION_TIME = "Acquisition time (s)" + GATE_ENABLE = "Enable" + GATE_PADDING_LEFT = "Gate padding left" + GATE_PADDING_RIGHT = "Gate padding right" + GATE_SHIFT = "Gate shift" + RX_GAIN = "RX Gain" + TX_GAIN = "TX Gain" + RX_LPF_BW = "RX LPF BW (Hz)" + TX_LPF_BW = "TX LPF BW (Hz)" + TX_I_DC_CORRECTION = "TX I DC correction" + TX_Q_DC_CORRECTION = "TX Q DC correction" + TX_I_GAIN_CORRECTION = "TX I Gain correction" + TX_Q_GAIN_CORRECTION = "TX Q Gain correction" + TX_PHASE_ADJUSTMENT = "TX phase adjustment" + RX_I_DC_CORRECTION = "RX I DC correction" + RX_Q_DC_CORRECTION = "RX Q DC correction" + RX_I_GAIN_CORRECTION = "RX I Gain correction" + RX_Q_GAIN_CORRECTION = "RX Q Gain correction" + RX_PHASE_ADJUSTMENT = "RX phase adjustment" + RX_OFFSET = "RX offset" + FFT_SHIFT = "FFT shift" + + # Constants for the Categories of the settings + ACQUISITION = "Acquisition" + GATE_SETTINGS = "Gate Settings" + RX_TX_SETTINGS = "RX/TX Settings" + CALIBRATION = "Calibration" + SIGNAL_PROCESSING = "Signal Processing" + + # Pulse parameter constants + TX = "TX" + RX = "RX" + + # Settings that are not changed by the user + OFFSET_FIRST_PULSE = 300 + + def __init__(self) -> None: + """Initializes the Lime NQR model.""" + super().__init__() + # Acquisition settings + channel_options = ["0", "1"] + channel_setting = SelectionSetting( + self.CHANNEL, self.ACQUISITION, channel_options, "0", "TX/RX Channel" + ) + self.add_setting("channel", channel_setting) + + tx_matching_options = ["0", "1"] + tx_matching_setting = SelectionSetting( + self.TX_MATCHING, self.ACQUISITION, tx_matching_options, "0", "TX Matching" + ) + self.add_setting("tx_matching", tx_matching_setting) + + rx_matching_options = ["0", "1"] + rx_matching_setting = SelectionSetting( + self.RX_MATCHING, self.ACQUISITION, rx_matching_options, "0", "RX Matching" + ) + self.add_setting("rx_matching", rx_matching_setting) + + sampling_frequency_options = [ + "30.72e6", + "15.36e6", + "7.68e6", + ] + sampling_frequency_setting = SelectionSetting( + self.SAMPLING_FREQUENCY, + self.ACQUISITION, + sampling_frequency_options, + "30.72e6", + "The rate at which the spectrometer samples the input signal.", + ) + self.add_setting("sampling_frequency", sampling_frequency_setting) + + rx_dwell_time_setting = StringSetting( + self.RX_DWELL_TIME, + self.ACQUISITION, + "22n", + "The time between samples in the receive path.", + ) + self.add_setting("rx_dwell_time", rx_dwell_time_setting) + + if_frequency_setting = FloatSetting( + self.IF_FREQUENCY, + self.ACQUISITION, + 5e6, + "The intermediate frequency to which the input signal is down converted during analog-to-digital conversion.", + min_value=0, + ) + self.add_setting("if_frequency", if_frequency_setting) + self.if_frequency = 5e6 + + acquisition_time_setting = FloatSetting( + self.ACQUISITION_TIME, + self.ACQUISITION, + 82e-6, + "Acquisition time - this is from the beginning of the pulse sequence", + min_value=0, + ) + self.add_setting("acquisition_time", acquisition_time_setting) + + # Gate Settings + gate_enable_setting = BooleanSetting( + self.GATE_ENABLE, + self.GATE_SETTINGS, + True, + "Setting that controls whether gate is on during transmitting.", + ) + self.add_setting("gate_enable", gate_enable_setting) + + gate_padding_left_setting = IntSetting( + self.GATE_PADDING_LEFT, + self.GATE_SETTINGS, + 10, + "The number of samples by which to extend the gate window to the left.", + min_value=0, + ) + self.add_setting("gate_padding_left", gate_padding_left_setting) + + gate_padding_right_setting = IntSetting( + self.GATE_PADDING_RIGHT, + self.GATE_SETTINGS, + 10, + "The number of samples by which to extend the gate window to the right.", + min_value=0, + ) + self.add_setting("gate_padding_right", gate_padding_right_setting) + + gate_shift_setting = IntSetting( + self.GATE_SHIFT, + self.GATE_SETTINGS, + 53, + "The delay, in number of samples, by which the gate window is shifted.", + min_value=0, + ) + self.add_setting("gate_shift", gate_shift_setting) + + # RX/TX settings + rx_gain_setting = IntSetting( + self.RX_GAIN, + self.RX_TX_SETTINGS, + 55, + "The gain level of the receiver’s amplifier.", + min_value=0, + max_value=55, + slider=True, + ) + self.add_setting("rx_gain", rx_gain_setting) + + tx_gain_setting = IntSetting( + self.TX_GAIN, + self.RX_TX_SETTINGS, + 30, + "The gain level of the transmitter’s amplifier.", + min_value=0, + max_value=55, + slider=True, + ) + self.add_setting("tx_gain", tx_gain_setting) + + rx_lpf_bw_setting = FloatSetting( + self.RX_LPF_BW, + self.RX_TX_SETTINGS, + 30.72e6 / 2, + "The bandwidth of the receiver’s low-pass filter which attenuates frequencies below a certain threshold.", + ) + self.add_setting("rx_lpf_bw", rx_lpf_bw_setting) + + tx_lpf_bw_setting = FloatSetting( + self.TX_LPF_BW, + self.RX_TX_SETTINGS, + 130.0e6, + "The bandwidth of the transmitter’s low-pass filter which limits the frequency range of the transmitted signal.", + ) + self.add_setting("tx_lpf_bw", tx_lpf_bw_setting) + + # Calibration settings + tx_i_dc_correction_setting = IntSetting( + self.TX_I_DC_CORRECTION, + self.CALIBRATION, + -45, + "Adjusts the direct current offset errors in the in-phase (I) component of the transmit (TX) path.", + min_value=-128, + max_value=127, + slider=True, + ) + self.add_setting("tx_i_dc_correction", tx_i_dc_correction_setting) + + tx_q_dc_correction_setting = IntSetting( + self.TX_Q_DC_CORRECTION, + self.CALIBRATION, + 0, + "Adjusts the direct current offset errors in the quadrature (Q) component of the transmit (TX) path.", + min_value=-128, + max_value=127, + slider=True, + ) + self.add_setting("tx_q_dc_correction", tx_q_dc_correction_setting) + + tx_i_gain_correction_setting = IntSetting( + self.TX_I_GAIN_CORRECTION, + self.CALIBRATION, + 2047, + "Modifies the gain settings for the I channel of the TX path, adjusting for imbalances.", + min_value=0, + max_value=2047, + slider=True, + ) + self.add_setting("tx_i_gain_correction", tx_i_gain_correction_setting) + + tx_q_gain_correction_setting = IntSetting( + self.TX_Q_GAIN_CORRECTION, + self.CALIBRATION, + 2039, + "Modifies the gain settings for the Q channel of the TX path, adjusting for imbalances.", + min_value=0, + max_value=2047, + slider=True, + ) + self.add_setting("tx_q_gain_correction", tx_q_gain_correction_setting) + + tx_phase_adjustment_setting = IntSetting( + self.TX_PHASE_ADJUSTMENT, + self.CALIBRATION, + 3, + "Corrects the Phase of I Q signals in the TX path.", + min_value=-2048, + max_value=2047, + slider=True, + ) + self.add_setting("tx_phase_adjustment", tx_phase_adjustment_setting) + + rx_i_dc_correction_setting = IntSetting( + self.RX_I_DC_CORRECTION, + self.CALIBRATION, + 0, + "Adjusts the direct current offset errors in the in-phase (I) component of the receive (RX) path.", + min_value=-63, + max_value=63, + slider=True, + ) + self.add_setting("rx_i_dc_correction", rx_i_dc_correction_setting) + + rx_q_dc_correction_setting = IntSetting( + self.RX_Q_DC_CORRECTION, + self.CALIBRATION, + 0, + "Adjusts the direct current offset errors in the quadrature (Q) component of the receive (RX) path.", + min_value=-63, + max_value=63, + slider=True, + ) + self.add_setting("rx_q_dc_correction", rx_q_dc_correction_setting) + + rx_i_gain_correction_setting = IntSetting( + self.RX_I_GAIN_CORRECTION, + self.CALIBRATION, + 2047, + "Modifies the gain settings for the I channel of the RX path, adjusting for imbalances.", + min_value=0, + max_value=2047, + slider=True, + ) + self.add_setting("rx_i_gain_correction", rx_i_gain_correction_setting) + + rx_q_gain_correction_setting = IntSetting( + self.RX_Q_GAIN_CORRECTION, + self.CALIBRATION, + 2047, + "Modifies the gain settings for the Q channel of the RX path, adjusting for imbalances.", + min_value=0, + max_value=2047, + slider=True, + ) + self.add_setting("rx_q_gain_correction", rx_q_gain_correction_setting) + + rx_phase_adjustment_setting = IntSetting( + self.RX_PHASE_ADJUSTMENT, + self.CALIBRATION, + 0, + "Corrects the Phase of I Q signals in the RX path.", + min_value=-2048, + max_value=2047, + slider=True, + ) + self.add_setting("rx_phase_adjustment", rx_phase_adjustment_setting) + + # Signal Processing settings + rx_offset_setting = FloatSetting( + self.RX_OFFSET, + self.SIGNAL_PROCESSING, + 2.4e-6, + "The offset of the RX event, this changes all the time", + ) + self.add_setting("rx_offset", rx_offset_setting) + + fft_shift_setting = BooleanSetting( + self.FFT_SHIFT, self.SIGNAL_PROCESSING, False, "FFT shift" + ) + self.add_setting("fft_shift", fft_shift_setting) + + self.averages = 1 + self.target_frequency = 100e6 + + @property + def target_frequency(self): + """The target frequency of the spectrometer.""" + return self._target_frequency + + @target_frequency.setter + def target_frequency(self, value): + self._target_frequency = value + + @property + def averages(self): + """The number of averages to be taken.""" + return self._averages + + @averages.setter + def averages(self, value): + self._averages = value + + @property + def if_frequency(self): + """The intermediate frequency to which the input signal is down converted during analog-to-digital conversion.""" + return self._if_frequency + + @if_frequency.setter + def if_frequency(self, value): + self._if_frequency = value diff --git a/tests/limenqr.py b/tests/limenqr.py new file mode 100644 index 0000000..d3b11e7 --- /dev/null +++ b/tests/limenqr.py @@ -0,0 +1,41 @@ +import unittest +import logging +import matplotlib.pyplot as plt +from quackseq.pulsesequence import QuackSequence +from quackseq.event import Event +from quackseq.functions import RectFunction, SincFunction +from quackseq_limenqr.limenqr import LimeNQR + +logging.basicConfig(level=logging.DEBUG) + +class TestQuackSequence(unittest.TestCase): + + def test_event_creation(self): + seq = QuackSequence("test - simulation run sequence") + + loopback = Event("tx", "15u", seq) + seq.add_event(loopback) + seq.set_tx_amplitude(loopback, 1) + seq.set_tx_phase(loopback, 0) + + rect = SincFunction() + seq.set_tx_shape(loopback, rect) + + seq.set_rx(loopback, True) + + TR = Event("TR", "1m", seq) + seq.add_event(TR) + + lime = LimeNQR() + lime.set_averages(1000) + lime.set_frequency(100e6) + lime.settings.channel = "0" + lime.settings.tx_gain = 30 + + result = lime.run_sequence(seq) + + #plt.plot(result.tdx, abs(result.tdy)) + #plt.show() + +if __name__ == "__main__": + unittest.main() \ No newline at end of file