mirror of
https://github.com/nqrduck/nqr-blochsimulator.git
synced 2024-06-30 01:09:08 +00:00
Formatting.
This commit is contained in:
parent
8ae00f8b98
commit
5b2b3645d1
|
@ -1,5 +1,6 @@
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
class PulseArray:
|
class PulseArray:
|
||||||
"""A class to represent a pulsearray for a NQR sequence."""
|
"""A class to represent a pulsearray for a NQR sequence."""
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ class Sample:
|
||||||
atom_density=None,
|
atom_density=None,
|
||||||
sample_volume=None,
|
sample_volume=None,
|
||||||
sample_length=None,
|
sample_length=None,
|
||||||
sample_diameter=None
|
sample_diameter=None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Constructs all the necessary attributes for the sample object.
|
Constructs all the necessary attributes for the sample object.
|
||||||
|
|
|
@ -9,6 +9,7 @@ logger = logging.getLogger(__name__)
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
logger.addHandler(logging.StreamHandler())
|
logger.addHandler(logging.StreamHandler())
|
||||||
|
|
||||||
|
|
||||||
class Simulation:
|
class Simulation:
|
||||||
"""Class for the simulation of the Bloch equations."""
|
"""Class for the simulation of the Bloch equations."""
|
||||||
|
|
||||||
|
@ -29,7 +30,6 @@ class Simulation:
|
||||||
temperature: float,
|
temperature: float,
|
||||||
loss_TX: float = 0,
|
loss_TX: float = 0,
|
||||||
loss_RX: float = 0,
|
loss_RX: float = 0,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Constructs all the necessary attributes for the simulation object.
|
Constructs all the necessary attributes for the simulation object.
|
||||||
|
@ -87,7 +87,9 @@ class Simulation:
|
||||||
def simulate(self):
|
def simulate(self):
|
||||||
reference_voltage = self.calculate_reference_voltage()
|
reference_voltage = self.calculate_reference_voltage()
|
||||||
|
|
||||||
B1 = self.calc_B1() * 1e3 # I think this is multiplied by 1e3 because everything is in mT
|
B1 = (
|
||||||
|
self.calc_B1() * 1e3
|
||||||
|
) # I think this is multiplied by 1e3 because everything is in mT
|
||||||
B1 = 17.3 # Something might be wrong with the calculation of the B1 field. This has to be checked.
|
B1 = 17.3 # Something might be wrong with the calculation of the B1 field. This has to be checked.
|
||||||
self.sample.gamma = self.sample.gamma * 1e-6 # We need our gamma in MHz / T
|
self.sample.gamma = self.sample.gamma * 1e-6 # We need our gamma in MHz / T
|
||||||
self.sample.T1 = self.sample.T1 * 1e3 # We need our T1 in ms
|
self.sample.T1 = self.sample.T1 * 1e3 # We need our T1 in ms
|
||||||
|
@ -104,7 +106,9 @@ class Simulation:
|
||||||
imag_pulsepower = imag_pulsepower * (1 - 10 ** (-self.loss_TX / 20))
|
imag_pulsepower = imag_pulsepower * (1 - 10 ** (-self.loss_TX / 20))
|
||||||
|
|
||||||
# Calculate the magnetization
|
# Calculate the magnetization
|
||||||
M_sy1 = self.bloch_symmetric_strang_splitting(B1, xdis, real_pulsepower, imag_pulsepower)
|
M_sy1 = self.bloch_symmetric_strang_splitting(
|
||||||
|
B1, xdis, real_pulsepower, imag_pulsepower
|
||||||
|
)
|
||||||
|
|
||||||
# Z-Component
|
# Z-Component
|
||||||
Mlong = np.squeeze(M_sy1[2, :, :]) # Indices start at 0 in Python
|
Mlong = np.squeeze(M_sy1[2, :, :]) # Indices start at 0 in Python
|
||||||
|
@ -112,7 +116,9 @@ class Simulation:
|
||||||
Mlong_avg = np.delete(Mlong_avg, -1) # Remove the last element
|
Mlong_avg = np.delete(Mlong_avg, -1) # Remove the last element
|
||||||
|
|
||||||
# XY-Component
|
# XY-Component
|
||||||
Mtrans = np.squeeze(M_sy1[0,:,:] + 1j*M_sy1[1,:,:]) # Indices start at 0 in Python
|
Mtrans = np.squeeze(
|
||||||
|
M_sy1[0, :, :] + 1j * M_sy1[1, :, :]
|
||||||
|
) # Indices start at 0 in Python
|
||||||
Mtrans_avg = np.mean(Mtrans, axis=0)
|
Mtrans_avg = np.mean(Mtrans, axis=0)
|
||||||
Mtrans_avg = np.delete(Mtrans_avg, -1) # Remove the last element
|
Mtrans_avg = np.delete(Mtrans_avg, -1) # Remove the last element
|
||||||
|
|
||||||
|
@ -129,8 +135,9 @@ class Simulation:
|
||||||
|
|
||||||
return timedomain_signal
|
return timedomain_signal
|
||||||
|
|
||||||
|
def bloch_symmetric_strang_splitting(
|
||||||
def bloch_symmetric_strang_splitting(self, B1, xdis, real_pulsepower, imag_pulsepower, relax = 1):
|
self, B1, xdis, real_pulsepower, imag_pulsepower, relax=1
|
||||||
|
):
|
||||||
"""This method simulates the Bloch equations using the symmetric strang splitting method.
|
"""This method simulates the Bloch equations using the symmetric strang splitting method.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
@ -155,7 +162,9 @@ class Simulation:
|
||||||
|
|
||||||
# Bloch simulation in magnetization domain
|
# Bloch simulation in magnetization domain
|
||||||
gadt = self.sample.gamma * dt / 2
|
gadt = self.sample.gamma * dt / 2
|
||||||
B1 = np.tile((gadt * (real_pulsepower - 1j * imag_pulsepower) * B1).reshape(-1, 1), Nx)
|
B1 = np.tile(
|
||||||
|
(gadt * (real_pulsepower - 1j * imag_pulsepower) * B1).reshape(-1, 1), Nx
|
||||||
|
)
|
||||||
K = gadt * xdis * w * self.gradient
|
K = gadt * xdis * w * self.gradient
|
||||||
phi = -np.sqrt(np.abs(B1) ** 2 + K**2)
|
phi = -np.sqrt(np.abs(B1) ** 2 + K**2)
|
||||||
|
|
||||||
|
@ -180,21 +189,44 @@ class Simulation:
|
||||||
M = np.zeros((3, Nx, Nu + 1))
|
M = np.zeros((3, Nx, Nu + 1))
|
||||||
M[:, :, 0] = M0
|
M[:, :, 0] = M0
|
||||||
Mt = M0
|
Mt = M0
|
||||||
D = np.diag([np.exp(-1 / self.sample.T2 * relax * dt), np.exp(-1 / self.sample.T2 * relax * dt), np.exp(-1 / self.sample.T1 * relax * dt)])
|
D = np.diag(
|
||||||
b = np.array([0, 0, self.initial_magnetization]) - np.array([0, 0, self.initial_magnetization * np.exp(-1 / self.sample.T1 * relax * dt)])
|
[
|
||||||
|
np.exp(-1 / self.sample.T2 * relax * dt),
|
||||||
|
np.exp(-1 / self.sample.T2 * relax * dt),
|
||||||
|
np.exp(-1 / self.sample.T1 * relax * dt),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
b = np.array([0, 0, self.initial_magnetization]) - np.array(
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
self.initial_magnetization * np.exp(-1 / self.sample.T1 * relax * dt),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
for n in range(Nu): # time loop
|
for n in range(Nu): # time loop
|
||||||
|
|
||||||
Mrot = np.zeros((3, Nx))
|
Mrot = np.zeros((3, Nx))
|
||||||
Mrot[0,:] = Bd1.T[:,n]*Mt[0,:] + Bd2.T[:,n]*Mt[1,:] + Bd3.T[:,n]*Mt[2,:]
|
Mrot[0, :] = (
|
||||||
Mrot[1,:] = Bd4.T[:,n]*Mt[0,:] + Bd5.T[:,n]*Mt[1,:] + Bd6.T[:,n]*Mt[2,:]
|
Bd1.T[:, n] * Mt[0, :] + Bd2.T[:, n] * Mt[1, :] + Bd3.T[:, n] * Mt[2, :]
|
||||||
Mrot[2,:] = Bd7.T[:,n]*Mt[0,:] + Bd8.T[:,n]*Mt[1,:] + Bd9.T[:,n]*Mt[2,:]
|
)
|
||||||
|
Mrot[1, :] = (
|
||||||
|
Bd4.T[:, n] * Mt[0, :] + Bd5.T[:, n] * Mt[1, :] + Bd6.T[:, n] * Mt[2, :]
|
||||||
|
)
|
||||||
|
Mrot[2, :] = (
|
||||||
|
Bd7.T[:, n] * Mt[0, :] + Bd8.T[:, n] * Mt[1, :] + Bd9.T[:, n] * Mt[2, :]
|
||||||
|
)
|
||||||
|
|
||||||
Mt = np.dot(D, Mrot) + np.tile(b, (Nx, 1)).T
|
Mt = np.dot(D, Mrot) + np.tile(b, (Nx, 1)).T
|
||||||
|
|
||||||
Mrot[0,:] = Bd1.T[:,n]*Mt[0,:] + Bd2.T[:,n]*Mt[1,:] + Bd3.T[:,n]*Mt[2,:]
|
Mrot[0, :] = (
|
||||||
Mrot[1,:] = Bd4.T[:,n]*Mt[0,:] + Bd5.T[:,n]*Mt[1,:] + Bd6.T[:,n]*Mt[2,:]
|
Bd1.T[:, n] * Mt[0, :] + Bd2.T[:, n] * Mt[1, :] + Bd3.T[:, n] * Mt[2, :]
|
||||||
Mrot[2,:] = Bd7.T[:,n]*Mt[0,:] + Bd8.T[:,n]*Mt[1,:] + Bd9.T[:,n]*Mt[2,:]
|
)
|
||||||
|
Mrot[1, :] = (
|
||||||
|
Bd4.T[:, n] * Mt[0, :] + Bd5.T[:, n] * Mt[1, :] + Bd6.T[:, n] * Mt[2, :]
|
||||||
|
)
|
||||||
|
Mrot[2, :] = (
|
||||||
|
Bd7.T[:, n] * Mt[0, :] + Bd8.T[:, n] * Mt[1, :] + Bd9.T[:, n] * Mt[2, :]
|
||||||
|
)
|
||||||
|
|
||||||
Mt = Mrot
|
Mt = Mrot
|
||||||
M[:, :, n + 1] = Mrot
|
M[:, :, n + 1] = Mrot
|
||||||
|
@ -209,7 +241,13 @@ class Simulation:
|
||||||
B1 : float
|
B1 : float
|
||||||
The B1 field of the solenoid coil in T."""
|
The B1 field of the solenoid coil in T."""
|
||||||
|
|
||||||
B1 = np.sqrt(2 * self.power_amplifier_power / 50) * np.pi * 4e-7 * self.number_turns / self.length_coil
|
B1 = (
|
||||||
|
np.sqrt(2 * self.power_amplifier_power / 50)
|
||||||
|
* np.pi
|
||||||
|
* 4e-7
|
||||||
|
* self.number_turns
|
||||||
|
/ self.length_coil
|
||||||
|
)
|
||||||
return B1
|
return B1
|
||||||
|
|
||||||
def calc_xdis(self) -> np.array:
|
def calc_xdis(self) -> np.array:
|
||||||
|
@ -229,7 +267,9 @@ class Simulation:
|
||||||
|
|
||||||
# xdis is a spatial function, but it is being repurposed here to convert through the gradient to a phase difference per time -> T2 dispersion of the isochromats
|
# xdis is a spatial function, but it is being repurposed here to convert through the gradient to a phase difference per time -> T2 dispersion of the isochromats
|
||||||
xdis = np.linspace(-1, 1, self.number_isochromats)
|
xdis = np.linspace(-1, 1, self.number_isochromats)
|
||||||
xdis = (foffr.T * 1e-6) / (self.sample.gamma / 2 / np.pi) / (self.gradient * 1e-3)
|
xdis = (
|
||||||
|
(foffr.T * 1e-6) / (self.sample.gamma / 2 / np.pi) / (self.gradient * 1e-3)
|
||||||
|
)
|
||||||
|
|
||||||
return xdis
|
return xdis
|
||||||
|
|
||||||
|
@ -243,11 +283,36 @@ class Simulation:
|
||||||
"""
|
"""
|
||||||
u = 4 * np.pi * 1e-7 # permeability of free space
|
u = 4 * np.pi * 1e-7 # permeability of free space
|
||||||
|
|
||||||
magnetization = self.sample.gamma * 2 * self.sample.atoms / (2 * self.sample.nuclear_spin +1) * h**2 * self.sample.resonant_frequency/ Boltzmann / self.temperature * self.sample.spin_factor
|
magnetization = (
|
||||||
|
self.sample.gamma
|
||||||
|
* 2
|
||||||
|
* self.sample.atoms
|
||||||
|
/ (2 * self.sample.nuclear_spin + 1)
|
||||||
|
* h**2
|
||||||
|
* self.sample.resonant_frequency
|
||||||
|
/ Boltzmann
|
||||||
|
/ self.temperature
|
||||||
|
* self.sample.spin_factor
|
||||||
|
)
|
||||||
|
|
||||||
coil_crossection = np.pi * (self.diameter_coil / 2) ** 2
|
coil_crossection = np.pi * (self.diameter_coil / 2) ** 2
|
||||||
reference_voltage = self.number_turns * coil_crossection * u * self.sample.gamma * 2 * self.sample.atoms / (2 * self.sample.nuclear_spin +1) * h**2 * self.sample.resonant_frequency **2 / Boltzmann / self.temperature * self.sample.spin_factor
|
reference_voltage = (
|
||||||
reference_voltage = reference_voltage * self.sample.powder_factor * self.sample.filling_factor
|
self.number_turns
|
||||||
|
* coil_crossection
|
||||||
|
* u
|
||||||
|
* self.sample.gamma
|
||||||
|
* 2
|
||||||
|
* self.sample.atoms
|
||||||
|
/ (2 * self.sample.nuclear_spin + 1)
|
||||||
|
* h**2
|
||||||
|
* self.sample.resonant_frequency**2
|
||||||
|
/ Boltzmann
|
||||||
|
/ self.temperature
|
||||||
|
* self.sample.spin_factor
|
||||||
|
)
|
||||||
|
reference_voltage = (
|
||||||
|
reference_voltage * self.sample.powder_factor * self.sample.filling_factor
|
||||||
|
)
|
||||||
return reference_voltage
|
return reference_voltage
|
||||||
|
|
||||||
def calculate_noise(self, timedomain_signal: np.array) -> np.array:
|
def calculate_noise(self, timedomain_signal: np.array) -> np.array:
|
||||||
|
@ -263,7 +328,9 @@ class Simulation:
|
||||||
noise_data : np.array
|
noise_data : np.array
|
||||||
The noise array that is added to the signal."""
|
The noise array that is added to the signal."""
|
||||||
n_timedomain_points = timedomain_signal.shape[0]
|
n_timedomain_points = timedomain_signal.shape[0]
|
||||||
noise_data = (self.noise * np.random.randn(self.averages, n_timedomain_points) + 1j * self.noise * np.random.randn(self.averages, n_timedomain_points))
|
noise_data = self.noise * np.random.randn(
|
||||||
|
self.averages, n_timedomain_points
|
||||||
|
) + 1j * self.noise * np.random.randn(self.averages, n_timedomain_points)
|
||||||
noise_data = np.sum(noise_data, axis=0) # Sum along the first axis
|
noise_data = np.sum(noise_data, axis=0) # Sum along the first axis
|
||||||
return noise_data
|
return noise_data
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,9 @@ from nqr_blochsimulator.classes.sample import Sample
|
||||||
from nqr_blochsimulator.classes.simulation import Simulation
|
from nqr_blochsimulator.classes.simulation import Simulation
|
||||||
from nqr_blochsimulator.classes.pulse import PulseArray
|
from nqr_blochsimulator.classes.pulse import PulseArray
|
||||||
|
|
||||||
|
|
||||||
class TestSimulation(unittest.TestCase):
|
class TestSimulation(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
||||||
self.sample = Sample(
|
self.sample = Sample(
|
||||||
"BiPh3",
|
"BiPh3",
|
||||||
density=1.585e6, # g/m^3
|
density=1.585e6, # g/m^3
|
||||||
|
@ -27,6 +27,7 @@ class TestSimulation(unittest.TestCase):
|
||||||
dwell_time = 1e-6
|
dwell_time = 1e-6
|
||||||
self.time_array = np.arange(0, simulation_length, dwell_time)
|
self.time_array = np.arange(0, simulation_length, dwell_time)
|
||||||
pulse_length = 3e-6
|
pulse_length = 3e-6
|
||||||
|
|
||||||
# Simple FID sequence with pulse length of 3µs
|
# Simple FID sequence with pulse length of 3µs
|
||||||
pulse_amplitude_array = np.zeros(int(simulation_length / dwell_time))
|
pulse_amplitude_array = np.zeros(int(simulation_length / dwell_time))
|
||||||
pulse_amplitude_array[: int(pulse_length / dwell_time)] = 1
|
pulse_amplitude_array[: int(pulse_length / dwell_time)] = 1
|
||||||
|
@ -35,7 +36,7 @@ class TestSimulation(unittest.TestCase):
|
||||||
self.pulse = PulseArray(
|
self.pulse = PulseArray(
|
||||||
pulseamplitude=pulse_amplitude_array,
|
pulseamplitude=pulse_amplitude_array,
|
||||||
pulsephase=pulse_phase_array,
|
pulsephase=pulse_phase_array,
|
||||||
dwell_time=dwell_time
|
dwell_time=dwell_time,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.simulation = Simulation(
|
self.simulation = Simulation(
|
||||||
|
@ -53,7 +54,7 @@ class TestSimulation(unittest.TestCase):
|
||||||
gain=6000,
|
gain=6000,
|
||||||
temperature=77,
|
temperature=77,
|
||||||
loss_TX=12,
|
loss_TX=12,
|
||||||
loss_RX=12
|
loss_RX=12,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_simulation(self):
|
def test_simulation(self):
|
||||||
|
|
Loading…
Reference in a new issue