From f4ac14661168467fcba8873af2811e90100d59fd Mon Sep 17 00:00:00 2001 From: jupfi Date: Wed, 29 May 2024 07:38:03 +0200 Subject: [PATCH] Working simulation. --- src/quackseq/spectrometer/simulator.py | 17 + .../spectrometer/simulator_controller.py | 354 ++++++++++++++++++ src/quackseq/spectrometer/simulator_model.py | 327 ++++++++++++++++ src/quackseq/spectrometer/spectrometer.py | 39 ++ .../spectrometer/spectrometer_controller.py | 19 + .../spectrometer/spectrometer_model.py | 14 +- ...er_setting.py => spectrometer_settings.py} | 3 - tests/simulator.py | 18 +- 8 files changed, 771 insertions(+), 20 deletions(-) create mode 100644 src/quackseq/spectrometer/simulator.py create mode 100644 src/quackseq/spectrometer/simulator_controller.py create mode 100644 src/quackseq/spectrometer/simulator_model.py create mode 100644 src/quackseq/spectrometer/spectrometer.py create mode 100644 src/quackseq/spectrometer/spectrometer_controller.py rename src/quackseq/spectrometer/{spectrometer_setting.py => spectrometer_settings.py} (98%) diff --git a/src/quackseq/spectrometer/simulator.py b/src/quackseq/spectrometer/simulator.py new file mode 100644 index 0000000..fc39f0d --- /dev/null +++ b/src/quackseq/spectrometer/simulator.py @@ -0,0 +1,17 @@ +from quackseq.spectrometer.spectrometer import Spectrometer + +from .simulator_model import SimulatorModel +from .simulator_controller import SimulatorController + +class Simulator(Spectrometer): + + def __init__(self): + self.model = SimulatorModel() + self.controller = SimulatorController(self.model) + + def run_sequence(self, sequence): + result =self.controller.run_sequence(sequence) + return result + + def set_averages(self, value: int): + self.model.average = value \ No newline at end of file diff --git a/src/quackseq/spectrometer/simulator_controller.py b/src/quackseq/spectrometer/simulator_controller.py new file mode 100644 index 0000000..1fccb40 --- /dev/null +++ b/src/quackseq/spectrometer/simulator_controller.py @@ -0,0 +1,354 @@ +"""The controller module for the simulator spectrometer.""" + +import logging +from datetime import datetime +import numpy as np + +from quackseq.spectrometer.spectrometer_controller import SpectrometerController +from quackseq.measurement import Measurement +from quackseq.pulseparameters import TXPulse, RXReadout +from quackseq.pulsesequence import QuackSequence + +from nqr_blochsimulator.classes.pulse import PulseArray +from nqr_blochsimulator.classes.sample import Sample +from nqr_blochsimulator.classes.simulation import Simulation + +logger = logging.getLogger(__name__) + + +class SimulatorController(SpectrometerController): + """The controller class for the nqrduck simulator module.""" + + def __init__(self, model): + """Initializes the SimulatorController.""" + super().__init__() + self.model = model + + def run_sequence(self, sequence: QuackSequence) -> None: + """This method is called when the start_measurement signal is received from the core. + + It will becalled if the simulator is the active spectrometer. + This will start the simulation based on the settings and the pulse sequence. + """ + logger.debug("Starting simulation") + sample = self.get_sample_from_settings() + logger.debug("Sample: %s", sample.name) + + dwell_time = self.calculate_dwelltime(sequence) + logger.debug("Dwell time: %s", dwell_time) + + try: + pulse_array = self.translate_pulse_sequence(sequence, dwell_time) + except AttributeError: + logger.warning("Could not translate pulse sequence") + return + + simulation = self.get_simulation(sample, pulse_array) + + result = simulation.simulate() + + tdx = ( + np.linspace(0, float(self.calculate_simulation_length(sequence)), len(result)) * 1e6 + ) + + rx_begin, rx_stop = self.translate_rx_event(sequence) + # If we have a RX event, we need to cut the result to the RX event + if rx_begin and rx_stop: + evidx = np.where((tdx > rx_begin) & (tdx < rx_stop))[0] + tdx = tdx[evidx] + result = result[evidx] + + # Measurement name date + module + target frequency + averages + sequence name + name = f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - Simulator - {self.model.target_frequency / 1e6} MHz - {self.model.averages} averages - {sequence.name}" + logger.debug(f"Measurement name: {name}") + + measurement_data = Measurement( + name, + tdx, + result / simulation.averages, + sample.resonant_frequency, + # frequency_shift=self.module.model.if_frequency, + ) + + return measurement_data + + def get_sample_from_settings(self) -> Sample: + """This method creates a sample object based on the settings in the model. + + Returns: + Sample: The sample object created from the settings. + """ + model = self.model + atom_density = None + sample_volume = None + sample_length = None + sample_diameter = None + + for samplesetting in model.settings[self.model.SAMPLE]: + logger.debug("Sample setting: %s", samplesetting.name) + + if samplesetting.name == model.NAME: + name = samplesetting.value + elif samplesetting.name == model.DENSITY: + density = float(samplesetting.value) + elif samplesetting.name == model.MOLAR_MASS: + molar_mass = float(samplesetting.value) + elif samplesetting.name == model.RESONANT_FREQUENCY: + resonant_frequency = float(samplesetting.value) + elif samplesetting.name == model.GAMMA: + gamma = float(samplesetting.value) + elif samplesetting.name == model.NUCLEAR_SPIN: + nuclear_spin = float(samplesetting.value) + elif samplesetting.name == model.SPIN_FACTOR: + spin_factor = float(samplesetting.value) + elif samplesetting.name == model.POWDER_FACTOR: + powder_factor = float(samplesetting.value) + elif samplesetting.name == model.FILLING_FACTOR: + filling_factor = float(samplesetting.value) + elif samplesetting.name == model.T1: + T1 = float(samplesetting.value) + elif samplesetting.name == model.T2: + T2 = float(samplesetting.value) + elif samplesetting.name == model.T2_STAR: + T2_star = float(samplesetting.value) + elif samplesetting.name == model.ATOM_DENSITY: + atom_density = float(samplesetting.value) + elif samplesetting.name == model.SAMPLE_VOLUME: + sample_volume = float(samplesetting.value) + elif samplesetting.name == model.SAMPLE_LENGTH: + sample_length = float(samplesetting.value) + elif samplesetting.name == model.SAMPLE_DIAMETER: + sample_diameter = float(samplesetting.value) + else: + logger.warning("Unknown sample setting: %s", samplesetting.name) + self.module.nqrduck_signal.emit( + "notification", + ["Error", "Unknown sample setting: " + samplesetting.name], + ) + return None + + sample = Sample( + name=name, + density=density, + molar_mass=molar_mass, + resonant_frequency=resonant_frequency, + gamma=gamma, + nuclear_spin=nuclear_spin, + spin_factor=spin_factor, + powder_factor=powder_factor, + filling_factor=filling_factor, + T1=T1, + T2=T2, + T2_star=T2_star, + atom_density=atom_density, + sample_volume=sample_volume, + sample_length=sample_length, + sample_diameter=sample_diameter, + ) + return sample + + def translate_pulse_sequence(self, sequence : QuackSequence, dwell_time: float) -> PulseArray: + """This method translates the pulse sequence from the core to a PulseArray object needed for the simulation. + + Args: + sequence (QuackSequence): The pulse sequence from the core. + dwell_time (float): The dwell time in seconds. + + Returns: + PulseArray: The pulse sequence translated to a PulseArray object. + """ + events = sequence.events + + amplitude_array = list() + for event in events: + logger.debug("Event %s has parameters: %s", event.name, event.parameters) + for parameter in event.parameters.values(): + logger.debug( + "Parameter %s has options: %s", parameter.name, parameter.options + ) + + if ( + parameter.name == sequence.TX_PULSE + and parameter.get_option_by_name(TXPulse.RELATIVE_AMPLITUDE).value + > 0 + ): + logger.debug(f"Adding pulse: {event.duration} s") + # If we have a pulse, we need to add it to the pulse array + pulse_shape = parameter.get_option_by_name( + TXPulse.TX_PULSE_SHAPE + ).value + pulse_amplitude = abs( + pulse_shape.get_pulse_amplitude( + event.duration, resolution=dwell_time + ) + ) + + amplitude_array.append(pulse_amplitude) + elif ( + parameter.name == sequence.TX_PULSE + and parameter.get_option_by_name(TXPulse.RELATIVE_AMPLITUDE).value + == 0 + ): + # If we have a wait, we need to add it to the pulse array + amplitude_array.append(np.zeros(int(event.duration / dwell_time))) + + amplitude_array = np.concatenate(amplitude_array) + + # This has not yet been implemented right now the phase is always 0 + phase_array = np.zeros(len(amplitude_array)) + + pulse_array = PulseArray( + pulseamplitude=amplitude_array, + pulsephase=phase_array, + dwell_time=float(dwell_time), + ) + + return pulse_array + + def get_simulation(self, sample: Sample, pulse_array: PulseArray) -> Simulation: + """This method creates a simulation object based on the settings and the pulse sequence. + + Args: + sample (Sample): The sample object created from the settings. + pulse_array (PulseArray): The pulse sequence translated to a PulseArray object. + + Returns: + Simulation: The simulation object created from the settings and the pulse sequence. + """ + model = self.model + + # noise = float(model.get_setting_by_name(model.NOISE).value) + simulation = Simulation( + sample=sample, + pulse=pulse_array, + number_isochromats=int( + model.get_setting_by_name(model.NUMBER_ISOCHROMATS).value + ), + initial_magnetization=float( + model.get_setting_by_name(model.INITIAL_MAGNETIZATION).value + ), + gradient=float(model.get_setting_by_name(model.GRADIENT).value), + noise=float(model.get_setting_by_name(model.NOISE).value), + length_coil=float(model.get_setting_by_name(model.LENGTH_COIL).value), + diameter_coil=float(model.get_setting_by_name(model.DIAMETER_COIL).value), + number_turns=float(model.get_setting_by_name(model.NUMBER_TURNS).value), + q_factor_transmit=float( + model.get_setting_by_name(model.Q_FACTOR_TRANSMIT).value + ), + q_factor_receive=float( + model.get_setting_by_name(model.Q_FACTOR_RECEIVE).value + ), + power_amplifier_power=float( + model.get_setting_by_name(model.POWER_AMPLIFIER_POWER).value + ), + gain=float(model.get_setting_by_name(model.GAIN).value), + temperature=float(model.get_setting_by_name(model.TEMPERATURE).value), + averages=int(model.averages), + loss_TX=float(model.get_setting_by_name(model.LOSS_TX).value), + loss_RX=float(model.get_setting_by_name(model.LOSS_RX).value), + conversion_factor=float( + model.get_setting_by_name(model.CONVERSION_FACTOR).value + ), + ) + return simulation + + def calculate_dwelltime(self, sequence : QuackSequence) -> float: + """This method calculates the dwell time based on the settings and the pulse sequence. + + Returns: + float: The dwell time in seconds. + """ + n_points = int( + self.model.get_setting_by_name(self.model.NUMBER_POINTS).value + ) + simulation_length = self.calculate_simulation_length(sequence) + dwell_time = simulation_length / n_points + return dwell_time + + def calculate_simulation_length(self, sequence : QuackSequence) -> float: + """This method calculates the simulation length based on the settings and the pulse sequence. + + Returns: + float: The simulation length in seconds. + """ + events = sequence.events + simulation_length = 0 + for event in events: + simulation_length += event.duration + return simulation_length + + def translate_rx_event(self, sequence : QuackSequence) -> tuple: + """This method translates the RX event of the pulse sequence to the limr object. + + Returns: + tuple: A tuple containing the start and stop time of the RX event in µs + """ + # This is a correction factor for the RX event. The offset of the first pulse is 2.2µs longer than from the specified samples. + events = sequence.events + + previous_events_duration = 0 + # offset = 0 + rx_duration = 0 + for event in events: + logger.debug("Event %s has parameters: %s", event.name, event.parameters) + for parameter in event.parameters.values(): + logger.debug( + "Parameter %s has options: %s", parameter.name, parameter.options + ) + + if ( + parameter.name == sequence.RX_READOUT + and parameter.get_option_by_name(RXReadout.RX).value + ): + # Get the length of all previous events + previous_events = events[: events.index(event)] + previous_events_duration = sum( + [event.duration for event in previous_events] + ) + rx_duration = event.duration + + rx_begin = float(previous_events_duration) + if rx_duration: + rx_stop = rx_begin + float(rx_duration) + return rx_begin * 1e6, rx_stop * 1e6 + + else: + return None, None + + def set_frequency(self, value: str) -> None: + """This method is called when the set_frequency signal is received from the core. + + For the simulator this just prints a warning that the simulator is selected. + + Args: + value (str) : The new frequency in MHz. + """ + logger.debug("Setting frequency to: %s", value) + try: + self.module.model.target_frequency = float(value) + logger.debug("Successfully set frequency to: %s", value) + except ValueError: + logger.warning("Could not set frequency to: %s", value) + self.module.nqrduck_signal.emit( + "notification", ["Error", "Could not set frequency to: " + value] + ) + self.module.nqrduck_signal.emit("failure_set_frequency", value) + + def set_averages(self, value: str) -> None: + """This method is called when the set_averages signal is received from the core. + + It sets the averages in the model used for the simulation. + + Args: + value (str): The value to set the averages to. + """ + logger.debug("Setting averages to: %s", value) + try: + self.module.model.averages = int(value) + logger.debug("Successfully set averages to: %s", value) + except ValueError: + logger.warning("Could not set averages to: %s", value) + self.module.nqrduck_signal.emit( + "notification", ["Error", "Could not set averages to: " + value] + ) + self.module.nqrduck_signal.emit("failure_set_averages", value) diff --git a/src/quackseq/spectrometer/simulator_model.py b/src/quackseq/spectrometer/simulator_model.py new file mode 100644 index 0000000..7489bb3 --- /dev/null +++ b/src/quackseq/spectrometer/simulator_model.py @@ -0,0 +1,327 @@ +"""The model module for the simulator spectrometer.""" + +import logging +from quackseq.spectrometer.spectrometer_model import SpectrometerModel +from quackseq.spectrometer.spectrometer_settings import IntSetting, FloatSetting, StringSetting +from quackseq.pulseparameters import TXPulse, RXReadout + +logger = logging.getLogger(__name__) + + +class SimulatorModel(SpectrometerModel): + """Model class for the simulator spectrometer.""" + + # Simulation settings + NUMBER_POINTS = "N. simulation points" + NUMBER_ISOCHROMATS = "N. of isochromats" + INITIAL_MAGNETIZATION = "Initial magnetization" + GRADIENT = "Gradient (mT/m))" + NOISE = "Noise (uV)" + + # Hardware settings + LENGTH_COIL = "Length coil (m)" + DIAMETER_COIL = "Diameter coil (m)" + NUMBER_TURNS = "Number turns" + Q_FACTOR_TRANSMIT = "Q factor Transmit" + Q_FACTOR_RECEIVE = "Q factor Receive" + POWER_AMPLIFIER_POWER = "PA power (W)" + GAIN = "Gain" + TEMPERATURE = "Temperature (K)" + AVERAGES = "Averages" + LOSS_TX = "Loss TX (dB)" + LOSS_RX = "Loss RX (dB)" + CONVERSION_FACTOR = "Conversion factor" + + # Sample settings, this will be done in a separate module later on + NAME = "Name" + DENSITY = "Density (g/cm^3)" + MOLAR_MASS = "Molar mass (g/mol)" + RESONANT_FREQUENCY = "Resonant freq. (Hz)" + GAMMA = "Gamma (Hz/T)" + NUCLEAR_SPIN = "Nuclear spin" + SPIN_FACTOR = "Spin factor" + POWDER_FACTOR = "Powder factor" + FILLING_FACTOR = "Filling factor" + T1 = "T1 (s)" + T2 = "T2 (s)" + T2_STAR = "T2* (s)" + ATOM_DENSITY = "Atom density (1/cm^3)" + SAMPLE_VOLUME = "Sample volume (m^3)" + SAMPLE_LENGTH = "Sample length (m)" + SAMPLE_DIAMETER = "Sample diameter (m)" + + # Categories of the settings + SIMULATION = "Simulation" + HARDWARE = "Hardware" + EXPERIMENTAL_Setup = "Experimental Setup" + SAMPLE = "Sample" + + def __init__(self): + """Initializes the SimulatorModel.""" + super().__init__() + + # Simulation settings + number_of_points_setting = IntSetting( + self.NUMBER_POINTS, + 8192, + "Number of points used for the simulation. This influences the dwell time in combination with the total event simulation given by the pulse sequence.", + min_value=0, + ) + self.add_setting( + number_of_points_setting, + self.SIMULATION, + ) + + number_of_isochromats_setting = IntSetting( + self.NUMBER_ISOCHROMATS, + 1000, + "Number of isochromats used for the simulation. This influences the computation time.", + min_value=0, + max_value=10000, + ) + self.add_setting(number_of_isochromats_setting, self.SIMULATION) + + initial_magnetization_setting = FloatSetting( + self.INITIAL_MAGNETIZATION, + 1, + "Initial magnetization", + min_value=0, + ) + self.add_setting(initial_magnetization_setting, self.SIMULATION) + + # This doesn't really do anything yet + gradient_setting = FloatSetting( + self.GRADIENT, + 1, + "Gradient", + ) + self.add_setting(gradient_setting, self.SIMULATION) + + noise_setting = FloatSetting( + self.NOISE, + 2, + "Adds a specified level of random noise to the simulation to mimic real-world signal variations.", + min_value=0, + max_value=100, + ) + self.add_setting(noise_setting, self.SIMULATION) + + # Hardware settings + coil_length_setting = FloatSetting( + self.LENGTH_COIL, + 30e-3, + "The length of the sample coil within the hardware setup.", + min_value=1e-3, + ) + self.add_setting(coil_length_setting, self.HARDWARE) + + coil_diameter_setting = FloatSetting( + self.DIAMETER_COIL, + 8e-3, + "The diameter of the sample coil.", + min_value=1e-3, + ) + self.add_setting(coil_diameter_setting, self.HARDWARE) + + number_turns_setting = FloatSetting( + self.NUMBER_TURNS, + 8, + "The total number of turns of the sample coil.", + min_value=1, + ) + self.add_setting(number_turns_setting, self.HARDWARE) + + q_factor_transmit_setting = FloatSetting( + self.Q_FACTOR_TRANSMIT, + 80, + "The quality factor of the transmit path, which has an effect on the field strength for excitation.", + min_value=1, + ) + self.add_setting(q_factor_transmit_setting, self.HARDWARE) + + q_factor_receive_setting = FloatSetting( + self.Q_FACTOR_RECEIVE, + 80, + "The quality factor of the receive path, which has an effect on the final SNR.", + min_value=1, + ) + self.add_setting(q_factor_receive_setting, self.HARDWARE) + + power_amplifier_power_setting = FloatSetting( + self.POWER_AMPLIFIER_POWER, + 110, + "The power output capability of the power amplifier, determines the strength of pulses that can be generated.", + min_value=0.1, + ) + self.add_setting(power_amplifier_power_setting, self.HARDWARE) + + gain_setting = FloatSetting( + self.GAIN, + 6000, + "The amplification factor of the receiver chain, impacting the final measured signal amplitude.", + min_value=0.1, + ) + self.add_setting(gain_setting, self.HARDWARE) + + temperature_setting = FloatSetting( + self.TEMPERATURE, + 300, + "The absolute temperature during the experiment. This influences the SNR of the measurement.", + min_value=0.1, + max_value=400, + ) + self.add_setting(temperature_setting, self.EXPERIMENTAL_Setup) + + loss_tx_setting = FloatSetting( + self.LOSS_TX, + 25, + "The signal loss occurring in the transmission path, affecting the effective RF pulse power.", + min_value=0.1, + max_value=60, + ) + self.add_setting(loss_tx_setting, self.EXPERIMENTAL_Setup) + + loss_rx_setting = FloatSetting( + self.LOSS_RX, + 25, + "The signal loss in the reception path, which can reduce the signal that is ultimately detected.", + min_value=0.1, + max_value=60, + ) + self.add_setting(loss_rx_setting, self.EXPERIMENTAL_Setup) + + conversion_factor_setting = FloatSetting( + self.CONVERSION_FACTOR, + 2884, + "Conversion factor (spectrometer units / V)", + ) + self.add_setting( + conversion_factor_setting, + self.EXPERIMENTAL_Setup, + ) # Conversion factor for the LimeSDR based spectrometer + + # Sample settings + sample_name_setting = StringSetting( + self.NAME, + "BiPh3", + "The name of the sample.", + ) + self.add_setting(sample_name_setting, self.SAMPLE) + + density_setting = FloatSetting( + self.DENSITY, + 1.585e6, + "The density of the sample. This is used to calculate the number of spins in the sample volume.", + min_value=0.1, + ) + self.add_setting(density_setting, self.SAMPLE) + + molar_mass_setting = FloatSetting( + self.MOLAR_MASS, + 440.3, + "The molar mass of the sample. This is used to calculate the number of spins in the sample volume.", + min_value=0.1, + ) + self.add_setting(molar_mass_setting, self.SAMPLE) + + resonant_frequency_setting = FloatSetting( + self.RESONANT_FREQUENCY, + 83.56e6, + "The resonant frequency of the observed transition.", + min_value=1e5, + ) + self.add_setting(resonant_frequency_setting, self.SAMPLE) + + gamma_setting = FloatSetting( + self.GAMMA, + 4.342e7, + "The gyromagnetic ratio of the sample’s nuclei.", + min_value=0, + ) + self.add_setting(gamma_setting, self.SAMPLE) + + # This could be updated to a selection setting + nuclear_spin_setting = FloatSetting( + self.NUCLEAR_SPIN, + 9 / 2, + "The nuclear spin of the sample’s nuclei.", + min_value=0, + ) + self.add_setting(nuclear_spin_setting, self.SAMPLE) + + spin_factor_setting = FloatSetting( + self.SPIN_FACTOR, + 2, + "The spin factor represents the scaling coefficient for observable nuclear spin transitions along the x-axis, derived from the Pauli I x 0 -matrix elements.", + min_value=0, + ) + self.add_setting(spin_factor_setting, self.SAMPLE) + + powder_factor_setting = FloatSetting( + self.POWDER_FACTOR, + 0.75, + "A factor representing the crystallinity of the solid sample. A value of 0.75 corresponds to a powder sample.", + min_value=0, + max_value=1, + ) + self.add_setting(powder_factor_setting, self.SAMPLE) + + filling_factor_setting = FloatSetting( + self.FILLING_FACTOR, + 0.7, + "The ratio of the sample volume that occupies the coil’s sensitive volume.", + min_value=0, + max_value=1, + ) + self.add_setting(filling_factor_setting, self.SAMPLE) + + t1_setting = FloatSetting( + self.T1, + 83.5e-5, + "The longitudinal or spin-lattice relaxation time of the sample, influencing signal recovery between pulses.", + min_value=1e-6, + ) + self.add_setting(t1_setting, self.SAMPLE) + + t2_setting = FloatSetting( + self.T2, + 396e-6, + "The transverse or spin-spin relaxation time, determining the rate at which spins dephase and the signal decays in the xy plane", + min_value=1e-6, + ) + self.add_setting(t2_setting, self.SAMPLE) + + t2_star_setting = FloatSetting( + self.T2_STAR, + 50e-6, + "The effective transverse relaxation time, incorporating effects of EFG inhomogeneities and other dephasing factors.", + min_value=1e-6, + ) + self.add_setting(t2_star_setting, self.SAMPLE) + + self.averages = 1 + self.target_frequency = 100e6 + + @property + def averages(self): + """The number of averages used for the simulation. + + More averages improve the signal-to-noise ratio of the simulated signal. + """ + return self._averages + + @averages.setter + def averages(self, value): + self._averages = value + + @property + def target_frequency(self): + """The target frequency for the simulation. + + Doesn't do anything at the moment. + """ + return self._target_frequency + + @target_frequency.setter + def target_frequency(self, value): + self._target_frequency = value diff --git a/src/quackseq/spectrometer/spectrometer.py b/src/quackseq/spectrometer/spectrometer.py new file mode 100644 index 0000000..3def7c3 --- /dev/null +++ b/src/quackseq/spectrometer/spectrometer.py @@ -0,0 +1,39 @@ +class Spectrometer(): + """Base class for spectrometers. + + This class should be inherited by all spectrometers. + The spectrometers then need to implement the methods of this class. + """ + + def run_sequence(self, sequence): + """Starts the measurement. + + This method should be called when the measurement is started. + """ + raise NotImplementedError + + def set_frequency(self, value : float): + """Sets the frequency of the spectrometer.""" + raise NotImplementedError + + def set_averages(self, value : int): + """Sets the number of averages.""" + raise NotImplementedError + + @property + def controller(self): + """The controller of the spectrometer.""" + return self._controller + + @controller.setter + def controller(self, controller): + self._controller = controller + + @property + def model(self): + """The model of the spectrometer.""" + return self._model + + @model.setter + def model(self, model): + self._model = model \ No newline at end of file diff --git a/src/quackseq/spectrometer/spectrometer_controller.py b/src/quackseq/spectrometer/spectrometer_controller.py new file mode 100644 index 0000000..1f5e64d --- /dev/null +++ b/src/quackseq/spectrometer/spectrometer_controller.py @@ -0,0 +1,19 @@ +"""Base class for all spectrometer controllers.""" + +class SpectrometerController(): + """The base class for all spectrometer controllers.""" + + def run_sequence(self, sequence): + """Starts the measurement. + + This method should be called when the measurement is started. + """ + raise NotImplementedError + + def set_frequency(self, value : float): + """Sets the frequency of the spectrometer.""" + raise NotImplementedError + + def set_averages(self, value : int): + """Sets the number of averages.""" + raise NotImplementedError diff --git a/src/quackseq/spectrometer/spectrometer_model.py b/src/quackseq/spectrometer/spectrometer_model.py index 838e3f5..eeff852 100644 --- a/src/quackseq/spectrometer/spectrometer_model.py +++ b/src/quackseq/spectrometer/spectrometer_model.py @@ -2,7 +2,7 @@ import logging from collections import OrderedDict -from quackseq.spectrometer.spectrometer_setting import Setting +from quackseq.spectrometer.spectrometer_settings import Setting logger = logging.getLogger(__name__) @@ -12,22 +12,14 @@ class SpectrometerModel(): 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 """ settings: OrderedDict - def __init__(self, module): - """Initializes the spectrometer model. - - Args: - module (Module) : The module that the spectrometer is connected to - """ - super().__init__(module) + def __init__(self): + """Initializes the spectrometer model.""" self.settings = OrderedDict() def add_setting(self, setting: Setting, category: str) -> None: diff --git a/src/quackseq/spectrometer/spectrometer_setting.py b/src/quackseq/spectrometer/spectrometer_settings.py similarity index 98% rename from src/quackseq/spectrometer/spectrometer_setting.py rename to src/quackseq/spectrometer/spectrometer_settings.py index e404295..5bd7b8e 100644 --- a/src/quackseq/spectrometer/spectrometer_setting.py +++ b/src/quackseq/spectrometer/spectrometer_settings.py @@ -109,7 +109,6 @@ class FloatSetting(NumericalSetting): def value(self, value): logger.debug(f"Setting {self.name} to {value}") self._value = float(value) - self.settings_changed.emit() class IntSetting(NumericalSetting): @@ -130,10 +129,8 @@ class IntSetting(NumericalSetting): 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 diff --git a/tests/simulator.py b/tests/simulator.py index 2e6f0d5..840f0e7 100644 --- a/tests/simulator.py +++ b/tests/simulator.py @@ -1,8 +1,13 @@ # Dummy test to communicate the structure +import logging +import matplotlib.pyplot as plt + from quackseq.pulsesequence import QuackSequence from quackseq.event import Event from quackseq.functions import RectFunction +from quackseq.spectrometer.simulator import Simulator +logging.basicConfig(level=logging.DEBUG) seq = QuackSequence("test") @@ -17,11 +22,11 @@ rect = RectFunction() seq.set_tx_shape(tx, rect) -blank = Event("blank", "10u", seq) +blank = Event("blank", "3u", seq) seq.add_event(blank) -rx = Event("rx", "10u", seq) +rx = Event("rx", "50u", seq) #rx.set_rx_phase(0) seq.set_rx(rx, True) @@ -36,11 +41,12 @@ json = seq.to_json() print(json) -#sim = Simulator() +sim = Simulator() -#sim.set_averages(100) +sim.set_averages(100) # Returns the data at the RX event -#result = sim.run(seq) +result = sim.run_sequence(seq) -#result.plot() +plt.plot(result.tdx, abs(result.tdy)) +plt.show()