mirror of
https://github.com/nqrduck/nqrduck-spectrometer-simulator.git
synced 2024-12-21 15:10:25 +00:00
Basic implementation of simulator. This is all very buggy and has to be tested thoroughly.
This commit is contained in:
parent
f5cdfdda4b
commit
0988406650
4 changed files with 366 additions and 3 deletions
|
@ -7,7 +7,7 @@ allow-direct-references = true
|
|||
|
||||
[project]
|
||||
name = "nqrduck-spectrometer-simulator"
|
||||
version = "0.0.1"
|
||||
version = "0.0.2"
|
||||
authors = [
|
||||
{ name="Julia Pfitzer", email="git@jupfi.me" },
|
||||
]
|
||||
|
@ -26,6 +26,8 @@ classifiers = [
|
|||
dependencies = [
|
||||
"nqrduck-spectrometer",
|
||||
"pyqt6",
|
||||
"numpy",
|
||||
"nqr_blochsimulator@git+https://github.com/jupfi/nqr-blochsimulator.git",
|
||||
]
|
||||
|
||||
[project.entry-points."nqrduck"]
|
||||
|
|
|
@ -1,5 +1,250 @@
|
|||
import logging
|
||||
import numpy as np
|
||||
from nqrduck_spectrometer.base_spectrometer_controller import BaseSpectrometerController
|
||||
from nqrduck_spectrometer.measurement import Measurement
|
||||
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__)
|
||||
|
||||
class SimulatorController(BaseSpectrometerController):
|
||||
def __init__(self, module):
|
||||
super().__init__(module)
|
||||
|
||||
def start_measurement(self):
|
||||
"""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()
|
||||
logger.debug("Dwell time: %s", dwell_time)
|
||||
|
||||
pulse_array = self.translate_pulse_sequence(dwell_time)
|
||||
|
||||
simulation = self.get_simulation(sample, pulse_array)
|
||||
|
||||
result = abs(simulation.simulate())
|
||||
tdx = np.linspace(0, float(self.calculate_simulation_length()), len(result)) * 1e6
|
||||
|
||||
measurement_data = Measurement(
|
||||
tdx,
|
||||
result,
|
||||
sample.resonant_frequency,
|
||||
# frequency_shift=self.module.model.if_frequency,
|
||||
)
|
||||
|
||||
# Emit the data to the nqrduck core
|
||||
logger.debug("Emitting measurement data")
|
||||
self.module.nqrduck_signal.emit("statusbar_message", "Finished Simulation")
|
||||
|
||||
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 = samplesetting.value
|
||||
elif samplesetting.name == model.MOLAR_MASS:
|
||||
molar_mass = samplesetting.value
|
||||
elif samplesetting.name == model.RESONANT_FREQUENCY:
|
||||
resonant_frequency = samplesetting.value
|
||||
elif samplesetting.name == model.GAMMA:
|
||||
gamma = samplesetting.value
|
||||
elif samplesetting.name == model.NUCLEAR_SPIN:
|
||||
nuclear_spin = samplesetting.value
|
||||
elif samplesetting.name == model.SPIN_FACTOR:
|
||||
spin_factor = samplesetting.value
|
||||
elif samplesetting.name == model.POWDER_FACTOR:
|
||||
powder_factor = samplesetting.value
|
||||
elif samplesetting.name == model.FILLING_FACTOR:
|
||||
filling_factor = samplesetting.value
|
||||
elif samplesetting.name == model.T1:
|
||||
T1 = samplesetting.value
|
||||
elif samplesetting.name == model.T2:
|
||||
T2 = samplesetting.value
|
||||
elif samplesetting.name == model.T2_STAR:
|
||||
T2_star = samplesetting.value
|
||||
elif samplesetting.name == model.ATOM_DENSITY:
|
||||
atom_density = samplesetting.value
|
||||
elif samplesetting.name == model.SAMPLE_VOLUME:
|
||||
sample_volume = samplesetting.value
|
||||
elif samplesetting.name == model.SAMPLE_LENGTH:
|
||||
sample_length = samplesetting.value
|
||||
elif samplesetting.name == model.SAMPLE_DIAMETER:
|
||||
sample_diameter = 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
|
||||
simulation = Simulation(
|
||||
sample = sample,
|
||||
pulse = pulse_array,
|
||||
number_isochromats = model.get_setting_by_name(model.NUMBER_ISOCHROMATS).value,
|
||||
initial_magnetization = model.get_setting_by_name(model.INITIAL_MAGNETIZATION).value,
|
||||
gradient = model.get_setting_by_name(model.GRADIENT).value,
|
||||
noise = model.get_setting_by_name(model.NOISE).value,
|
||||
length_coil = model.get_setting_by_name(model.LENGTH_COIL).value,
|
||||
diameter_coil = model.get_setting_by_name(model.DIAMETER_COIL).value,
|
||||
number_turns = model.get_setting_by_name(model.NUMBER_TURNS).value,
|
||||
power_amplifier_power = model.get_setting_by_name(model.POWER_AMPLIFIER_POWER).value,
|
||||
gain = model.get_setting_by_name(model.GAIN).value,
|
||||
temperature = model.get_setting_by_name(model.TEMPERATURE).value,
|
||||
averages = model.averages,
|
||||
loss_TX = model.get_setting_by_name(model.LOSS_TX).value,
|
||||
loss_RX = model.get_setting_by_name(model.LOSS_RX).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 = 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 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.
|
||||
"""
|
||||
self.module.nqrduck_signal.emit(
|
||||
"notification", ["Warning", "Could not set averages to because the simulator is selected as active spectrometer "]
|
||||
)
|
||||
|
||||
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)
|
||||
|
|
|
@ -1,5 +1,121 @@
|
|||
import logging
|
||||
from nqrduck_spectrometer.base_spectrometer_model import BaseSpectrometerModel
|
||||
from nqrduck_spectrometer.pulseparameters import TXPulse, RXReadout
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SimulatorModel(BaseSpectrometerModel):
|
||||
# Simulation settings
|
||||
NUMBER_POINTS = "N. simulation points"
|
||||
NUMBER_ISOCHROMATS = "N. of isochromats"
|
||||
INITIAL_MAGNETIZATION = "Initial magnetization"
|
||||
GRADIENT = "Gradient (mT/m))"
|
||||
NOISE = "Noise (uV)"
|
||||
LENGTH_COIL = "Length coil (m)"
|
||||
DIAMETER_COIL = "Diameter coil (m)"
|
||||
NUMBER_TURNS = "Number turns"
|
||||
POWER_AMPLIFIER_POWER = "PA power (W)"
|
||||
GAIN = "Gain"
|
||||
TEMPERATURE = "Temperature (K)"
|
||||
AVERAGES = "Averages"
|
||||
LOSS_TX = "Loss TX (dB)"
|
||||
LOSS_RX = "Loss RX (dB)"
|
||||
|
||||
# Sample settinggs, this will be done in a seperate 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):
|
||||
super().__init__(module)
|
||||
|
||||
# Simulation settings
|
||||
self.add_setting(
|
||||
self.NUMBER_POINTS,
|
||||
300,
|
||||
"Number of points used for the simulation. This influences the dwell time in combination with the total event simulation given by the pulse sequence. ",
|
||||
self.SIMULATION,
|
||||
)
|
||||
self.add_setting(
|
||||
self.NUMBER_ISOCHROMATS, 1000, "Number of isochromats", self.SIMULATION
|
||||
)
|
||||
self.add_setting(
|
||||
self.INITIAL_MAGNETIZATION, 1, "Initial magnetization", self.SIMULATION
|
||||
)
|
||||
self.add_setting(self.GRADIENT, 1, "Gradient", self.SIMULATION)
|
||||
self.add_setting(self.NOISE, 0, "Noise", self.SIMULATION)
|
||||
self.add_setting(self.LENGTH_COIL, 6, "Length coil", self.HARDWARE)
|
||||
self.add_setting(self.DIAMETER_COIL, 3, "Diameter coil", self.HARDWARE)
|
||||
self.add_setting(self.NUMBER_TURNS, 9, "Number turns", self.HARDWARE)
|
||||
self.add_setting(
|
||||
self.POWER_AMPLIFIER_POWER, 500, "Power amplifier power", self.HARDWARE
|
||||
)
|
||||
self.add_setting(
|
||||
self.GAIN, 6000, "Gain of the complete measurement chain", self.HARDWARE
|
||||
)
|
||||
self.add_setting(self.TEMPERATURE, 300, "Temperature", self.EXPERIMENTAL_Setup)
|
||||
self.add_setting(self.LOSS_TX, 12, "Loss TX", self.EXPERIMENTAL_Setup)
|
||||
self.add_setting(self.LOSS_RX, 12, "Loss RX", self.EXPERIMENTAL_Setup)
|
||||
|
||||
# Sample settings
|
||||
self.add_setting(self.NAME, "BiPh3", "Name", self.SAMPLE)
|
||||
self.add_setting(self.DENSITY, 1.585e6, "Density", self.SAMPLE)
|
||||
self.add_setting(self.MOLAR_MASS, 440.3, "Molar mass", self.SAMPLE)
|
||||
self.add_setting(
|
||||
self.RESONANT_FREQUENCY, 83.56e6, "Resonant frequency", self.SAMPLE
|
||||
)
|
||||
self.add_setting(self.GAMMA, 4.342e7, "Gamma", self.SAMPLE)
|
||||
self.add_setting(self.NUCLEAR_SPIN, 9 / 2, "Nuclear spin", self.SAMPLE)
|
||||
self.add_setting(self.SPIN_FACTOR, 2, "Spin factor", self.SAMPLE)
|
||||
self.add_setting(self.POWDER_FACTOR, 0.75, "Powder factor", self.SAMPLE)
|
||||
self.add_setting(self.FILLING_FACTOR, 0.7, "Filling factor", self.SAMPLE)
|
||||
self.add_setting(self.T1, 83.5e-5, "T1", self.SAMPLE)
|
||||
self.add_setting(self.T2, 396e-6, "T2", self.SAMPLE)
|
||||
self.add_setting(self.T2_STAR, 50e-6, "T2*", 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)
|
||||
|
||||
# Try to load the pulse programmer module
|
||||
try:
|
||||
from nqrduck_pulseprogrammer.pulseprogrammer import pulse_programmer
|
||||
|
||||
self.pulse_programmer = pulse_programmer
|
||||
logger.debug("Pulse programmer found.")
|
||||
self.pulse_programmer.controller.on_loading(self.pulse_parameter_options)
|
||||
except ImportError:
|
||||
logger.warning("No pulse programmer found.")
|
||||
|
||||
@property
|
||||
def averages(self):
|
||||
return self._averages
|
||||
|
||||
@averages.setter
|
||||
def averages(self, value):
|
||||
self._averages = value
|
||||
|
|
|
@ -3,5 +3,5 @@ from nqrduck_spectrometer.base_spectrometer_view import BaseSpectrometerView
|
|||
class SimulatorView(BaseSpectrometerView):
|
||||
def __init__(self, module):
|
||||
super().__init__(module)
|
||||
|
||||
# This automatically generates the settings widget based on the settings in the model
|
||||
self.widget = self.load_settings_ui()
|
Loading…
Reference in a new issue