Basic implementation of simulator. This is all very buggy and has to be tested thoroughly.

This commit is contained in:
jupfi 2023-08-24 10:07:09 +02:00
parent f5cdfdda4b
commit 0988406650
4 changed files with 366 additions and 3 deletions

View file

@ -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"]

View file

@ -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)

View file

@ -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)
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

View file

@ -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()