Change to quackseq structure.

This commit is contained in:
jupfi 2024-05-29 16:04:05 +02:00
parent 6481e674bf
commit 13116d09c6
4 changed files with 22 additions and 610 deletions

View file

@ -1,19 +1,13 @@
"""The controller module for the simulator spectrometer.""" """The controller module for the simulator spectrometer."""
import logging import logging
from datetime import datetime
import numpy as np
from nqrduck_spectrometer.base_spectrometer_controller import BaseSpectrometerController from nqrduck_spectrometer.base_spectrometer_controller import BaseSpectrometerController
from nqrduck_spectrometer.measurement import Measurement from quackseq_simulator.simulator import Simulator
from nqrduck_spectrometer.pulseparameters import TXPulse, RXReadout
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__) logger = logging.getLogger(__name__)
class SimulatorController(BaseSpectrometerController): class DuckSimulatorController(BaseSpectrometerController):
"""The controller class for the nqrduck simulator module.""" """The controller class for the nqrduck simulator module."""
def __init__(self, module): def __init__(self, module):
@ -21,301 +15,24 @@ class SimulatorController(BaseSpectrometerController):
super().__init__(module) super().__init__(module)
def start_measurement(self): def start_measurement(self):
"""This method is called when the start_measurement signal is received from the core. """This method is called when the start_measurement signal is received."""
sequence = self.module.model.pulse_programmer.model.pulse_sequence
It will becalled if the simulator is the active spectrometer. simulator = Simulator()
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() simulator.model = self.module.model.quackseq_model
logger.debug("Dwell time: %s", dwell_time)
try: simulator.model.target_frequency = self.module.model.target_frequency
pulse_array = self.translate_pulse_sequence(dwell_time) simulator.model.averages = self.module.model.averages
except ValueError:
logger.warning("Could not translate pulse sequence")
self.module.nqrduck_signal.emit(
"measurement_error",
"Could not translate pulse sequence. Did you configure one?",
)
return
simulation = self.get_simulation(sample, pulse_array) measurement_data = simulator.run_sequence(sequence)
result = simulation.simulate()
tdx = (
np.linspace(0, float(self.calculate_simulation_length()), len(result)) * 1e6
)
rx_begin, rx_stop = self.translate_rx_event()
# 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.module.model.target_frequency / 1e6} MHz - {self.module.model.averages} averages - {self.module.model.pulse_programmer.model.pulse_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,
)
# Emit the data to the nqrduck core # Emit the data to the nqrduck core
logger.debug("Emitting measurement data") logger.debug("Emitting measurement data")
self.module.nqrduck_signal.emit("statusbar_message", "Finished Simulation") self.module.nqrduck_signal.emit("statusbar_message", "Finished Simulation")
self.module.nqrduck_signal.emit("measurement_data", measurement_data) self.module.nqrduck_signal.emit("measurement_data", 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.module.model
atom_density = None
sample_volume = None
sample_length = None
sample_diameter = None
for samplesetting in model.settings[self.module.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, dwell_time: float) -> PulseArray:
"""This method translates the pulse sequence from the core to a PulseArray object needed for the simulation.
Args:
dwell_time (float): The dwell time in seconds.
Returns:
PulseArray: The pulse sequence translated to a PulseArray object.
"""
events = self.module.model.pulse_programmer.model.pulse_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 == self.module.model.TX
and parameter.get_option_by_name(TXPulse.RELATIVE_AMPLITUDE).value
> 0
):
# 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 == self.module.model.TX
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.module.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) -> 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.module.model.get_setting_by_name(self.module.model.NUMBER_POINTS).value
)
simulation_length = self.calculate_simulation_length()
dwell_time = simulation_length / n_points
return dwell_time
def calculate_simulation_length(self) -> float:
"""This method calculates the simulation length based on the settings and the pulse sequence.
Returns:
float: The simulation length in seconds.
"""
events = self.module.model.pulse_programmer.model.pulse_sequence.events
simulation_length = 0
for event in events:
simulation_length += event.duration
return simulation_length
def translate_rx_event(self) -> 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 = self.module.model.pulse_programmer.model.pulse_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 == self.module.model.RX
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: def set_frequency(self, value: str) -> None:
"""This method is called when the set_frequency signal is received from the core. """This method is called when the set_frequency signal is received from the core.

View file

@ -2,328 +2,23 @@
import logging import logging
from nqrduck_spectrometer.base_spectrometer_model import BaseSpectrometerModel from nqrduck_spectrometer.base_spectrometer_model import BaseSpectrometerModel
from nqrduck_spectrometer.pulseparameters import TXPulse, RXReadout from quackseq_simulator.simulator_model import SimulatorModel
from nqrduck_spectrometer.settings import (
FloatSetting,
IntSetting,
StringSetting,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class SimulatorModel(BaseSpectrometerModel): class DuckSimulatorModel(BaseSpectrometerModel):
"""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"
# Pulse parameter constants
TX = "TX"
RX = "RX"
def __init__(self, module): def __init__(self, module):
"""Initializes the SimulatorModel.""" """Initializes the SimulatorModel."""
super().__init__(module) super().__init__(module)
# Simulation settings self.quackseq_model = SimulatorModel()
number_of_points_setting = IntSetting( self.visualize_settings()
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,
spin_box=(True, False),
)
self.add_setting(
number_of_points_setting,
self.SIMULATION,
)
number_of_isochromats_setting = IntSetting( self.quackseq_visuals
self.NUMBER_ISOCHROMATS,
1000,
"Number of isochromats used for the simulation. This influences the computation time.",
min_value=0,
max_value=10000,
spin_box=(True, False),
)
self.add_setting(number_of_isochromats_setting, self.SIMULATION)
initial_magnetization_setting = FloatSetting(
self.INITIAL_MAGNETIZATION,
1,
"Initial magnetization",
min_value=0,
spin_box=(True, False),
)
self.add_setting(initial_magnetization_setting, self.SIMULATION)
# This doesn't really do anything yet
gradient_setting = FloatSetting(
self.GRADIENT,
1,
"Gradient",
spin_box=(True, False),
)
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,
spin_box=(True, False),
)
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,
spin_box=(True, True),
)
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,
spin_box=(True, True),
)
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,
spin_box=(True, True),
)
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 samples 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 samples 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,
spin_box=(True, False),
)
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 coils sensitive volume.",
min_value=0,
max_value=1,
spin_box=(True, False),
)
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)
# Pulse parameter options
self.add_pulse_parameter_option(self.TX, TXPulse)
# self.add_pulse_parameter_option(self.GATE, Gate)
self.add_pulse_parameter_option(self.RX, RXReadout)
self.averages = 1
self.target_frequency = 100e6
# Try to load the pulse programmer module # Try to load the pulse programmer module
try: try:
@ -331,7 +26,7 @@ class SimulatorModel(BaseSpectrometerModel):
self.pulse_programmer = pulse_programmer self.pulse_programmer = pulse_programmer
logger.debug("Pulse programmer found.") logger.debug("Pulse programmer found.")
self.pulse_programmer.controller.on_loading(self.pulse_parameter_options) self.pulse_programmer.controller.on_loading()
except ImportError: except ImportError:
logger.warning("No pulse programmer found.") logger.warning("No pulse programmer found.")

View file

@ -1,8 +1,8 @@
"""Creation of the Simulator Spectrometer.""" """Creation of the Simulator Spectrometer."""
from nqrduck_spectrometer.base_spectrometer import BaseSpectrometer from nqrduck_spectrometer.base_spectrometer import BaseSpectrometer
from .model import SimulatorModel from .model import DuckSimulatorModel
from .view import SimulatorView from .view import DuckSimulatorView
from .controller import SimulatorController from .controller import DuckSimulatorController
Simulator = BaseSpectrometer(SimulatorModel, SimulatorView, SimulatorController) Simulator = BaseSpectrometer(DuckSimulatorModel, DuckSimulatorView, DuckSimulatorController)

View file

@ -3,7 +3,7 @@
from nqrduck_spectrometer.base_spectrometer_view import BaseSpectrometerView from nqrduck_spectrometer.base_spectrometer_view import BaseSpectrometerView
class SimulatorView(BaseSpectrometerView): class DuckSimulatorView(BaseSpectrometerView):
"""The View class for the simulator module.""" """The View class for the simulator module."""
def __init__(self, module): def __init__(self, module):