diff --git a/src/quackseq_simulator/simulator.py b/src/quackseq_simulator/simulator.py index 9462b4e..b3767ef 100644 --- a/src/quackseq_simulator/simulator.py +++ b/src/quackseq_simulator/simulator.py @@ -15,3 +15,7 @@ class Simulator(Spectrometer): def set_averages(self, value: int): self.model.average = value + + @property + def settings(self): + return self.model.settings diff --git a/src/quackseq_simulator/simulator_controller.py b/src/quackseq_simulator/simulator_controller.py index 1fccb40..df180ca 100644 --- a/src/quackseq_simulator/simulator_controller.py +++ b/src/quackseq_simulator/simulator_controller.py @@ -48,7 +48,10 @@ class SimulatorController(SpectrometerController): result = simulation.simulate() tdx = ( - np.linspace(0, float(self.calculate_simulation_length(sequence)), len(result)) * 1e6 + np.linspace( + 0, float(self.calculate_simulation_length(sequence)), len(result) + ) + * 1e6 ) rx_begin, rx_stop = self.translate_rx_event(sequence) @@ -84,48 +87,18 @@ class SimulatorController(SpectrometerController): 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 + name = model.settings.sample_name + density = model.settings.density + molar_mass = model.settings.molar_mass + resonant_frequency = model.settings.resonant_frequency + gamma = model.settings.gamma + nuclear_spin = model.settings.nuclear_spin + spin_factor = model.settings.spin_factor + powder_factor = model.settings.powder_factor + filling_factor = model.settings.filling_factor + T1 = model.settings.T1 + T2 = model.settings.T2 + T2_star = model.settings.T2_star sample = Sample( name=name, @@ -147,7 +120,9 @@ class SimulatorController(SpectrometerController): ) return sample - def translate_pulse_sequence(self, sequence : QuackSequence, dwell_time: float) -> PulseArray: + 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: @@ -221,51 +196,39 @@ class SimulatorController(SpectrometerController): 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), + number_isochromats=int(model.settings.number_isochromats), + initial_magnetization=float(model.settings.initial_magnetization), + gradient=float(model.settings.gradient), + noise=float(model.settings.noise), + length_coil=float(model.settings.length_coil), + diameter_coil=float(model.settings.diameter_coil), + number_turns=float(model.settings.number_turns), + q_factor_transmit=float(model.settings.q_factor_transmit), + q_factor_receive=float(model.settings.q_factor_receive), + power_amplifier_power=float(model.settings.power_amplifier_power), + gain=float(model.settings.gain), + temperature=float(model.settings.temperature), 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 - ), + loss_TX=float(model.settings.loss_tx), + loss_RX=float(model.settings.loss_rx), + conversion_factor=float(model.settings.conversion_factor), ) return simulation - def calculate_dwelltime(self, sequence : QuackSequence) -> float: + 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 + self.model.get_setting_by_display_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: + def calculate_simulation_length(self, sequence: QuackSequence) -> float: """This method calculates the simulation length based on the settings and the pulse sequence. Returns: @@ -277,14 +240,14 @@ class SimulatorController(SpectrometerController): simulation_length += event.duration return simulation_length - def translate_rx_event(self, sequence : QuackSequence) -> tuple: + 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 + events = sequence.events previous_events_duration = 0 # offset = 0 @@ -314,41 +277,3 @@ class SimulatorController(SpectrometerController): 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_simulator/simulator_model.py b/src/quackseq_simulator/simulator_model.py index 7489bb3..771af6d 100644 --- a/src/quackseq_simulator/simulator_model.py +++ b/src/quackseq_simulator/simulator_model.py @@ -2,7 +2,11 @@ import logging from quackseq.spectrometer.spectrometer_model import SpectrometerModel -from quackseq.spectrometer.spectrometer_settings import IntSetting, FloatSetting, StringSetting +from quackseq.spectrometer.spectrometer_settings import ( + IntSetting, + FloatSetting, + StringSetting, +) from quackseq.pulseparameters import TXPulse, RXReadout logger = logging.getLogger(__name__) @@ -33,7 +37,7 @@ class SimulatorModel(SpectrometerModel): CONVERSION_FACTOR = "Conversion factor" # Sample settings, this will be done in a separate module later on - NAME = "Name" + SAMPLE_NAME = "Name" DENSITY = "Density (g/cm^3)" MOLAR_MASS = "Molar mass (g/mol)" RESONANT_FREQUENCY = "Resonant freq. (Hz)" @@ -63,241 +67,264 @@ class SimulatorModel(SpectrometerModel): # Simulation settings number_of_points_setting = IntSetting( self.NUMBER_POINTS, + self.SIMULATION, 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_points", number_of_points_setting, - self.SIMULATION, ) number_of_isochromats_setting = IntSetting( self.NUMBER_ISOCHROMATS, + self.SIMULATION, 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) + self.add_setting("number_isochromats", number_of_isochromats_setting) initial_magnetization_setting = FloatSetting( self.INITIAL_MAGNETIZATION, + self.SIMULATION, 1, "Initial magnetization", min_value=0, ) - self.add_setting(initial_magnetization_setting, self.SIMULATION) + self.add_setting("initial_magnetization", initial_magnetization_setting) - # This doesn't really do anything yet gradient_setting = FloatSetting( self.GRADIENT, + self.SIMULATION, 1, "Gradient", ) - self.add_setting(gradient_setting, self.SIMULATION) + self.add_setting("gradient", gradient_setting) noise_setting = FloatSetting( self.NOISE, + self.SIMULATION, 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) + self.add_setting("noise", noise_setting) # Hardware settings coil_length_setting = FloatSetting( self.LENGTH_COIL, + self.HARDWARE, 30e-3, "The length of the sample coil within the hardware setup.", min_value=1e-3, ) - self.add_setting(coil_length_setting, self.HARDWARE) + self.add_setting("length_coil", coil_length_setting) coil_diameter_setting = FloatSetting( self.DIAMETER_COIL, + self.HARDWARE, 8e-3, "The diameter of the sample coil.", min_value=1e-3, ) - self.add_setting(coil_diameter_setting, self.HARDWARE) + self.add_setting("diameter_coil", coil_diameter_setting) number_turns_setting = FloatSetting( self.NUMBER_TURNS, + self.HARDWARE, 8, "The total number of turns of the sample coil.", min_value=1, ) - self.add_setting(number_turns_setting, self.HARDWARE) + self.add_setting("number_turns", number_turns_setting) q_factor_transmit_setting = FloatSetting( self.Q_FACTOR_TRANSMIT, + self.HARDWARE, 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) + self.add_setting("q_factor_transmit", q_factor_transmit_setting) q_factor_receive_setting = FloatSetting( self.Q_FACTOR_RECEIVE, + self.HARDWARE, 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) + self.add_setting("q_factor_receive", q_factor_receive_setting) power_amplifier_power_setting = FloatSetting( self.POWER_AMPLIFIER_POWER, + self.HARDWARE, 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) + self.add_setting("power_amplifier_power", power_amplifier_power_setting) gain_setting = FloatSetting( self.GAIN, + self.HARDWARE, 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) + self.add_setting("gain", gain_setting) temperature_setting = FloatSetting( self.TEMPERATURE, + self.EXPERIMENTAL_Setup, 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) + self.add_setting("temperature", temperature_setting) loss_tx_setting = FloatSetting( self.LOSS_TX, + self.EXPERIMENTAL_Setup, 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) + self.add_setting("loss_tx", loss_tx_setting) loss_rx_setting = FloatSetting( self.LOSS_RX, + self.EXPERIMENTAL_Setup, 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) + self.add_setting("loss_rx", loss_rx_setting) conversion_factor_setting = FloatSetting( self.CONVERSION_FACTOR, + self.EXPERIMENTAL_Setup, 2884, "Conversion factor (spectrometer units / V)", ) - self.add_setting( - conversion_factor_setting, - self.EXPERIMENTAL_Setup, - ) # Conversion factor for the LimeSDR based spectrometer + self.add_setting("conversion_factor", conversion_factor_setting) # Conversion factor for the LimeSDR based spectrometer # Sample settings sample_name_setting = StringSetting( - self.NAME, + self.SAMPLE_NAME, + self.SAMPLE, "BiPh3", "The name of the sample.", ) - self.add_setting(sample_name_setting, self.SAMPLE) + self.add_setting("sample_name", sample_name_setting) density_setting = FloatSetting( self.DENSITY, + self.SAMPLE, 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) + self.add_setting("density", density_setting) molar_mass_setting = FloatSetting( self.MOLAR_MASS, + self.SAMPLE, 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) + self.add_setting("molar_mass", molar_mass_setting) resonant_frequency_setting = FloatSetting( self.RESONANT_FREQUENCY, + self.SAMPLE, 83.56e6, "The resonant frequency of the observed transition.", min_value=1e5, ) - self.add_setting(resonant_frequency_setting, self.SAMPLE) + self.add_setting("resonant_frequency", resonant_frequency_setting) gamma_setting = FloatSetting( self.GAMMA, + self.SAMPLE, 4.342e7, "The gyromagnetic ratio of the sample’s nuclei.", min_value=0, ) - self.add_setting(gamma_setting, self.SAMPLE) + self.add_setting("gamma", gamma_setting) - # This could be updated to a selection setting nuclear_spin_setting = FloatSetting( self.NUCLEAR_SPIN, + self.SAMPLE, 9 / 2, "The nuclear spin of the sample’s nuclei.", min_value=0, ) - self.add_setting(nuclear_spin_setting, self.SAMPLE) + self.add_setting("nuclear_spin", nuclear_spin_setting) spin_factor_setting = FloatSetting( self.SPIN_FACTOR, + self.SAMPLE, 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) + self.add_setting("spin_factor", spin_factor_setting) powder_factor_setting = FloatSetting( self.POWDER_FACTOR, + self.SAMPLE, 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) + self.add_setting("powder_factor", powder_factor_setting) filling_factor_setting = FloatSetting( self.FILLING_FACTOR, + self.SAMPLE, 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) + self.add_setting("filling_factor", filling_factor_setting) t1_setting = FloatSetting( self.T1, + self.SAMPLE, 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) + self.add_setting("T1", t1_setting) t2_setting = FloatSetting( self.T2, + self.SAMPLE, 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) + self.add_setting("T2", t2_setting) t2_star_setting = FloatSetting( self.T2_STAR, + self.SAMPLE, 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.add_setting("T2_star", t2_star_setting) self.averages = 1 self.target_frequency = 100e6 diff --git a/tests/simulator.py b/tests/simulator.py index b8cc4b3..777cc09 100644 --- a/tests/simulator.py +++ b/tests/simulator.py @@ -24,6 +24,8 @@ class TestQuackSequence(unittest.TestCase): sim = Simulator() sim.set_averages(100) + sim.settings.noise = 0 + result = sim.run_sequence(seq) self.assertIsNotNone(result) self.assertTrue(hasattr(result, "tdx"))