def test_matrix_exponential(self): """Test the matrix exponential function matrix_exponential() with real matrices.""" # The 3D, rank-2 matrices. R1 = array([[1, 4, 5], [-4, 2, 6], [-5, -6, 3]], float64) R2 = array([[0, 1, 0], [0, 0, 0], [0, 0, 0]], float64) # The real matrix exponentials. eR1 = array([[-1.242955024379687, -3.178944439554645, 6.804083368075889], [-6.545353831891249, -2.604941866769356, 1.228233941393001], [ 0.975355249080821, -7.711099455690256, -3.318642157729292]], float64) eR2 = array([[ 1., 0., 0.], [ 0., 1., 0.], [ 0., 0., 1.]], float64) # The maths. eR1_test = matrix_exponential(R1) eR2_test = matrix_exponential(R2) # Printouts. print("Real matrix:\n%s" % eR1) print("Calculated matrix:\n%s" % eR1_test) # Checks. for i in range(3): for j in range(3): self.assertAlmostEqual(eR1_test[i, j], eR1[i, j]) self.assertAlmostEqual(eR2_test[i, j], eR2[i, j])
def test_matrix_exponential(self): """Test the matrix exponential function matrix_exponential() with real matrices.""" # The 3D, rank-2 matrices. R1 = array([[1, 4, 5], [-4, 2, 6], [-5, -6, 3]], float64) R2 = array([[0, 1, 0], [0, 0, 0], [0, 0, 0]], float64) # The real matrix exponentials. eR1 = array( [[-1.242955024379687, -3.178944439554645, 6.804083368075889], [-6.545353831891249, -2.604941866769356, 1.228233941393001], [0.975355249080821, -7.711099455690256, -3.318642157729292]], float64) eR2 = array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]], float64) # The maths. eR1_test = matrix_exponential(R1) eR2_test = matrix_exponential(R2) # Printouts. print("Real matrix:\n%s" % eR1) print("Calculated matrix:\n%s" % eR1_test) # Checks. for i in range(3): for j in range(3): self.assertAlmostEqual(eR1_test[i, j], eR1[i, j]) self.assertAlmostEqual(eR2_test[i, j], eR2[i, j])
def test_matrix_exponential2(self): """Test the matrix exponential function matrix_exponential() with complex matrices.""" # The 3D, rank-2 matrix. R1 = array([[ -0.024156250059605 + 0.j, 0.021093750372529 + 0.j ], [0.021093750372529 + 0.j, -0.024156250059605 - 0.587233662605286j]], complex64) # The real matrix exponentials. eR1 = array([[ 0.976344227790833 - 4.17836126871407e-05j, 0.0194285903126001 - 0.00587434694170952j ], [ 0.0194285865873098 - 0.00587435066699982j, 0.812806785106659 - 0.540918707847595j ]], complex64) # The maths. eR1_test = matrix_exponential(R1) # Printouts. print( "Real matrix:\n[%20.15g %20.15gj, %20.15g %20.15gj],\n[%20.15g %20.15gj, %20.15g %20.15gj]\n" % (eR1[0, 0].real, eR1[0, 0].imag, eR1[0, 1].real, eR1[0, 1].imag, eR1[1, 0].real, eR1[1, 0].imag, eR1[1, 1].real, eR1[1, 1].imag)) print( "Calculated matrix:\n[%20.15g %20.15gj, %20.15g %20.15gj],\n[%20.15g %20.15gj, %20.15g %20.15gj]\n" % (eR1_test[0, 0].real, eR1_test[0, 0].imag, eR1_test[0, 1].real, eR1_test[0, 1].imag, eR1_test[1, 0].real, eR1_test[1, 0].imag, eR1_test[1, 1].real, eR1_test[1, 1].imag)) # Checks. for i in range(2): for j in range(2): self.assertAlmostEqual(eR1_test[i, j].real, eR1[i, j].real, 5) self.assertAlmostEqual(eR1_test[i, j].imag, eR1[i, j].imag, 5)
def test_matrix_exponential2(self): """Test the matrix exponential function matrix_exponential() with complex matrices.""" # The 3D, rank-2 matrix. R1 = array([[-0.024156250059605+0.j, 0.021093750372529+0.j], [ 0.021093750372529+0.j, -0.024156250059605-0.587233662605286j]], complex64) # The real matrix exponentials. eR1 = array([[ 0.976344227790833 -4.17836126871407e-05j, 0.0194285903126001 -0.00587434694170952j], [ 0.0194285865873098 -0.00587435066699982j, 0.812806785106659 -0.540918707847595j]], complex64) # The maths. eR1_test = matrix_exponential(R1) # Printouts. print("Real matrix:\n[%20.15g %20.15gj, %20.15g %20.15gj],\n[%20.15g %20.15gj, %20.15g %20.15gj]\n" % (eR1[0, 0].real, eR1[0, 0].imag, eR1[0, 1].real, eR1[0, 1].imag, eR1[1, 0].real, eR1[1, 0].imag, eR1[1, 1].real, eR1[1, 1].imag)) print("Calculated matrix:\n[%20.15g %20.15gj, %20.15g %20.15gj],\n[%20.15g %20.15gj, %20.15g %20.15gj]\n" % (eR1_test[0, 0].real, eR1_test[0, 0].imag, eR1_test[0, 1].real, eR1_test[0, 1].imag, eR1_test[1, 0].real, eR1_test[1, 0].imag, eR1_test[1, 1].real, eR1_test[1, 1].imag)) # Checks. for i in range(2): for j in range(2): self.assertAlmostEqual(eR1_test[i, j].real, eR1[i, j].real, 5) self.assertAlmostEqual(eR1_test[i, j].imag, eR1[i, j].imag, 5)
def r2eff_ns_mmq_2site_mq(M0=None, F_vector=array([1, 0], float64), m1=None, m2=None, R20A=None, R20B=None, pA=None, pB=None, dw=None, dwH=None, k_AB=None, k_BA=None, inv_tcpmg=None, tcp=None, back_calc=None, num_points=None, power=None): """The 2-site numerical solution to the Bloch-McConnell equation for MQ data. The notation used here comes from: - Dmitry M. Korzhnev, Philipp Neudecker, Anthony Mittermaier, Vladislav Yu. Orekhov, and Lewis E. Kay (2005). Multiple-site exchange in proteins studied with a suite of six NMR relaxation dispersion experiments: An application to the folding of a Fyn SH3 domain mutant. J. Am. Chem. Soc., 127, 15602-15611. (doi: http://dx.doi.org/10.1021/ja054550e). and: - Dmitry M. Korzhnev, Philipp Neudecker, Anthony Mittermaier, Vladislav Yu. Orekhov, and Lewis E. Kay (2005). Multiple-site exchange in proteins studied with a suite of six NMR relaxation dispersion experiments: An application to the folding of a Fyn SH3 domain mutant. J. Am. Chem. Soc., 127, 15602-15611. (doi: http://dx.doi.org/10.1021/ja054550e). This function calculates and stores the R2eff values. @keyword M0: This is a vector that contains the initial magnetizations corresponding to the A and B state transverse magnetizations. @type M0: numpy float64, rank-1, 7D array @keyword F_vector: The observable magnitisation vector. This defaults to [1, 0] for X observable magnitisation. @type F_vector: numpy rank-1, 2D float64 array @keyword m1: A complex numpy matrix to be populated. @type m1: numpy rank-2, 2D complex64 array @keyword m2: A complex numpy matrix to be populated. @type m2: numpy rank-2, 2D complex64 array @keyword R20A: The transverse, spin-spin relaxation rate for state A. @type R20A: float @keyword R20B: The transverse, spin-spin relaxation rate for state B. @type R20B: float @keyword pA: The population of state A. @type pA: float @keyword pB: The population of state B. @type pB: float @keyword dw: The chemical exchange difference between states A and B in rad/s. @type dw: float @keyword dwH: The proton chemical exchange difference between states A and B in rad/s. @type dwH: float @keyword k_AB: The rate of exchange from site A to B (rad/s). @type k_AB: float @keyword k_BA: The rate of exchange from site B to A (rad/s). @type k_BA: float @keyword inv_tcpmg: The inverse of the total duration of the CPMG element (in inverse seconds). @type inv_tcpmg: float @keyword tcp: The tau_CPMG times (1 / 4.nu1). @type tcp: numpy rank-1 float array @keyword back_calc: The array for holding the back calculated R2eff values. Each element corresponds to one of the CPMG nu1 frequencies. @type back_calc: numpy rank-1 float array @keyword num_points: The number of points on the dispersion curve, equal to the length of the tcp and back_calc arguments. @type num_points: int @keyword power: The matrix exponential power array. @type power: numpy int16, rank-1 array """ # Populate the m1 and m2 matrices (only once per function call for speed). populate_matrix(matrix=m1, R20A=R20A, R20B=R20B, dw=-dw-dwH, k_AB=k_AB, k_BA=k_BA) # D+ matrix component. populate_matrix(matrix=m2, R20A=R20A, R20B=R20B, dw=dw-dwH, k_AB=k_AB, k_BA=k_BA) # Z- matrix component. # Loop over the time points, back calculating the R2eff values. for i in range(num_points): # The M1 and M2 matrices. M1 = matrix_exponential(m1*tcp[i]) # Equivalent to D+. M2 = matrix_exponential(m2*tcp[i]) # Equivalent to Z-. # The complex conjugates M1* and M2* M1_star = conj(M1) # Equivalent to D+*. M2_star = conj(M2) # Equivalent to Z-*. # Repetitive dot products (minimised for speed). M1_M2 = dot(M1, M2) M2_M1 = dot(M2, M1) M1_M2_M2_M1 = dot(M1_M2, M2_M1) M2_M1_M1_M2 = dot(M2_M1, M1_M2) M1_M2_star = dot(M1_star, M2_star) M2_M1_star = dot(M2_star, M1_star) M1_M2_M2_M1_star = dot(M1_M2_star, M2_M1_star) M2_M1_M1_M2_star = dot(M2_M1_star, M1_M2_star) # Special case of 1 CPMG block - the power is zero. if power[i] == 1: # M1.M2. A = M1_M2 # M1*.M2*. B = M1_M2_star # M2.M1. C = M2_M1 # M2*.M1*. D = M2_M1_star # Matrices for even number of CPMG blocks. elif power[i] % 2 == 0: # The power factor (only calculate once). fact = int(floor(power[i] / 2)) # (M1.M2.M2.M1)^(n/2). A = square_matrix_power(M1_M2_M2_M1, fact) # (M2*.M1*.M1*.M2*)^(n/2). B = square_matrix_power(M2_M1_M1_M2_star, fact) # (M2.M1.M1.M2)^(n/2). C = square_matrix_power(M2_M1_M1_M2, fact) # (M1*.M2*.M2*.M1*)^(n/2). D = square_matrix_power(M1_M2_M2_M1_star, fact) # Matrices for odd number of CPMG blocks. else: # The power factor (only calculate once). fact = int(floor((power[i] - 1) / 2)) # (M1.M2.M2.M1)^((n-1)/2).M1.M2. A = square_matrix_power(M1_M2_M2_M1, fact) A = dot(A, M1_M2) # (M1*.M2*.M2*.M1*)^((n-1)/2).M1*.M2*. B = square_matrix_power(M1_M2_M2_M1_star, fact) B = dot(B, M1_M2_star) # (M2.M1.M1.M2)^((n-1)/2).M2.M1. C = square_matrix_power(M2_M1_M1_M2, fact) C = dot(C, M2_M1) # (M2*.M1*.M1*.M2*)^((n-1)/2).M2*.M1*. D = square_matrix_power(M2_M1_M1_M2_star, fact) D = dot(D, M2_M1_star) # The next lines calculate the R2eff using a two-point approximation, i.e. assuming that the decay is mono-exponential. A_B = dot(A, B) C_D = dot(C, D) Mx = dot(dot(F_vector, (A_B + C_D)), M0) Mx = Mx.real / 2.0 if Mx <= 0.0 or isNaN(Mx): back_calc[i] = 1e99 else: back_calc[i]= -inv_tcpmg * log(Mx / pA)
def r2eff_ns_mmq_2site_sq_dq_zq(M0=None, F_vector=array([1, 0], float64), m1=None, m2=None, R20A=None, R20B=None, pA=None, pB=None, dw=None, dwH=None, k_AB=None, k_BA=None, inv_tcpmg=None, tcp=None, back_calc=None, num_points=None, power=None): """The 2-site numerical solution to the Bloch-McConnell equation for SQ, ZQ, and DQ data. The notation used here comes from: - Dmitry M. Korzhnev, Philipp Neudecker, Anthony Mittermaier, Vladislav Yu. Orekhov, and Lewis E. Kay (2005). Multiple-site exchange in proteins studied with a suite of six NMR relaxation dispersion experiments: An application to the folding of a Fyn SH3 domain mutant. J. Am. Chem. Soc., 127, 15602-15611. (doi: http://dx.doi.org/10.1021/ja054550e). This function calculates and stores the R2eff values. @keyword M0: This is a vector that contains the initial magnetizations corresponding to the A and B state transverse magnetizations. @type M0: numpy float64, rank-1, 7D array @keyword F_vector: The observable magnitisation vector. This defaults to [1, 0] for X observable magnitisation. @type F_vector: numpy rank-1, 2D float64 array @keyword m1: A complex numpy matrix to be populated. @type m1: numpy rank-2, 2D complex64 array @keyword m2: A complex numpy matrix to be populated. @type m2: numpy rank-2, 2D complex64 array @keyword R20A: The transverse, spin-spin relaxation rate for state A. @type R20A: float @keyword R20B: The transverse, spin-spin relaxation rate for state B. @type R20B: float @keyword pA: The population of state A. @type pA: float @keyword pB: The population of state B. @type pB: float @keyword dw: The combined chemical exchange difference between states A and B in rad/s. It should be set to dwH for 1H SQ data, dw for heteronuclear SQ data, dwH-dw for ZQ data, and dwH+dw for DQ data. @type dw: float @keyword dwH: Unused - this is simply to match the r2eff_ns_mmq_2site_mq() function arguments. @type dwH: float @keyword k_AB: The rate of exchange from site A to B (rad/s). @type k_AB: float @keyword k_BA: The rate of exchange from site B to A (rad/s). @type k_BA: float @keyword inv_tcpmg: The inverse of the total duration of the CPMG element (in inverse seconds). @type inv_tcpmg: float @keyword tcp: The tau_CPMG times (1 / 4.nu1). @type tcp: numpy rank-1 float array @keyword back_calc: The array for holding the back calculated R2eff values. Each element corresponds to one of the CPMG nu1 frequencies. @type back_calc: numpy rank-1 float array @keyword num_points: The number of points on the dispersion curve, equal to the length of the tcp and back_calc arguments. @type num_points: int @keyword power: The matrix exponential power array. @type power: numpy int16, rank-1 array """ # Populate the m1 and m2 matrices (only once per function call for speed). populate_matrix(matrix=m1, R20A=R20A, R20B=R20B, dw=dw, k_AB=k_AB, k_BA=k_BA) populate_matrix(matrix=m2, R20A=R20A, R20B=R20B, dw=-dw, k_AB=k_AB, k_BA=k_BA) # Loop over the time points, back calculating the R2eff values. for i in range(num_points): # The A+/- matrices. A_pos = matrix_exponential(m1*tcp[i]) A_neg = matrix_exponential(m2*tcp[i]) # The evolution for one n. evol_block = dot(A_pos, dot(A_neg, dot(A_neg, A_pos))) # The full evolution. evol = square_matrix_power(evol_block, power[i]) # The next lines calculate the R2eff using a two-point approximation, i.e. assuming that the decay is mono-exponential. Mx = dot(F_vector, dot(evol, M0)) Mx = Mx.real if Mx <= 0.0 or isNaN(Mx): back_calc[i] = 1e99 else: back_calc[i] = -inv_tcpmg * log(Mx / pA)
def ns_r1rho_2site(M0=None, matrix=None, r1rho_prime=None, omega=None, offset=None, r1=0.0, pA=None, pB=None, dw=None, k_AB=None, k_BA=None, spin_lock_fields=None, relax_time=None, inv_relax_time=None, back_calc=None, num_points=None): """The 2-site numerical solution to the Bloch-McConnell equation for R1rho data. This function calculates and stores the R1rho values. @keyword M0: This is a vector that contains the initial magnetizations corresponding to the A and B state transverse magnetizations. @type M0: numpy float64, rank-1, 7D array @keyword matrix: A numpy array to be populated to create the evolution matrix. @type matrix: numpy rank-2, 6D float64 array @keyword r1rho_prime: The R1rho_prime parameter value (R1rho with no exchange). @type r1rho_prime: float @keyword omega: The chemical shift for the spin in rad/s. @type omega: float @keyword offset: The spin-lock offsets for the data. @type offset: numpy rank-1 float array @keyword r1: The R1 relaxation rate. @type r1: float @keyword pA: The population of state A. @type pA: float @keyword pB: The population of state B. @type pB: float @keyword dw: The chemical exchange difference between states A and B in rad/s. @type dw: float @keyword k_AB: The rate of exchange from site A to B (rad/s). @type k_AB: float @keyword k_BA: The rate of exchange from site B to A (rad/s). @type k_BA: float @keyword spin_lock_fields: The R1rho spin-lock field strengths (in rad.s^-1). @type spin_lock_fields: numpy rank-1 float array @keyword relax_time: The total relaxation time period for each spin-lock field strength (in seconds). @type relax_time: float @keyword inv_relax_time: The inverse of the relaxation time period for each spin-lock field strength (in inverse seconds). This is used for faster calculations. @type inv_relax_time: float @keyword back_calc: The array for holding the back calculated R2eff values. Each element corresponds to one of the CPMG nu1 frequencies. @type back_calc: numpy rank-1 float array @keyword num_points: The number of points on the dispersion curve, equal to the length of the tcp and back_calc arguments. @type num_points: int """ # Repetitive calculations (to speed up calculations). Wa = omega # Larmor frequency [s^-1]. Wb = omega + dw # Larmor frequency [s^-1]. W = pA*Wa + pB*Wb # Population-averaged Larmor frequency [s^-1]. dA = Wa - offset # Offset of spin-lock from A. dB = Wb - offset # Offset of spin-lock from B. d = W - offset # Offset of spin-lock from population-average. # Loop over the time points, back calculating the R2eff values. for i in range(num_points): # The matrix that contains all the contributions to the evolution, i.e. relaxation, exchange and chemical shift evolution. rr1rho_3d(matrix=matrix, R1=r1, r1rho_prime=r1rho_prime, pA=pA, pB=pB, wA=dA, wB=dB, w1=spin_lock_fields[i], k_AB=k_AB, k_BA=k_BA) # The following lines rotate the magnetization previous to spin-lock into the weff frame. theta = atan(spin_lock_fields[i]/dA) M0[0] = sin(theta) # The A state initial X magnetisation. M0[2] = cos(theta) # The A state initial Z magnetisation. # This matrix is a propagator that will evolve the magnetization with the matrix R. Rexpo = matrix_exponential(matrix*relax_time) # Magnetization evolution. MA = dot(M0, dot(Rexpo, M0)) # The next lines calculate the R1rho using a two-point approximation, i.e. assuming that the decay is mono-exponential. if MA <= 0.0 or isNaN(MA): back_calc[i] = 1e99 else: back_calc[i]= -inv_relax_time * log(MA)
def r2eff_ns_cpmg_2site_3D(r180x=None, M0=None, r10a=0.0, r10b=0.0, r20a=None, r20b=None, pA=None, pB=None, dw=None, k_AB=None, k_BA=None, inv_tcpmg=None, tcp=None, back_calc=None, num_points=None, power=None): """The 2-site numerical solution to the Bloch-McConnell equation. This function calculates and stores the R2eff values. @keyword r180x: The X-axis pi-pulse propagator. @type r180x: numpy float64, rank-2, 7D array @keyword M0: This is a vector that contains the initial magnetizations corresponding to the A and B state transverse magnetizations. @type M0: numpy float64, rank-1, 7D array @keyword r10a: The R1 value for state A. @type r10a: float @keyword r10b: The R1 value for state B. @type r10b: float @keyword r20a: The R2 value for state A in the absence of exchange. @type r20a: float @keyword r20b: The R2 value for state B in the absence of exchange. @type r20b: float @keyword pA: The population of state A. @type pA: float @keyword pB: The population of state B. @type pB: float @keyword dw: The chemical exchange difference between states A and B in rad/s. @type dw: float @keyword k_AB: The rate of exchange from site A to B (rad/s). @type k_AB: float @keyword k_BA: The rate of exchange from site B to A (rad/s). @type k_BA: float @keyword inv_tcpmg: The inverse of the total duration of the CPMG element (in inverse seconds). @type inv_tcpmg: float @keyword tcp: The tau_CPMG times (1 / 4.nu1). @type tcp: numpy rank-1 float array @keyword back_calc: The array for holding the back calculated R2eff values. Each element corresponds to one of the CPMG nu1 frequencies. @type back_calc: numpy rank-1 float array @keyword num_points: The number of points on the dispersion curve, equal to the length of the tcp and back_calc arguments. @type num_points: int @keyword power: The matrix exponential power array. @type power: numpy int16, rank-1 array """ # The matrix R that contains all the contributions to the evolution, i.e. relaxation, exchange and chemical shift evolution. R = rcpmg_3d(R1A=r10a, R1B=r10b, R2A=r20a, R2B=r20b, pA=pA, pB=pB, dw=dw, k_AB=k_AB, k_BA=k_BA) # Loop over the time points, back calculating the R2eff values. for i in range(num_points): # Initial magnetisation. Mint = M0 # This matrix is a propagator that will evolve the magnetization with the matrix R for a delay tcp. Rexpo = matrix_exponential(R*tcp[i]) # Loop over the CPMG elements, propagating the magnetisation. for j in range(2*power[i]): Mint = dot(Rexpo, Mint) Mint = dot(r180x, Mint) Mint = dot(Rexpo, Mint) # The next lines calculate the R2eff using a two-point approximation, i.e. assuming that the decay is mono-exponential. Mx = fabs(Mint[1] / pA) if Mx <= 0.0 or isNaN(Mx): back_calc[i] = 1e99 else: back_calc[i]= -inv_tcpmg * log(Mx)
def r2eff_ns_cpmg_2site_star(Rr=None, Rex=None, RCS=None, R=None, M0=None, r20a=None, r20b=None, dw=None, inv_tcpmg=None, tcp=None, back_calc=None, num_points=None, power=None): """The 2-site numerical solution to the Bloch-McConnell equation using complex conjugate matrices. This function calculates and stores the R2eff values. @keyword Rr: The matrix that contains only the R2 relaxation terms ("Redfield relaxation", i.e. non-exchange broadening). @type Rr: numpy complex64, rank-2, 2D array @keyword Rex: The matrix that contains the exchange terms between the two states A and B. @type Rex: numpy complex64, rank-2, 2D array @keyword RCS: The matrix that contains the chemical shift evolution. It works here only with X magnetization, and the complex notation allows to evolve in the transverse plane (x, y). @type RCS: numpy complex64, rank-2, 2D array @keyword R: The matrix that contains all the contributions to the evolution, i.e. relaxation, exchange and chemical shift evolution. @type R: numpy complex64, rank-2, 2D array @keyword M0: This is a vector that contains the initial magnetizations corresponding to the A and B state transverse magnetizations. @type M0: numpy float64, rank-1, 2D array @keyword r20a: The R2 value for state A in the absence of exchange. @type r20a: float @keyword r20b: The R2 value for state B in the absence of exchange. @type r20b: float @keyword dw: The chemical exchange difference between states A and B in rad/s. @type dw: float @keyword inv_tcpmg: The inverse of the total duration of the CPMG element (in inverse seconds). @type inv_tcpmg: float @keyword tcp: The tau_CPMG times (1 / 4.nu1). @type tcp: numpy rank-1 float array @keyword back_calc: The array for holding the back calculated R2eff values. Each element corresponds to one of the CPMG nu1 frequencies. @type back_calc: numpy rank-1 float array @keyword num_points: The number of points on the dispersion curve, equal to the length of the tcp and back_calc arguments. @type num_points: int @keyword power: The matrix exponential power array. @type power: numpy int16, rank-1 array """ # The matrix that contains only the R2 relaxation terms ("Redfield relaxation", i.e. non-exchange broadening). Rr[0, 0] = -r20a Rr[1, 1] = -r20b # The matrix that contains the chemical shift evolution. It works here only with X magnetization, and the complex notation allows to evolve in the transverse plane (x, y). The chemical shift for state A is assumed to be zero. RCS[1, 1] = complex(0.0, -dw) # The matrix R that contains all the contributions to the evolution, i.e. relaxation, exchange and chemical shift evolution. R = add(Rr, Rex) R = add(R, RCS) # This is the complex conjugate of the above. It allows the chemical shift to run in the other direction, i.e. it is used to evolve the shift after a 180 deg pulse. The factor of 2 is to minimise the number of multiplications for the prop_2 matrix calculation. cR2 = conj(R) * 2.0 # Loop over the time points, back calculating the R2eff values. for i in range(num_points): # This matrix is a propagator that will evolve the magnetization with the matrix R for a delay tcp. eR_tcp = matrix_exponential(R*tcp[i]) # This is the propagator for an element of [delay tcp; 180 deg pulse; 2 times delay tcp; 180 deg pulse; delay tau], i.e. for 2 times tau-180-tau. prop_2 = dot(dot(eR_tcp, matrix_exponential(cR2*tcp[i])), eR_tcp) # Now create the total propagator that will evolve the magnetization under the CPMG train, i.e. it applies the above tau-180-tau-tau-180-tau so many times as required for the CPMG frequency under consideration. prop_total = square_matrix_power(prop_2, power[i]) # Now we apply the above propagator to the initial magnetization vector - resulting in the magnetization that remains after the full CPMG pulse train. It is called M of t (t is the time after the CPMG train). Moft = dot(prop_total, M0) # The next lines calculate the R2eff using a two-point approximation, i.e. assuming that the decay is mono-exponential. Mx = Moft[0].real / M0[0] if Mx <= 0.0 or isNaN(Mx): back_calc[i] = 1e99 else: back_calc[i]= -inv_tcpmg * log(Mx)