nqr-blochsimulator/src/nqr_blochsimulator/classes/simulation.py
2023-08-22 16:06:38 +02:00

202 lines
7.3 KiB
Python

import numpy as np
from numpy import pi
from scipy import signal
class Simulation:
def __init__(self) -> None:
pass
def blochsim(sim_points, sim_time, reference, isochrom, sample, pulse):
# PRE-SETTINGS
d = {"M0c": 1} # initial mag
NISO = 100
if isochrom > 0:
NISO = isochrom # number of isochromates
nsamples = (
sim_points # number of sample/rasterization points for the calculation
)
sim_length = sim_time # in s; Not larger than the repetition time!
modulation = "OFF" # select a optional modulation of the pulse ['OFF','SIN']
# Replace by the NWA power later
B1c_calc = np.sqrt(2 * 500 / 50) * pi * 4e-7 * 9 / 6e-3 # 17 od 8.5
d["B1c"] = 17.3e-3 # for Peak B1 T %12.5%14.3
# SAMPLE SETTINGS
d["T1"] = sample["T1"] # in s; T1, T2
d["T2"] = sample["T2"] #
T2STAR = sample["T2s"] # only used for some calculations
d["gamma"] = (
sample["gamma"] / (2 * pi) / 1e6
) # gamma in MHz/T eg 5e6 % sample.gamma in rad/(T s) eg 0.8
d["relax"] = 1 # Flag 1: with Relax, 0 without Relax
# Parameter preparation
# clear up some unit problems
# DO NOT CHANGE
d["B1c"] = d["B1c"] * 1e3
d["T1"] = d["T1"] * 1e3
d["T2"] = d["T2"] * 1e3
d["gamma"] = d["gamma"] * 2 * pi
d["Nx"] = NISO
d["M0"] = np.array(
[np.zeros(NISO), np.zeros(NISO), np.ones(NISO)]
) # initial magnetization
d["dt"] = sim_length / nsamples # time step width
d["dt"] = (
d["dt"] * 1e3
) # again unit correction. could be changed if necessary, but other time factors
# in later calculations would have to be changed too
# Pulse Designer
u = np.zeros((nsamples, 1))
v = np.zeros((nsamples, 1))
w = np.ones((nsamples, 1))
tt = (np.array(range(1, nsamples + 1)) * d["dt"] - d["dt"]).reshape(
-1, 1
) # time axis in ms
# PULSE TEMPLATES
pulse_dur_pow_pha = pulse
num_pulses, _ = pulse_dur_pow_pha.shape
# loop through every pulse
for count in range(num_pulses):
pulse_begin = pulse_dur_pow_pha[count, 0]
pulse_end = pulse_dur_pow_pha[count, 1]
pha = pulse_dur_pow_pha[count, 3] * (2 * pi / 360) # phase in rad
ind_begin = np.argmin(np.abs(tt * 1e-3 - pulse_begin)) # minValue is unused
ind_end = np.argmin(np.abs(tt * 1e-3 - pulse_end))
ind_end = ind_end - 1
u_pow, v_pow = np.pol2cart(
pha, pulse_dur_pow_pha[count, 2]
) # theta angle; rho abs
if modulation == "OFF":
u[ind_begin:ind_end, 0] = u_pow # set real pulse power
v[ind_begin:ind_end, 0] = v_pow # set imag pulse power
elif modulation == "SIN":
u[ind_begin:ind_end, 0] = u_pow * np.sin(
(pi * 1e-3 / (pulse_end - pulse_begin)) * tt[ind_begin:ind_end, 0]
)
v[ind_begin:ind_end, 0] = v_pow * np.sin(
(pi * 1e-3 / (pulse_end - pulse_begin)) * tt[ind_begin:ind_end, 0]
)
# Some sidenotes that can be ignored
for count in range(1):
d["G3"] = 1 # mT/m, fhwm of 2mm Gradient scaling
w = w * d["G3"] # Gradient in mT/m
# Isochromatic simulaten by modeling with Lorentz distribution
Df = 1 / pi / T2STAR # FWHF of Lorentzian in Hz
foffr = []
uu = np.random.rand(NISO, 1) - 0.5
foffr = Df / 2 * np.tan(pi * uu) # cauchy distributed frequency offset
d["xdis"] = np.linspace(-1, 1, NISO) # in m spatial resolution
d["xdis"] = (
np.array(foffr) * 1e-6 / (d["gamma"] / 2 / pi) / (d["G3"] * 1e-3)
) # Conversion factors: foffr from Hz/T to MHz/T as required for d.gamma/2/pi, conversion from Hz-Gamma to radian gamma, and gradient must be scaled from mT/m to T/m
# USE BLOCH EQUATIONS
# M_sy1 = bloch_symmetric_strang_splitting_vectorised(u, v, w, d) # This function would need to be defined or imported
# Z-Component
# Mlong = np.squeeze(M_sy1[2, :, :]) # Coordinates M: space components - location(isochromat) - time
# Mlong_avg = np.mean(Mlong, 1)
# Mlong_avg = Mlong_avg[:-1]
# siglong = np.abs(Mlong_avg)
# XY-Component
# Mtrans = np.squeeze(M_sy1[0, :, :] + 1j*M_sy1[1, :, :]) # Coordinates M: space components - location(isochromat) - time
# Mtrans_avg = np.mean(Mtrans, 1)
# Mtrans_avg = Mtrans_avg[:-1]
# sigtrans = Mtrans_avg * reference
# return sigtrans
def bloch_symmetric_strang_splitting_vectorised(u, v, w, d):
"""Vectorised version of bloch_symmetric_strang_splitting
Parameters
----------
u : array_like
Real part of pulse
v : array_like
Imaginary part of pulse
w : array_like
Gradient
d : dict
"""
xdis = d["xdis"]
Nx = d["Nx"]
Nu = len(u)
M0 = d["M0"]
dt = d["dt"]
gadt = d["gamma"] * dt / 2
B1 = np.tile(gadt * np.transpose(u - 1j * v) * d["B1c"], (Nx, 1))
K = gadt * xdis * np.transpose(w) * d["G3"]
phi = -np.sqrt(np.abs(B1) ** 2 + K**2)
cs = np.cos(phi)
si = np.sin(phi)
n1 = np.real(B1) / np.abs(phi)
n2 = np.imag(B1) / np.abs(phi)
n3 = K / np.abs(phi)
n1[np.isnan(n1)] = 1
n2[np.isnan(n2)] = 0
n3[np.isnan(n3)] = 0
Bd1 = n1 * n1 * (1 - cs) + cs
Bd2 = n1 * n2 * (1 - cs) - n3 * si
Bd3 = n1 * n3 * (1 - cs) + n2 * si
Bd4 = n2 * n1 * (1 - cs) + n3 * si
Bd5 = n2 * n2 * (1 - cs) + cs
Bd6 = n2 * n3 * (1 - cs) - n1 * si
Bd7 = n3 * n1 * (1 - cs) - n2 * si
Bd8 = n3 * n2 * (1 - cs) + n1 * si
Bd9 = n3 * n3 * (1 - cs) + cs
M = np.zeros((3, Nx, Nu))
M[:, :, 0] = M0
Mt = M0
D = np.diag(
[
np.exp(-1 / d["T2"] * d["relax"] * dt),
np.exp(-1 / d["T2"] * d["relax"] * dt),
np.exp(-1 / d["T1"] * d["relax"] * dt),
]
)
b = np.array([0, 0, d["M0c"]]) - np.array(
[0, 0, d["M0c"] * np.exp(-1 / d["T1"] * d["relax"] * dt)]
)
for n in range(Nu): # Time loop
Mrot = np.array(
[
Bd1[:, n] * Mt[0, :] + Bd2[:, n] * Mt[1, :] + Bd3[:, n] * Mt[2, :],
Bd4[:, n] * Mt[0, :] + Bd5[:, n] * Mt[1, :] + Bd6[:, n] * Mt[2, :],
Bd7[:, n] * Mt[0, :] + Bd8[:, n] * Mt[1, :] + Bd9[:, n] * Mt[2, :],
]
)
Mt = np.dot(D, Mrot) + np.tile(b, (Nx, 1)).transpose()
Mrot = np.array(
[
Bd1[:, n] * Mt[0, :] + Bd2[:, n] * Mt[1, :] + Bd3[:, n] * Mt[2, :],
Bd4[:, n] * Mt[0, :] + Bd5[:, n] * Mt[1, :] + Bd6[:, n] * Mt[2, :],
Bd7[:, n] * Mt[0, :] + Bd8[:, n] * Mt[1, :] + Bd9[:, n] * Mt[2, :],
]
)
Mt = Mrot
M[:, :, n + 1] = Mrot
return M