def __init__(self, shape='exp', theta=90, t_ramp=0.001): # ramp shape [string] self.shape = shape # relaxation during switch-off [logical] self.rds = False # pre-polarization B-field amplitude in units of [B0] self.factor = 100 # initial orientation between PP and B0 [deg] self._theta = theta # azimuthal angle of pre-polarization field [deg] self._phi = 0 # pre-polarization B-field switch amplitude in units of [B0] self.switch_factor = 1 # switch-off ramp time [s] self.t_ramp = t_ramp # switch-off ramp slope time [s] self.t_slope = t_ramp / 10 # calculate initial orientation self.orient = misc.get_orient_from_angles(self._theta, self._phi) # adiabatic quality p self.adiab_qual = None # plot data which is calculated later (only when needed) self.plot_t = None self.plot_amp = None self.plot_alpha = None self.plot_omega = None self.plot_dadt = None
def calc_plot_features(self): """Calculate all relevant ramp parameter for plotting.""" # create dummy time vector just for plotting time = np.linspace(0, self.Ramp.t_ramp, 1001) delta_t = time[1] - time[0] # prepare plot variables bp_amp = np.linspace(0, self.Ramp.t_ramp, 1001) alpha = np.linspace(0, self.Ramp.t_ramp, 1001) omega = np.linspace(0, self.Ramp.t_ramp, 1001) dadt = np.linspace(0, self.Ramp.t_ramp, 1001) for i in np.arange(0, len(time)): # get the PP B-field amplitude bp_amp[i] = self.B0 * self.Ramp.get_ramp_amplitude(time[i]) # Earth's magnetic field B0 b_earth = np.array([0., 0., self.B0]) # effective B-field Bp+B0 b_eff = bp_amp[i] * self.Ramp.orient + b_earth # amplitude of the effective B-field [T] b_eff_n = np.sqrt(b_eff[0]**2 + b_eff[1]**2 + b_eff[2]**2) # angle between primary and pre-polarization field [rad] alpha[i] = misc.get_angle_between_vectors(b_earth, b_eff) # angular frequency of the effective B-field [rad/s] omega[i] = self.gamma * b_eff_n if i > 0: # rate of change of the angle alpha [rad/s] dadt[i - 1] = np.abs((alpha[i] - alpha[i - 1]) / delta_t) self.Ramp.plot_t = time * 1e3 # in [ms] self.Ramp.plot_amp = bp_amp / self.B0 self.Ramp.plot_alpha = alpha self.Ramp.plot_omega = omega self.Ramp.plot_dadt = dadt
def plot_bloch_sphere(axis, lonlat, radius=1, lon_range=(-180.0, 180.0), lat_range=(-90.0, 90.0)): """Draw a unit sized Bloch Sphere.""" # from the lon and lat increments get the longitudinal and latitudinal # circles in xyz coordinates lons, lats = misc.get_sphere_grid(lonlat, radius=radius, lon_range=lon_range, lat_range=lat_range) # first draw the longitudinal circles xlin = lons[0] ylin = lons[1] zlin = lons[2] for i in np.arange(0, xlin.shape[0]): axis.plot(xlin[i, :], ylin[i, :], zlin[i, :], color='darkgray', linewidth=1) # then draw the latitudinal circles xlin = lats[0] ylin = lats[1] zlin = lats[2] for i in np.arange(0, xlin.shape[0]): axis.plot(xlin[i, :], ylin[i, :], zlin[i, :], color='darkgray', linewidth=1) # draw the inner lines axis.plot([-radius, radius], [0, 0], [0, 0], color='darkgray', linewidth=1) axis.plot([0, 0], [-radius, radius], [0, 0], color='darkgray', linewidth=1) axis.plot([0, 0], [0, 0], [-radius, radius], color='darkgray', linewidth=1) # draw the main axes axis.plot([0, radius * 1.2], [0, 0], [0, 0], color='r', linewidth=1.3) axis.plot([0, 0], [0, radius * 1.2], [0, 0], color='g', linewidth=1.3) axis.plot([0, 0], [0, 0], [0, radius * 1.2], color='b', linewidth=1.3) # draw the axis label axis.text(radius * 1.3, 0, 0, "X", color='r', ha='center') axis.text(0, radius * 1.3, 0, "Y", color='g', ha='center') axis.text(0, 0, radius * 1.3, "Z", color='b', ha='center') # set the limits axis.set_xlim(-1, 1) axis.set_ylim(-1, 1) axis.set_zlim(-1, 1) # set the view axis.view_init(azim=45, elev=30) # remove the axis lines axis.axis('off')
def bloch_fcn(self, time, mag): """Bloch equation to be solved by the ode-solver.""" if time <= self.Ramp.t_ramp: # get PP B-field amplitude # the Ramp amplitude is just a factor, hence the multiplication # with B0 to make it a value in [T] bp_amp = self.B0 * self.Ramp.get_ramp_amplitude(time) # Earth's magnetic field B0 b_earth = np.array([0., 0., self.B0]) # PP magnetic field is the Bp amplitude times the orientation # vector b_prepol = bp_amp * self.Ramp.orient # merge both B-fields b_total = b_prepol + b_earth # if rds is off scale the relaxation times and therewith basically # switch them off if self.Ramp.rds is True: T1 = self.T1 T2 = self.T2 else: T1 = self.T1 * 1e6 T2 = self.T2 * 1e6 # if the B-field vector is not parallel to B0 rotate B and m # into z-axis to apply relaxation b_total_n = b_total / np.linalg.norm(b_total) if np.any(b_total_n is not self.zunit): rot_mat = misc.get_rotmat_from_vectors(b_total, self.zunit) b_total = rot_mat.dot(b_total) mag = rot_mat.dot(mag) # dM/dt dmagdt = self.gamma * np.cross(mag, b_total) - \ np.array([mag[0], mag[1], 0.]) / T2 - \ np.array([0., 0., mag[2] - self.M0[2]]) / T1 rot_mat_transp = rot_mat.T dmagdt = rot_mat_transp.dot(dmagdt) else: # dM/dt dmagdt = self.gamma * np.cross(mag, b_total) - \ np.array([mag[0], mag[1], 0.]) / T2 - \ np.array([0., 0., mag[2] - self.M0[2]]) / T1 else: # assemble B field only from B0 b_total = np.array([0., 0., self.B0]) # dM/dt dmagdt = self.gamma * np.cross(mag, b_total) - \ np.array([mag[0], mag[1], 0.]) / self.T2 - \ np.array([0., 0., mag[2] - self.M0[2]]) / self.T1 return dmagdt
def solve(self, m_init, info=False, use_numba=False, atol=1e-9, rtol=1e-9): """Solve the Bloch equation with scipy 'solve_ivp'. Uses ‘RK45’: Explicit Runge-Kutta method of order 5(4) [1]. The error is controlled assuming accuracy of the fourth-order method, but steps are taken using the fifth-order accurate formula (local extrapolation is done). A quartic interpolation polynomial is used for the dense output [2]. Can be applied in the complex domain. References ---------- .. [1] J. R. Dormand, P. J. Prince, “A family of embedded Runge-Kutta formulae”, Journal of Computational and Applied Mathematics, Vol. 6, No. 1, pp. 19-26, 1980. .. [2] L. W. Shampine, “Some Practical Runge-Kutta Formulas”, Mathematics of Computation,, Vol. 46, No. 173, pp. 135-150, 1986. """ if use_numba: self.solver = solve_ivp(self.bloch_fcn_numba, [0, self.t_sim], m_init, rtol=rtol, atol=atol) else: self.solver = solve_ivp(self.bloch_fcn, [0, self.t_sim], m_init, rtol=rtol, atol=atol) if info: print(self.solver.message) if self.solver.success: self.t = self.solver.t.T self.m = self.solver.y # transform magnetization into rotating frame of reference self.inst_phase = self.larmor_w * self.t self.calc_m_rot() # calculate FFT of lab-frame magnetization freq, spec = misc.get_fft(self.t, self.m[[0, 1], :], True) self.fft_freq = freq self.fft_spec = spec else: raise ValueError("Something went south during integration.")
def solve(self, m_init, info=False, use_numba=False, atol=1e-9, rtol=1e-9): """Solve the Bloch equation with scipy 'solve_ivp'. Uses ‘RK45’: Explicit Runge-Kutta method of order 5(4) [1]. The error is controlled assuming accuracy of the fourth-order method, but steps are taken using the fifth-order accurate formula (local extrapolation is done). A quartic interpolation polynomial is used for the dense output [2]. Can be applied in the complex domain. References ---------- .. [1] J. R. Dormand, P. J. Prince, “A family of embedded Runge-Kutta formulae”, Journal of Computational and Applied Mathematics, Vol. 6, No. 1, pp. 19-26, 1980. .. [2] L. W. Shampine, “Some Practical Runge-Kutta Formulas”, Mathematics of Computation,, Vol. 46, No. 173, pp. 135-150, 1986. """ if use_numba: self.solver = solve_ivp(self.bloch_fcn_numba, [0, self.t_sim], m_init, rtol=rtol, atol=atol) else: self.solver = solve_ivp(self.bloch_fcn, [0, self.t_sim], m_init, rtol=rtol, atol=atol) if info: print(self.solver.message) if self.solver.success: self.t = self.solver.t.T self.m = self.solver.y # transform magnetization into rotating frame of reference # therefore calculate the instantaneous phase for all time steps # during the pulse if self.t_sim > self.Pulse.t_pulse: # standard phase self.inst_phase = self.larmor_w*self.t # auxiliary phase angle = 0 phi_aux = np.zeros(len(self.t)) # two logical mask for time steps during pulse (maks1) and # time steps after (mask2) mask1 = self.t <= self.Pulse.t_pulse mask2 = self.t > self.Pulse.t_pulse # calculate instantaneous phase during the pulse t_pulse = self.t[mask1] self.Pulse.fmod.get_modulated_phase(t_now=t_pulse) self.inst_phase[mask1] = self.Pulse.fmod.modulated_phase # auxiliary phase angle at the end of the pulse is used for # all time steps after the pulse phi_aux = np.zeros(len(self.t)) self.Pulse.fmod.get_modulated_phase(t_now=self.Pulse.t_pulse, flag=1) phi_aux[mask2] = self.Pulse.fmod.modulated_phase # now perform rot-frame transformation self.calc_m_rot(phi=phi_aux) # calculate pulse FFT b_pulse = self.Pulse.get_pulse_amplitude(t_pulse) freq, spec = misc.get_fft(t_pulse, b_pulse, True) self.Pulse.fft_freq = freq self.Pulse.fft_spec = spec else: # perform rot-frame transformation self.Pulse.fmod.t_now = self.t self.inst_phase = self.Pulse.fmod.modulated_phase self.calc_m_rot() # calculate pulse FFT b_pulse = self.Pulse.get_pulse_amplitude(self.t) freq, spec = misc.get_fft(self.t, b_pulse, True) self.Pulse.fft_freq = freq self.Pulse.fft_spec = spec # calculate FFT of lab-frame magnetization freq, spec = misc.get_fft(self.t, self.m[[0, 1], :], True) self.fft_freq = freq self.fft_spec = spec else: raise ValueError("Something went south during integration.")
def phi(self, value): self._phi = value self.orient = misc.get_orient_from_angles(self.theta, self._phi)
def theta(self, value): self._theta = value self.orient = misc.get_orient_from_angles(self._theta, self.phi)